# Automated issue-to-deploy pipeline End-to-end flow from a user clicking **Report Issue** in the portal to a fix running in production: ``` Portal header (bug icon) [App/components/layout/report-issue-button.tsx] │ server action → Forgejo API ▼ Forgejo issue (label: portal) [git.pelagiamarine.com/shad0w/pelagia-portal] │ polled every 10 min by Windows Scheduled Task "PelagiaClaudeIssueWatcher" ▼ TRIAGE (watcher phase 1) [dev PC, headless Claude Code, analysis only] │ Claude reads the issue + repo, posts a requirements-breakdown comment, │ and routes it: adds `claude-queue` (auto-fixable) or `interactive` (human) ▼ FIX (watcher phase 2, only for claude-queue) [headless Claude Code in C:\...\src\pelagia-autofix] │ Claude implements + verifies fix; watcher pushes branch claude/issue-N │ and opens a PR (label: claude-pr) ▼ Human review: merge the PR, then create a release tag vX.Y.Z │ tag push triggers .forgejo/workflows/deploy.yml ▼ forgejo-runner on pms1 (pm2: forgejo-runner, label "host") │ checks out the tag in ~/pms, pnpm install + build + prisma migrate deploy ▼ pm2 restart ppms → live at pms.pelagiamarine.com ``` `interactive`-routed issues stop after triage for a human to pick up (run with Claude in a steered session). The triage breakdown comment is plain (no bot marker) so, for `claude-queue` issues, the fix stage reads it back as refined requirements. ## Components | Piece | Where | Notes | |---|---|---| | Report Issue button | `App/components/layout/report-issue-button.tsx` + `report-issue-actions.ts` | Any signed-in user; files issue with only the `portal` label (triage routes it) | | Forgejo helper | `App/lib/forgejo.ts` | Needs `FORGEJO_URL`, `FORGEJO_REPO`, `FORGEJO_TOKEN` env (token scope: `write:issue`) | | Issue watcher (active) | `automation/claude-issue-watcher.sh` on pms1 | Bash port; runs 24/7 via cron. Config + logs under `~/issue-watcher/` | | Issue watcher (Windows, disabled) | `automation/claude-issue-watcher.ps1` | PowerShell original. `PelagiaClaudeIssueWatcher` task is **disabled** (pms1 is the sole worker; two pollers would race) | | Forgejo helper | `App/lib/forgejo.ts` | Needs `FORGEJO_URL`, `FORGEJO_REPO`, `FORGEJO_TOKEN` env (token scope: `write:issue`) | | Deploy workflow | `.forgejo/workflows/deploy.yml` | Triggers on `v*` tags; runs on the `host` runner | | Runner | pms1 `~/forgejo-runner`, pm2 process `forgejo-runner` | Registered as `pms1-host` with labels `host`, `docker` | ## Where the watcher runs (pms1) The watcher runs on **pms1** under cron (every 10 min), polling Forgejo over the local loopback (`http://127.0.0.1:3001`). - Script: `~/issue-watcher/claude-issue-watcher.sh` (source: `automation/claude-issue-watcher.sh`) - Config: `~/issue-watcher/watcher.config.json` (gitignored; holds the token + `claudeExe` = the nvm `claude` path) - Work clone: `~/pelagia-autofix` (separate from the deployed `~/pms`) - Logs: `~/issue-watcher/logs/` (`watcher-.log`, per-issue `claude-*.log`, `cron.log`) - Crontab: `*/10 * * * * PATH=:... ~/issue-watcher/claude-issue-watcher.sh >> ~/issue-watcher/logs/cron.log 2>&1` **Auth:** Claude Code must be signed in on pms1 (`ssh` in, run `claude`, complete the login → writes `~/.claude/.credentials.json`). The watcher has a preflight that no-ops until those credentials exist, so cron can be enabled before sign-in and activates automatically once signed in. (An `ANTHROPIC_API_KEY` env var also satisfies it.) The Windows variant (`.ps1` + `register-watcher-task.ps1`) is the portable fallback; re-enable its task only if pms1 is unavailable, and disable one before enabling the other. ## Issue label lifecycle ``` portal ──(triage)──▶ claude-queue ─▶ claude-working ─▶ claude-pr | claude-failed └────▶ interactive (stops here — handle with Claude interactively) ``` - A `portal` issue with no decision label yet is triaged once (`maxTriagePerRun` per run). Triage adds `claude-queue` or `interactive` and posts a breakdown. - `claude-queue` → `claude-working` → `claude-pr` (PR opened) or `claude-failed`. - To retry a failed issue, re-add `claude-queue`. - To queue any manually-created issue for Claude (skipping triage), add `claude-queue` directly. To force human handling, add `interactive`. - Triage is skipped for issues that already carry any decision label. ## Releasing After merging a Claude PR (or any change) on `master`: ```powershell git pull git tag v0.2.0 # semver: bump patch for fixes, minor for features git push pms1 master --tags ``` The runner deploys the tag and restarts the app. Watch progress under **Actions** on the Forgejo repo, or `pm2 logs forgejo-runner` on pms1. ## Operational notes - The watcher runs Claude Code with `--dangerously-skip-permissions` inside the dedicated `pelagia-autofix` clone — never point `workDir` at your main checkout. - Watcher only works issues while this PC is on; queued issues are picked up on the next run after boot (`-StartWhenAvailable`). - Tokens: `portal-report-issue` (write:issue, used by the app) and `claude-watcher` (write:issue + write:repository, used by the watcher). Both belong to the `shad0w` Forgejo account. Rotate via `docker exec -u 1000 forgejo forgejo admin user generate-access-token ...`. - Server-side env for the button lives in `~/pms/App/.env` on pms1 (`FORGEJO_URL=http://127.0.0.1:3001` so it does not depend on the tunnel). - **Known Forgejo 10 bug:** clicking *Update branch* on a PR (or pushing to its head branch) can make the page show "This pull request is broken due to missing fork information" even though the PR is fine (API still reports `mergeable: true`). Fix: close and reopen the PR — via the UI, or: ```powershell $h = @{ Authorization = "token " } Invoke-RestMethod -Method Patch -Headers $h -ContentType application/json ` -Uri https://git.pelagiamarine.com/api/v1/repos/shad0w/pelagia-portal/pulls/ -Body '{"state":"closed"}' Invoke-RestMethod -Method Patch -Headers $h -ContentType application/json ` -Uri https://git.pelagiamarine.com/api/v1/repos/shad0w/pelagia-portal/pulls/ -Body '{"state":"open"}' ``` Fixed upstream in newer Gitea/Forgejo — resolves itself if Forgejo is upgraded past v10.