- staging-up.sh binds the dev server to 127.0.0.1 (tunnel-only, no public access) and sets NEXT_PUBLIC_ENV_LABEL so the 'INTERNAL DEV / STAGING - NOT PRODUCTION' banner shows. - staging-tunnel.cmd: Windows launcher that opens the SSH tunnel + browser (wired to a desktop shortcut). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|---|---|---|
| .. | ||
| claude-issue-watcher.ps1 | ||
| claude-issue-watcher.sh | ||
| README.md | ||
| refresh-test-db.sh | ||
| register-watcher-task.ps1 | ||
| run-watcher.cmd | ||
| staging-tunnel.cmd | ||
| staging-up.sh | ||
| watcher.config.example.json | ||
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 nvmclaudepath) - Work clone:
~/pelagia-autofix(separate from the deployed~/pms) - Logs:
~/issue-watcher/logs/(watcher-<date>.log, per-issueclaude-*.log,cron.log) - Crontab:
*/10 * * * * PATH=<nvm bin>:... ~/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.
Test database (for autofix verification)
So the fix stage can verify against realistic data without touching production:
pelagia_test— a PostgreSQL database on pms1, owned bypelagia_user, that is a daily mirror of production (pelagia). Created once as superuser; refreshed byautomation/refresh-test-db.shvia cron at 03:30 (pg_dump pelagia | psql pelagia_test).- The autofix clone's
~/pelagia-autofix/App/.envpointsDATABASE_URLatpelagia_testand runs in safe dev mode — no Resend/SSO secrets, so email is console-logged and storage is local.NEXTAUTH_URL/PORTare set to 3100 (production app is on 3000). - The fix prompt tells Claude it may run integration tests against this DB
(
set -a; . ./.env; set +a; pnpm test:integration) and may start a dev server on port 3100 only, stopping it by port (fuser -k 3100/tcp) — never a broadpkill next, which would take down production (it also runs anext-server).
Because the test DB is refreshed daily, anything the autofix writes to it (test data,
schema experiments) is disposable. Schema-migration issues are routed to interactive
by triage, so the unattended fixer should not be altering the schema anyway.
Staging (smoke test before deploy)
automation/staging-up.sh (deployed to ~/issue-watcher/ on pms1) brings up a
staging instance of the latest master so changes can be clicked through
before a release tag deploys them to prod.
- Checkout:
~/pelagia-staging(separate from~/pmsand~/pelagia-autofix) - Process: pm2
ppms-stagingon port 3200, against the prod-mirror test DB (pelagia_test), safe dev mode (console email, local storage, SSO disabled). - Refresh to newer master + restart: re-run
~/issue-watcher/staging-up.sh. - Stop:
pm2 delete ppms-staging. - Access is SSH-tunnel only — the dev server binds to
127.0.0.1:3200, so it is not reachable from the public internet. Open a tunnel and browsehttp://localhost:3200:ssh -L 3200:localhost:3200 shad0w@<pms1>. On Windows, the desktop shortcut "Pelagia Staging (tunnel)" (automation/staging-tunnel.cmd) opens the tunnel and the browser in one click. - A fixed banner "INTERNAL DEV / STAGING - NOT PRODUCTION" is shown (driven by
NEXT_PUBLIC_ENV_LABELin the staging.env; theEnvBannercomponent renders nothing when the var is unset, so production is unaffected). - Log in with a password user (SSO is off here), e.g.
admin@pelagiamarine.com.
Issue label lifecycle
portal ──(triage)──▶ claude-queue ─▶ claude-working ─▶ claude-pr | claude-failed
└────▶ interactive (stops here — handle with Claude interactively)
- A
portalissue with no decision label yet is triaged once (maxTriagePerRunper run). Triage addsclaude-queueorinteractiveand posts a breakdown. claude-queue→claude-working→claude-pr(PR opened) orclaude-failed.- To retry a failed issue, re-add
claude-queue. - To queue any manually-created issue for Claude (skipping triage), add
claude-queuedirectly. To force human handling, addinteractive. - Triage is skipped for issues that already carry any decision label.
Releasing
After merging a Claude PR (or any change) on master:
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-permissionsinside the dedicatedpelagia-autofixclone — never pointworkDirat 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) andclaude-watcher(write:issue + write:repository, used by the watcher). Both belong to theshad0wForgejo account. Rotate viadocker exec -u 1000 forgejo forgejo admin user generate-access-token .... -
Server-side env for the button lives in
~/pms/App/.envon pms1 (FORGEJO_URL=http://127.0.0.1:3001so 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:$h = @{ Authorization = "token <claude-watcher token>" } Invoke-RestMethod -Method Patch -Headers $h -ContentType application/json ` -Uri https://git.pelagiamarine.com/api/v1/repos/shad0w/pelagia-portal/pulls/<N> -Body '{"state":"closed"}' Invoke-RestMethod -Method Patch -Headers $h -ContentType application/json ` -Uri https://git.pelagiamarine.com/api/v1/repos/shad0w/pelagia-portal/pulls/<N> -Body '{"state":"open"}'Fixed upstream in newer Gitea/Forgejo — resolves itself if Forgejo is upgraded past v10.