watch-deploy
Monitor (and, within bounds, remediate) a deploy triggered by a merge — staging automatically post-merge, production only after the owner approves the queued deploy. Polls the deploy run to a terminal state, classifies failures, attempts bounded fix-forward, and escalates with a precise diagnosis.
- License 许可
- Apache-2.0
- 最近更新
- 2026-09-21
- Stars
- ★ 0
- Forks
- 0
01 前言 Front Matter
- args
- env (stg|prod), optional triggering sha, optional run-id
- name
- watch-deploy
- description
- Monitor (and, within bounds, remediate) a deploy triggered by a merge — staging automatically post-merge, production only after the owner approves the queued deploy. Polls the deploy run to a terminal state, classifies failures, attempts bounded fix-forward, and escalates with a precise diagnosis.
03 SKILL.md 正文
Watch a deploy that a merge to main triggered, confirm it reaches a healthy terminal state, and fix-forward or escalate when it does not. This is the active counterpart to /wave-wrapup Step 11.6's passive "latest run is green" check: it follows the specific deploy a merge kicked off, rather than inspecting only the most-recent run.
Motivation: deploy#418 — a user-service merge silently broke the staging deploy at the image-pull step and only self-healed because an isnad-graph push happened to follow. Nothing watched the dispatched run or remediated. See also main#623 (this skill's tracking issue) and the P3W14-retro session-start "red default-branch publish" check, which is passive and does not follow a fresh deploy.
Note: all repo paths in bash blocks below are rooted at
$REPO_ROOTto avoid cwd drift when the skill is invoked from a worktree or child-repo subdirectory (#149).
When to use
- Automatically after a merge that triggers a staging deploy — any merge to
mainin a fan-in repo (noorinalabs-isnad-graph,noorinalabs-user-service) fires arepository_dispatchtonoorinalabs-deploy/.github/workflows/deploy-stg.yml. Call/watch-deploy stg <sha>to follow it. - From
/wave-wrapupStep 11.6 — once per wave→main merge in a deploy-triggering repo, so a wave is not declared green until its triggered deploys are confirmed healthy (not just the latest run on the board). - For production — only after the owner has approved the queued prod deploy. Prod deploys wait on a manual approval gate (owner directive 2026-06-09). This skill MUST NOT approve, trigger, or otherwise advance a prod deploy. Once the owner approves, call
/watch-deploy prod <sha>to monitor the approved run.
Environment contract
| env | Workflow (in noorinalabs-deploy) |
Trigger | This skill may act before owner? |
|---|---|---|---|
stg |
deploy-stg.yml |
auto — repository_dispatch from a service repo's ghcr-publish.yml after both images publish |
Yes — staging is auto-deploy; bounded remediation is allowed |
prod |
deploy-prod.yml |
workflow_dispatch from promote.yml, gated on owner approval |
No — never approve/trigger; monitor only after approval, never auto-remediate prod |
Instructions
1. Resolve the run to watch
REPO_ROOT="$(git rev-parse --show-toplevel)"
DEPLOY_REPO="noorinalabs/noorinalabs-deploy"
ENV="{stg|prod}" # first arg
SHA="{triggering sha or empty}" # optional second arg
RUN_ID="{run-id or empty}" # optional third arg
case "$ENV" in
stg) WORKFLOW="deploy-stg.yml"; RUN_NAME="Deploy to staging" ;;
prod) WORKFLOW="deploy-prod.yml"; RUN_NAME="Deploy to production" ;;
*) echo "ERROR: env must be stg or prod"; exit 1 ;;
esac
Find the run. Note two facts that make naive matching wrong:
- A
$WORKFLOWrun'sheadShais the deploy repo's sha, and itsdisplayTitleis the event-type (deploy-noorinalabs-<service>) — neither is the triggering service sha. So you cannot filtergh run listby the service sha. - The service sha lives in the dispatch
client_payload, which is not exposed bygh run list.
Correlate by time instead (deploys are serialized — deploy-stg.yml uses concurrency: deploy-staging, cancel-in-progress: false): the run a merge triggered is the earliest $WORKFLOW run whose createdAt is at/after the merge commit's timestamp.
if [ -n "$RUN_ID" ]; then
RUN=$(gh run view "$RUN_ID" --repo "$DEPLOY_REPO" --json databaseId,status,conclusion,url,headSha,createdAt)
elif [ -n "$SHA" ]; then
# Resolve the service commit's timestamp. We don't know which fan-in repo the
# sha belongs to, so probe them (the sha is unique across repos in practice).
MERGE_TS=""
for r in noorinalabs-isnad-graph noorinalabs-user-service; do
ts=$(gh api "repos/noorinalabs/$r/commits/$SHA" --jq '.commit.committer.date' 2>/dev/null || true)
[ -n "$ts" ] && { MERGE_TS="$ts"; SRC_REPO="$r"; break; }
done
if [ -z "$MERGE_TS" ]; then
echo "Could not resolve $SHA in any fan-in repo — falling back to latest $WORKFLOW run."
RUN=$(gh run list --repo "$DEPLOY_REPO" --workflow "$WORKFLOW" --limit 1 \
--json databaseId,status,conclusion,url,headSha,createdAt --jq '.[0] // empty')
else
echo "Merge $SHA in $SRC_REPO at $MERGE_TS — selecting earliest $WORKFLOW run at/after that."
RUN=$(gh run list --repo "$DEPLOY_REPO" --workflow "$WORKFLOW" --limit 30 \
--json databaseId,status,conclusion,url,headSha,createdAt \
--jq "[.[] | select(.createdAt >= \"$MERGE_TS\")] | sort_by(.createdAt) | .[0] // empty")
fi
else
# No sha — watch the latest run, and say so (not a specific merge's deploy).
echo "No sha given — watching the latest $WORKFLOW run."
RUN=$(gh run list --repo "$DEPLOY_REPO" --workflow "$WORKFLOW" --limit 1 \
--json databaseId,status,conclusion,url,headSha,createdAt --jq '.[0] // empty')
fi
[ -z "$RUN" ] && { echo "No $WORKFLOW run found to watch (env=$ENV, sha=${SHA:-none})."; exit 0; }
RUN_DB=$(echo "$RUN" | jq -r '.databaseId'); RUN_URL=$(echo "$RUN" | jq -r '.url')
echo "Watching: $RUN_URL"
For prod, the $WORKFLOW is dispatched by promote.yml (a workflow_dispatch, not a service-repo push), so the sha-correlation above does not apply — pass the RUN_ID of the approved prod run, or omit the sha to watch the latest prod run.
2. Production approval guard (prod only — HARD STOP)
For prod, before watching, confirm the run is past the owner-approval gate. If the run is waiting/action_required (pending approval), STOP and report — do not poll, do not nudge, do not approve. Surface the approval URL and wait for the owner.
if [ "$ENV" = "prod" ]; then
ST=$(echo "$RUN" | jq -r '.status')
if [ "$ST" = "waiting" ] || [ "$ST" = "action_required" ]; then
echo "PROD DEPLOY AWAITING OWNER APPROVAL — $RUN_URL"
echo "This skill will NOT approve or advance it. Re-invoke /watch-deploy prod once you have approved."
exit 0
fi
fi
3. Poll to a terminal state
Poll the run until status == completed (bounded wait; deploys are minutes, not hours). Prefer gh run watch where available; otherwise poll.
gh run watch "$RUN_DB" --repo "$DEPLOY_REPO" --exit-status >/dev/null 2>&1 || true
RUN=$(gh run view "$RUN_DB" --repo "$DEPLOY_REPO" --json status,conclusion,url,databaseId)
CONCLUSION=$(echo "$RUN" | jq -r '.conclusion')
echo "Terminal conclusion: $CONCLUSION — $RUN_URL"
If CONCLUSION == success: for stg, confirm the post-deploy verifier is also green (the Verify Deployment / health-check run). Report success and stop.
4. Classify the failure
If CONCLUSION != success, pull the failed step log and classify:
LOG=$(gh run view "$RUN_DB" --repo "$DEPLOY_REPO" --log-failed 2>/dev/null || true)
| Class | Signal in failed log | Meaning |
|---|---|---|
| image-not-found | failed to resolve reference, : not found, manifest unknown, or a MISS line from the deploy-stg pre-flight |
A resolved image tag is absent in GHCR (deploy#418 class) |
| health-check | health check failed, kafka healthcheck flap |
Container came up but a healthcheck did not pass (often the pipeline-only kafka flap — deploy#393 class, non-blocking) |
| migrate-gate | alembic, upgrade head, advisory-lock contention |
Pre-deploy DB migration failed |
| infra/ssh | ssh, connection refused, Permission denied, runner/network error |
Transient or host-level infra problem |
| other | none of the above | Unknown — escalate with the raw failing step |
5. Remediate (bounded) or escalate
Staging — bounded fix-forward is allowed:
| Class | Action |
|---|---|
| image-not-found | Re-dispatch the last-good stg images: gh workflow run deploy-stg.yml --repo "$DEPLOY_REPO" (defaults to stg-latest for every service). Then re-watch ONCE. If the original was a per-sha deploy whose image never published, also surface the upstream publish run so the real gap is filed. (Post-deploy#418 this class should be caught by the pre-flight with a readable MISS; if it still reaches a deploy, that is itself worth a follow-up.) |
| health-check | If it is the known kafka pipeline-only flap (deploy#393 class), note it as non-blocking and do not count it as a wave-blocking failure; confirm the app containers (api, user-service) are healthy. Otherwise re-watch ONCE; if still red, escalate. |
| migrate-gate | Do not auto-retry migrations. Escalate with the alembic error and the failing revision. |
| infra/ssh | Re-watch / re-dispatch ONCE (likely transient). If it recurs, escalate. |
| other | Escalate with the raw failing step. |
After one remediation attempt, re-resolve and re-poll (Steps 1–3) exactly once. Do not loop indefinitely — a second failure escalates.
Production — NO auto-remediation. On any prod failure: report the class + failing step, and recommend the owner-gated path (rollback.yml to the prior prod-<short>, or fix-forward + re-promote via promote.yml). Never trigger a prod redeploy or rollback yourself.
6. Report
**watch-deploy: {env} — {merge sha or "latest"}**
- Run: {url}
- Terminal: {success | <class> failure}
- Verifier (stg): {green | red | n/a}
- Remediation: {none | re-dispatched stg-latest, re-watched → green | escalated}
- Escalation: {none | <what the owner/operator must do>}
If a real defect surfaced (a publish gap, a recurring health failure, an infra problem), file it per /file-bug and link it. Do not silently swallow a failure that self-healed on retry — record it so the underlying flake is tracked.
What this skill does NOT do
- It never approves, triggers, or advances a production deploy — that gate is the owner's. It only monitors after approval.
- It does not loop more than one remediation attempt — a second failure is an escalation, not another retry.
- It does not edit code or workflows — fixes to the deploy mechanics themselves are filed as issues (e.g. deploy#418) and go through the normal PR path.
- It does not roll back production — it recommends
rollback.yml; the owner runs it.
04 同仓库更多 Skills
wave-wrapup
Finalize a wave — PR review, merge sequencing, issue cleanup, worktree cleanup, and handoff to retro
wave-start
Initialize a new wave — worktree cleanup, branch creation, label setup, status file update
wave-scope
Reconcile declared-vs-labeled wave scope between /wave-retro and /wave-kickoff — collect dispositions, fold in retro carry-forwards and memory must-includes, and refresh the meta-issue
wave-retro
Automated wave retrospective — PR analysis, assessments, trust matrix updates, feedback log, charter change proposals
wave-kickoff
Automated wave planning — branch creation, label management, issue labeling, kickoff comments, and execution plan
wave-audit
Audit open issues against merged PRs — find and close orphaned issues with proper comments