diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..2dc9a54 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,14 @@ +# Python files: LF +*.py text eol=lf + +# TypeScript/JSON: LF +*.ts text eol=lf +*.json text eol=lf +*.yaml text eol=lf +*.yml text eol=lf + +# Shell scripts: LF +*.sh text eol=lf + +# Docs: LF +*.md text eol=lf diff --git a/.gitea/pull_request_template.md b/.gitea/pull_request_template.md new file mode 100644 index 0000000..c661c4f --- /dev/null +++ b/.gitea/pull_request_template.md @@ -0,0 +1,31 @@ +## Description + + +Closes # + +## Type +- [ ] feat — new feature (MINOR version bump) +- [ ] fix — bug fix (PATCH version bump) +- [ ] docs — documentation only +- [ ] refactor — code restructuring (no behavior change) + +## Platform(s) Affected +- [ ] Hermes Python (Tanko, Mumuni, Koonimo, Koby) +- [ ] pi TypeScript (Abiba) +- [ ] Agent Zero (kagentz) +- [ ] Shared infrastructure (config, deploy, docs) + +## Pre-Merge Checklist +- [ ] Config schema validated against `config.yaml.example` +- [ ] Plugin starts and connects to Zulip on affected CT(s) +- [ ] Health endpoint returns 200 +- [ ] @mention detection works (bot receives own mentions) +- [ ] Error handling: timeout produces graceful message +- [ ] No API keys or secrets in code +- [ ] ADRs referenced if decision changed + +## Testing + + +## Screenshots / Logs + diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..557ab04 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,36 @@ +# Gitea Actions CI — zulip-platform-plugins +# Activates when Gitea Actions runners are configured. +# Until then, use manual pre-merge checklist in PR template. + +name: CI + +on: + pull_request: + branches: [main] + push: + branches: [main] + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Validate shared config schema + run: | + python3 -c "import yaml; cfg = yaml.safe_load(open('config.yaml.example')); assert 'agent' in cfg; assert 'zulip' in cfg" + + - name: Python lint (Hermes + Agent Zero) + run: | + pip install ruff + ruff check hermes-zulip-plugin/ agent-zero-plugin/ + + - name: TypeScript check (pi extension) + run: | + cd pi-zulip-extension + npm ci + npx tsc --noEmit + + - name: Check no secrets committed + run: | + ! grep -r "api_key.*[A-Za-z0-9]\{20,\}" --include="*.py" --include="*.ts" --include="*.yaml" . || echo "WARNING: possible API key in code" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..13ff510 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# Config files with secrets — NEVER commit +config.yaml +*.local.yaml + +# Dependencies +node_modules/ +__pycache__/ +*.pyc +.venv/ +venv/ + +# Build outputs +dist/ +*.tsbuildinfo + +# IDE +.vscode/ +.idea/ + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log +deploy.log diff --git a/docs/GITOPS.md b/docs/GITOPS.md new file mode 100644 index 0000000..06912ba --- /dev/null +++ b/docs/GITOPS.md @@ -0,0 +1,143 @@ +# GitOps Workflow — zulip-platform-plugins + +## Overview + +This document defines the development workflow from prototype to production maintenance for the multi-platform Zulip agent communication plugins. It replaces the current "everyone pushes to main" approach with a structured pipeline that supports swarm development across 6 CTs. + +## Branch Strategy + +``` +main ← PROTECTED — production code. PR + review required. + │ + ├── feat/* ← Feature branches (e.g., feat/issue-5-hermes-core) + ├── fix/* ← Bug fixes (e.g., fix/acl-spoofing) + ├── docs/* ← Documentation only + └── release/* ← Release candidates for staging validation +``` + +### Branch Naming Convention + +| Prefix | Purpose | Example | +|--------|---------|---------| +| `feat/` | New feature or enhancement | `feat/issue-5-hermes-core-adapter` | +| `fix/` | Bug fix | `fix/private-topic-acl-spoof` | +| `docs/` | Documentation changes | `docs/add-deployment-guide` | +| `release/` | Pre-production release candidate | `release/v1.0.0-rc1` | +| `hotfix/` | Emergency production fix | `hotfix/zulip-sdk-reconnect` | + +### Branch Protection (main) + +Apply via Gitea UI: `Settings > Branches > Add Rule` + +``` +Branch: main +- [x] Enable Push Restriction (restrict to repository admins only) +- [x] Require Pull Request to merge (approvals: 1) +- [x] Dismiss stale reviews when new commits are pushed +- [x] Block merge if status checks fail (when CI is added) +``` + +## Development Flow + +### Phase 1: Prototype (per-agent CT) + +``` +1. Agent clones repo on their CT +2. Creates feat/ branch from main +3. Implements plugin code +4. Tests locally on their CT (health endpoint, Zulip connect) +5. Opens PR to main +6. Review by Kwame or designated reviewer +7. Merge → main +``` + +### Phase 2: Integration Testing + +``` +1. Tag main: git tag -a v0.1.0 -m "Integration test release" +2. Push tag: git push origin v0.1.0 +3. Run: ./scripts/deploy.sh v0.1.0 +4. deploy.sh SSHs to each CT and checks out the tag +5. Run E2E test suite (#17) against deployed tag +6. If all pass → promote to v1.0.0 +``` + +### Phase 3: Production + +``` +1. Tag main: git tag -a v1.0.0 -m "Production release" +2. Push tag +3. Run: ./scripts/deploy.sh v1.0.0 +4. Verify health endpoints on all 6 CTs +5. Monitor Zulip #agent-hub for agent responses +``` + +### Phase 4: Maintenance + +``` +1. Bug reported → fix/ branch from main +2. Fix + test → PR → review → merge +3. Tag patch release: v1.0.1 +4. deploy.sh v1.0.1 (can be per-CT for targeted fixes) +5. Update ADRs if the fix changes a decision +``` + +## Versioning + +Semantic versioning: `MAJOR.MINOR.PATCH` + +| Bump | When | +|------|------| +| MAJOR | Breaking change to plugin contract, config schema, or Zulip API | +| MINOR | New feature, new ADR, new platform support | +| PATCH | Bug fix, doc update, config tweak | + +Tag format: `v{MAJOR}.{MINOR}.{PATCH}` — e.g., `v1.0.0` + +## CI/CD (Future — Gitea Actions) + +When Gitea Actions runners are configured, add `.gitea/workflows/ci.yml`: + +```yaml +on: [pull_request, push] +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Validate config schema + run: python3 -c "import yaml; yaml.safe_load(open('config.yaml.example'))" + - name: Lint Python + run: ruff check hermes-zulip-plugin/ agent-zero-plugin/ + - name: TypeScript check + run: cd pi-zulip-extension && npm ci && npx tsc --noEmit +``` + +## deploy.sh Contract + +`deploy.sh` accepts a tag or branch as its argument: + +```bash +./scripts/deploy.sh v1.0.0 # Deploy specific release +./scripts/deploy.sh main # Deploy latest (staging only) +./scripts/deploy.sh --ct=tanko v1.0.0 # Single-CT deploy +``` + +### Deploy behavior: +1. SSH into each CT +2. `cd /path/to/plugin && git fetch --tags && git checkout $TAG` +3. Install/update dependencies (pip install -r requirements.txt / npm ci) +4. Restart plugin service (systemctl restart zulip-plugin) +5. Verify health endpoint responds 200 +6. Log result to `deploy.log` + +## Rollback + +```bash +./scripts/rollback.sh v0.9.0 # Rollback all CTs to previous tag +./scripts/rollback.sh --ct=abiba v0.9.0 # Single-CT rollback +``` + +## Pre-Merge Checklist (PR Template) + +See `.gitea/pull_request_template.md` diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100755 index 0000000..19f8789 --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,134 @@ +#!/usr/bin/env bash +# deploy.sh — GitOps deployment for zulip-platform-plugins +# Usage: ./deploy.sh [--ct=] +# ./deploy.sh v1.0.0 # Deploy to all 6 CTs +# ./deploy.sh main # Deploy latest (staging only!) +# ./deploy.sh --ct=tanko v1.0.0 # Deploy to single CT +set -euo pipefail + +DEPLOY_LOG="deploy.log" +TAG="" +SINGLE_CT="" + +# Parse args +for arg in "$@"; do + case "$arg" in + --ct=*) SINGLE_CT="${arg#--ct=}" ;; + *) TAG="$arg" ;; + esac +done + +if [[ -z "$TAG" ]]; then + echo "Usage: $0 [--ct=]" + exit 1 +fi + +# Agent CT registry (must match CONTEXT.md) +declare -A AGENTS=( + ["tanko"]="amdpve CT 112" + ["mumuni"]="minipve CT 114" + ["koonimo"]="amdpve CT 113" + ["koby"]="amdpve CT 111" + ["kagentz"]="amdpve CT 105" + ["abiba"]="amdpve CT 100" +) + +# Platform paths per agent type +declare -A PLUGIN_PATHS=( + ["tanko"]="/opt/hermes-zulip-plugin" + ["mumuni"]="/opt/hermes-zulip-plugin" + ["koonimo"]="/opt/hermes-zulip-plugin" + ["koby"]="/opt/hermes-zulip-plugin" + ["kagentz"]="/opt/agent-zero-plugin" + ["abiba"]="/root/.pi/agent/extensions/zulip.ts" +) + +declare -A SERVICE_NAMES=( + ["tanko"]="zulip-plugin" + ["mumuni"]="zulip-plugin" + ["koonimo"]="zulip-plugin" + ["koby"]="zulip-plugin" + ["kagentz"]="zulip-plugin" + ["abiba"]="pi" # pi reload, not systemctl +) + +GITEA_REPO="https://git.sysloggh.net/SyslogSolution/zulip-platform-plugins.git" +HEALTH_PORT=9200 + +log() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$DEPLOY_LOG" +} + +deploy_agent() { + local agent="$1" + local ct_info="${AGENTS[$agent]}" + local plugin_path="${PLUGIN_PATHS[$agent]}" + local service="${SERVICE_NAMES[$agent]}" + + log "=== Deploying $agent ($ct_info) @ $TAG ===" + log "Path: $plugin_path" + + # 1. Git pull & checkout tag + cd "$plugin_path" || { log "ERROR: $agent — path $plugin_path not found"; return 1; } + git fetch --tags origin + git checkout "$TAG" + log "$agent: checked out $TAG" + + # 2. Install dependencies (platform-specific) + case "$agent" in + tanko|mumuni|koonimo|koby) + pip install -r requirements.txt --quiet + ;; + kagentz) + pip install -r requirements.txt --quiet + ;; + abiba) + # pi extensions are TypeScript — no pip install needed + # If npm deps needed: npm ci + log "$agent: pi extension — skipping pip install" + ;; + esac + + # 3. Restart service + case "$agent" in + abiba) + # pi reload via command, not systemctl + log "$agent: triggering /reload" + ;; + *) + systemctl restart "$service" + log "$agent: restarted $service" + ;; + esac + + # 4. Health check + sleep 3 + if curl -sf "http://localhost:$HEALTH_PORT/health" > /dev/null 2>&1; then + log "OK: $agent health check passed" + else + log "WARN: $agent health check failed — check logs" + return 1 + fi +} + +# --- Main --- +log "Deploy started — target: $TAG" + +if [[ -n "$SINGLE_CT" ]]; then + deploy_agent "$SINGLE_CT" +else + FAILED="" + for agent in tanko mumuni koonimo koby kagentz abiba; do + if ! deploy_agent "$agent"; then + FAILED="$FAILED $agent" + fi + done + + log "=== Deploy complete ===" + if [[ -n "$FAILED" ]]; then + log "FAILED:$FAILED" + log "Run rollback: ./scripts/rollback.sh " + exit 1 + fi + log "All 6 agents deployed successfully." +fi diff --git a/scripts/rollback.sh b/scripts/rollback.sh new file mode 100755 index 0000000..a3f3403 --- /dev/null +++ b/scripts/rollback.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# rollback.sh — Rollback zulip-platform-plugins to previous tag +# Usage: ./rollback.sh [--ct=] +set -euo pipefail + +TAG="${1:-}" +SINGLE_CT="" + +for arg in "$@"; do + case "$arg" in + --ct=*) SINGLE_CT="${arg#--ct=}" ;; + esac +done + +if [[ -z "$TAG" ]]; then + echo "Usage: $0 [--ct=]" + echo "Example: $0 v0.9.0 --ct=abiba" + exit 1 +fi + +echo "Rolling back to $TAG..." +./scripts/deploy.sh --ct="${SINGLE_CT:-all}" "$TAG"