mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-27 23:21:53 +02:00
* perf: remove repeated test work and preserve AUQ execution budgets * fix: validate native evaluation fixture evidence at its actual boundaries * fix: clarify deployment approval and recovery state transitions * chore: document coverage and release v1.90.2.0 * test: preserve Windows scheduling and native no-change consent * test: recognize verified reads through fixture symlinks * test: isolate alias-name installation from runtime assets
502 lines
23 KiB
Cheetah
502 lines
23 KiB
Cheetah
---
|
|
name: land-and-deploy
|
|
preamble-tier: 4
|
|
version: 1.0.0
|
|
description: |
|
|
Land and deploy workflow. Merges the PR, waits for CI and deploy,
|
|
verifies production health via canary checks. Takes over after /ship
|
|
creates the PR. Use when: "merge", "land", "deploy", "merge and verify",
|
|
"land it", "ship it to production". (gstack)
|
|
allowed-tools:
|
|
- Bash
|
|
- Read
|
|
- Write
|
|
- Glob
|
|
- AskUserQuestion
|
|
sensitive: true
|
|
triggers:
|
|
- merge and deploy
|
|
- land the pr
|
|
- ship to production
|
|
---
|
|
|
|
{{PREAMBLE}}
|
|
|
|
{{THIRD_PARTY_ACTIONS}}
|
|
|
|
{{ASIDE_SETUP}}
|
|
|
|
{{BROWSE_FALLBACK}}
|
|
|
|
{{BASE_BRANCH_DETECT}}
|
|
|
|
**If the platform detected above is GitLab or unknown:** STOP with: "GitLab support for /land-and-deploy is not yet implemented. Run `/ship` to create the MR, then merge manually via the GitLab web UI." Do not proceed.
|
|
|
|
# /land-and-deploy — Merge, Deploy, Verify
|
|
|
|
As **Release Engineer**, pick up the PR created by `/ship`: check readiness, merge
|
|
with approval, monitor deployment, verify production, and report evidence.
|
|
|
|
## User-invocable
|
|
When the user types `/land-and-deploy`, run this skill.
|
|
|
|
## Arguments
|
|
- `/land-and-deploy` — auto-detect PR from current branch, no post-deploy URL
|
|
- `/land-and-deploy <url>` — auto-detect PR, verify deploy at this URL
|
|
- `/land-and-deploy #123` — specific PR number
|
|
- `/land-and-deploy #123 <url>` — specific PR + verification URL
|
|
|
|
## Automation and approval
|
|
|
|
Automate read-only detection and polling. First-run setup confirmation (Step 1.5)
|
|
and pre-merge approval (Step 3.5) are mandatory when applicable. Stop on missing
|
|
access, unknown target/state, failing required CI, conflicts, or failing tests.
|
|
After any merge error, read server state before deciding whether to stop.
|
|
Failures, timeouts, staging choices, rollback, and optional cleanup use the explicit
|
|
decisions below; no approval overrides a blocker or authorizes a different revision.
|
|
|
|
## Voice & Tone
|
|
|
|
Narrate progress, name the actual app/PR and explain the stakes before asking.
|
|
First run: teach what each check does. Confirmed runs: brief status updates.
|
|
|
|
---
|
|
|
|
{{SECTION_INDEX:land-and-deploy}}
|
|
|
|
---
|
|
|
|
## Step 1: Pre-flight
|
|
|
|
Tell the user: "Checking access and finding your PR."
|
|
|
|
1. Check GitHub CLI authentication:
|
|
```bash
|
|
gh auth status
|
|
```
|
|
If unauthenticated, **STOP**; ask the user to run `gh auth login`, then retry.
|
|
|
|
2. Save any URL as `VERIFY_URL` (an explicit verification request). Set `PR_NUMBER`
|
|
to the numeric `#NNN` argument, or detect it once from the current branch:
|
|
```bash
|
|
REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner) || exit 1
|
|
if [ -z "$PR_NUMBER" ]; then
|
|
PR_NUMBER=$(gh pr view --repo "$REPO" --json number -q .number) || exit 1
|
|
fi
|
|
PR_JSON=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json number,state,title,url,mergeable,baseRefName,headRefName,headRefOid,baseRefOid) || exit 1
|
|
PR_HEAD=$(printf '%s' "$PR_JSON" | jq -er .headRefOid) || exit 1
|
|
HEAD_BRANCH=$(printf '%s' "$PR_JSON" | jq -er .headRefName) || exit 1
|
|
BASE_BRANCH=$(printf '%s' "$PR_JSON" | jq -er .baseRefName) || exit 1
|
|
```
|
|
Carry these values across fresh shells. Every later command targets this repository
|
|
and PR, never implicit current-branch detection. A failed query is unknown, not an
|
|
empty PR. Tell the user the selected number, title, head → base and head SHA.
|
|
|
|
3. No PR: **STOP**, suggest `/ship`. CLOSED: **STOP**, ask to reopen it. MERGED:
|
|
**STOP**, suggest `/canary <url>`; do not merge again or claim a deploy happened.
|
|
Only OPEN continues. Before any HEAD-based evidence, require the matching clean checkout:
|
|
```bash
|
|
LOCAL_HEAD=$(git rev-parse HEAD) || exit 1
|
|
LOCAL_BRANCH=$(git branch --show-current) || exit 1
|
|
LOCAL_STATUS=$(git status --porcelain) || exit 1
|
|
if [ "$LOCAL_HEAD" != "$PR_HEAD" ] || [ "$LOCAL_BRANCH" != "$HEAD_BRANCH" ] || [ -n "$LOCAL_STATUS" ]; then
|
|
echo "LOCAL_TARGET_MISMATCH"
|
|
exit 1
|
|
fi
|
|
git fetch "https://github.com/$REPO.git" "$BASE_BRANCH" || exit 1
|
|
BASE_SHA=$(git rev-parse FETCH_HEAD) || exit 1
|
|
SCOPE_RESULT=$(~/.claude/skills/gstack/bin/gstack-diff-scope "$BASE_SHA") || exit 1
|
|
eval "$SCOPE_RESULT"
|
|
```
|
|
On mismatch, **STOP** and ask the user to save their work, check out/update the PR
|
|
branch, and rerun. Do not switch, reset, or stash for them. Preserve `BASE_SHA`, the
|
|
PR's commit list and all scope flags before merging; cleanup may change HEAD afterward.
|
|
Unknown scope is not docs-only. `DOCS_ONLY=true` requires SCOPE_DOCS and no other scope.
|
|
|
|
---
|
|
|
|
## Step 1.5: First-run dry-run validation
|
|
|
|
Check for prior setup confirmation and changed configuration (not proof of a successful deploy):
|
|
|
|
```bash
|
|
{{SLUG_EVAL}}
|
|
if [ ! -f ~/.gstack/projects/$SLUG/land-deploy-confirmed ]; then
|
|
echo "FIRST_RUN"
|
|
else
|
|
# Check if deploy config has changed since confirmation
|
|
SAVED_HASH=$(cat ~/.gstack/projects/$SLUG/land-deploy-confirmed 2>/dev/null)
|
|
CURRENT_HASH=$(sed -n '/## Deploy Configuration/,/^## /p' CLAUDE.md 2>/dev/null | shasum -a 256 | cut -d' ' -f1)
|
|
# Also hash workflow files that affect deploy behavior
|
|
WORKFLOW_HASH=$(find .github/workflows -maxdepth 1 \( -name '*deploy*' -o -name '*cd*' \) 2>/dev/null | xargs cat 2>/dev/null | shasum -a 256 | cut -d' ' -f1)
|
|
COMBINED_HASH="${CURRENT_HASH}-${WORKFLOW_HASH}"
|
|
if [ "$SAVED_HASH" != "$COMBINED_HASH" ] && [ -n "$SAVED_HASH" ]; then
|
|
echo "CONFIG_CHANGED"
|
|
else
|
|
echo "CONFIRMED"
|
|
fi
|
|
fi
|
|
```
|
|
|
|
**If CONFIRMED:** Say "Setup was previously confirmed." Go to Step 2; do NOT read the dry-run section.
|
|
|
|
**If FIRST_RUN or CONFIG_CHANGED:** Read and execute the dry-run section:
|
|
|
|
{{SECTION:first-run-validation}}
|
|
|
|
Choice A saves the fingerprint and continues to Step 2; B/C stop.
|
|
|
|
---
|
|
|
|
## Step 2: Pre-merge checks
|
|
|
|
Tell the user: "Checking CI status and merge readiness..."
|
|
|
|
```bash
|
|
gh pr checks "$PR_NUMBER" --repo "$REPO" --required --json name,state,bucket,link
|
|
```
|
|
|
|
Parse valid JSON using `bucket` (pass/fail/pending/skipping/cancel). Exit 8 means
|
|
pending; a nonzero exit with valid failing checks is a CI failure. Auth/network/schema
|
|
errors are **STOP**, never "no required checks". An empty successful result or the
|
|
CLI's explicit "no required checks reported" response means none are configured.
|
|
1. Required checks **FAILING/cancelled**: **STOP**, list failures to fix.
|
|
2. Required checks **PENDING**: announce the wait and proceed to Step 3.
|
|
3. All pass (or none required): report that exact result. Skip only Step 3's wait;
|
|
continue to Step 3.4, then Step 3.5 before merging.
|
|
|
|
Also check for merge conflicts:
|
|
```bash
|
|
gh pr view "$PR_NUMBER" --repo "$REPO" --json mergeable -q .mergeable
|
|
```
|
|
If `CONFLICTING`: **STOP**, resolve conflicts first. Failed/UNKNOWN readback: **STOP**,
|
|
readiness is not established. Cancelled required checks are failures, not passes.
|
|
|
|
---
|
|
|
|
## Step 3: Wait for CI (if pending)
|
|
|
|
If required checks are still pending, wait for them to complete. Use a timeout of 15 minutes:
|
|
|
|
```bash
|
|
gh pr checks "$PR_NUMBER" --repo "$REPO" --required --watch --fail-fast --interval 30
|
|
```
|
|
|
|
Record the CI wait time for the deploy report.
|
|
|
|
Pass: report duration and continue to Step 3.4, then Step 3.5 before merging.
|
|
Failure: **STOP**, show failing checks. Timeout (15 minutes): **STOP**, point to
|
|
GitHub Actions. Enforce the deadline; do not leave an unbounded watch running.
|
|
|
|
---
|
|
|
|
## Step 3.4: VERSION drift detection (workspace-aware ship)
|
|
|
|
Check that another workspace has not claimed this PR's VERSION since `/ship`.
|
|
|
|
```bash
|
|
BRANCH_VERSION=$(git show "$PR_HEAD:VERSION" 2>/dev/null | tr -d '\r\n[:space:]')
|
|
BASE_VERSION=$(git show "$BASE_SHA:VERSION" 2>/dev/null | tr -d '\r\n[:space:]')
|
|
QUEUE_JSON=$(bun run ~/.claude/skills/gstack/bin/gstack-next-version \
|
|
--base "$BASE_BRANCH" \
|
|
--exclude-pr "$PR_NUMBER" \
|
|
--bump patch \
|
|
--current-version "$BASE_VERSION" 2>/dev/null || echo '{"offline":true}')
|
|
NEXT_SLOT=$(echo "$QUEUE_JSON" | jq -r '.version // empty')
|
|
OFFLINE=$(echo "$QUEUE_JSON" | jq -r '.offline // false')
|
|
```
|
|
|
|
Use the existing conservative patch-level allocation; compare numeric version
|
|
components, not lexical strings. If this project has no VERSION, report this check
|
|
not applicable. A missing/unparseable version on only one side is unavailable, not green.
|
|
|
|
1. `OFFLINE=true`, helper failure or invalid result: report VERSION check unavailable
|
|
with the reason; continue to Step 3.5. CI's version gate is the backstop.
|
|
2. `BRANCH_VERSION >= NEXT_SLOT`: no drift; continue.
|
|
3. `BRANCH_VERSION < NEXT_SLOT`: **STOP** with "VERSION drift detected", both versions
|
|
and instructions to rerun `/ship` from the feature branch. Its ALREADY_BUMPED path
|
|
reconciles VERSION, package.json, CHANGELOG header and PR title together. Do NOT
|
|
auto-bump or merge here: duplicate versions can overwrite another branch's release notes.
|
|
|
|
---
|
|
|
|
{{SECTION:readiness-gate}}
|
|
|
|
---
|
|
|
|
{{SECTION:merge-and-deploy}}
|
|
|
|
---
|
|
|
|
## Step 6: Wait for deploy (if applicable)
|
|
|
|
Unless returning for rollback, set `TARGET=production` and `DEPLOY_SHA=MERGE_SHA`. Use the deployment facts from
|
|
Steps 3.5/5; preserve status separately from canary health. A reachable URL alone
|
|
does not prove this revision deployed. No configured trigger: do not invent one.
|
|
|
|
### Strategy A: GitHub Actions workflow
|
|
|
|
If a deploy workflow was detected, find the run triggered by the merge commit:
|
|
|
|
```bash
|
|
gh run list --repo "$REPO" --branch "$BASE_BRANCH" --limit 10 --json databaseId,headSha,status,conclusion,name,workflowName
|
|
```
|
|
|
|
Match `DEPLOY_SHA`, workflow and target environment. If no run appears yet, repeat
|
|
the lookup within the same 20-minute deadline. A name match on another SHA is not evidence.
|
|
|
|
Poll every 30 seconds:
|
|
```bash
|
|
gh run view <run-id> --repo "$REPO" --json status,conclusion
|
|
```
|
|
|
|
### Strategy B: Platform CLI (Fly.io, Render, Heroku)
|
|
|
|
If a deploy status command was configured in CLAUDE.md (e.g., `fly status --app myapp`), use it instead of or in addition to GitHub Actions polling.
|
|
|
|
**Fly.io:** Check the configured app (do not issue `fly deploy`):
|
|
```bash
|
|
fly status --app {app} 2>/dev/null
|
|
```
|
|
Look for started Machines and a release tied to `DEPLOY_SHA`; time alone is not proof.
|
|
|
|
**Render:** Check its release record for the connected branch/revision, then reachability:
|
|
```bash
|
|
curl -sf {production-url} -o /dev/null -w "%{http_code}" 2>/dev/null
|
|
```
|
|
Poll every 30 seconds. HTTP 200 proves reachability, not which release is live.
|
|
|
|
**Heroku:** Check latest release:
|
|
```bash
|
|
heroku releases --app {app} -n 1 2>/dev/null
|
|
```
|
|
|
|
### Strategy C: Auto-deploy platforms (Vercel, Netlify)
|
|
|
|
When configured to auto-deploy on this merge, wait 60 seconds, inspect the deployment
|
|
record for `DEPLOY_SHA`, then Step 7. No record means deployment UNVERIFIED, not success.
|
|
|
|
### Strategy D: Custom deploy hooks
|
|
|
|
Run only the configured read-only status command. Check its exit code and revision
|
|
output; a generic health check cannot certify a new deployment.
|
|
|
|
### Common: Timing and failure handling
|
|
|
|
Record deploy start time. Show progress every 2 minutes: "Deploy is still running... ({X}m so far). This is normal for most platforms."
|
|
|
|
Matching revision successfully deployed: record `DEPLOY_STATUS=PASSED`, duration,
|
|
and evidence. Continue to Step 7, or Step 5's URL question if none is available.
|
|
|
|
If deploy fails/cancels: record `DEPLOY_STATUS=FAILED`, then use AskUserQuestion:
|
|
- **Re-ground:** "The deploy workflow failed after the merge. The code is merged but may not be live yet. Here's what I can do:"
|
|
- **RECOMMENDATION:** Choose A to investigate before reverting.
|
|
- A) Let me look at the deploy logs to figure out what went wrong
|
|
- B) Revert the merge immediately — roll back to the previous version
|
|
- C) Continue to health checks anyway — the deploy failure might be a flaky step, and the site might actually be fine
|
|
|
|
**A:** Read `gh run view <run-id> --repo "$REPO" --log-failed` (or configured platform
|
|
logs), summarize the cause and evidence limits, then ask: revert (Step 8), inspect
|
|
health (Step 7), or finish unverified (Step 9). No automatic code edits or redeploy.
|
|
**B:** Step 8. **C:** Step 7 if a URL exists, otherwise Step 5's URL question. A passing
|
|
canary never erases FAILED deployment evidence.
|
|
|
|
At 20 minutes (including waiting for a run to appear), ask: **A)** wait another bounded
|
|
20 minutes, **B)** finish without verification. A resets only the wait deadline and
|
|
resumes the same lookup/poll; B records pending/unknown deployment and goes to Step 9.
|
|
Status-query failure is unknown: show the error and offer the same bounded wait or
|
|
finish choices, not a fabricated success. During rollback monitoring, failure offers
|
|
logs or a pending report, never a second automatic revert.
|
|
|
|
---
|
|
|
|
## Step 7: Canary verification (conditional depth)
|
|
|
|
Tell the user which target/revision is confirmed or unverified, then check its URL.
|
|
If browser access is unavailable, record SKIPPED with the reason for this target.
|
|
Staging choice A returns to its production route; C goes to Step 9 without claiming
|
|
STAGING VERIFIED. Production goes to Step 9 with incomplete health evidence.
|
|
|
|
Use the saved pre-merge scope and Step 5's precedence rule; URL/triggered-deploy paths
|
|
still verify docs-only. Set `TARGET=production` unless entering from staging choice A/C.
|
|
|
|
| Diff Scope | Canary Depth |
|
|
|------------|-------------|
|
|
| SCOPE_DOCS only | Smoke when Step 5 routes here; otherwise skipped there |
|
|
| SCOPE_CONFIG only | Smoke: the Aside script below; `responseStatus` in `NAV=` must be 200 |
|
|
| SCOPE_BACKEND only | Console errors + perf check |
|
|
| SCOPE_FRONTEND (any) | Full: console + perf + screenshot |
|
|
| Mixed scopes | Full canary |
|
|
|
|
**Full canary sequence** — one `aside repl` script does the whole check (console hook first, then load, then evidence):
|
|
|
|
```bash
|
|
aside repl '
|
|
const HOOK = `(() => { window.__gstackErrs = window.__gstackErrs || []; const oe = console.error; console.error = (...a) => { window.__gstackErrs.push(a.map(String).join(" ")); oe.apply(console, a); }; window.addEventListener("error", e => window.__gstackErrs.push("uncaught: " + e.message)); window.addEventListener("unhandledrejection", e => window.__gstackErrs.push("unhandledrejection: " + (e.reason && e.reason.message || e.reason))); })()`;
|
|
const pg = await openTab("about:blank");
|
|
await pg._sendToTarget("Page.addScriptToEvaluateOnNewDocument", { source: HOOK });
|
|
await pg.goto("<url>");
|
|
console.log("URL=" + pg.url());
|
|
console.log("CONSOLE_ERRORS=" + JSON.stringify(await pg.evaluate(() => window.__gstackErrs)));
|
|
console.log("NAV=" + await pg.evaluate(() => JSON.stringify(performance.getEntriesByType("navigation")[0])));
|
|
console.log("TEXT_START"); console.log((await pg.evaluate(() => document.body.innerText)).slice(0, 20000)); console.log("TEXT_END");
|
|
await pg.screenshot({ path: "post-deploy.jpg", type: "jpeg", quality: 60, fullPage: true });
|
|
const a = await annotatedScreenshot(pg);
|
|
await fs.writeFile(path.join(pwd, "post-deploy-annotated.png"), Buffer.from(a.base64Image, "base64"));
|
|
console.log("ASIDE_DIR=" + pwd);
|
|
await closeTab(pg);
|
|
console.log("GSTACK_STEP_OK");
|
|
'
|
|
```
|
|
|
|
Then copy the evidence out of the printed session directory:
|
|
|
|
```bash
|
|
mkdir -p .gstack/deploy-reports && cp "<ASIDE_DIR>/post-deploy.jpg" "<ASIDE_DIR>/post-deploy-annotated.png" .gstack/deploy-reports/
|
|
```
|
|
|
|
Read the output line by line:
|
|
|
|
- `URL=` — the page loaded and stayed on the site (not a redirect to an error page). A line starting with `[error` or a missing `GSTACK_STEP_OK` means the load failed.
|
|
- `CONSOLE_ERRORS=` — check for critical errors: entries containing `Error`, `Uncaught`, `Failed to load`, `TypeError`, `ReferenceError`. Ignore warnings.
|
|
- `NAV=` — `responseStatus` is the HTTP status of the document (Chromium PerformanceNavigationTiming) — must be 200. `loadEventEnd` is the page load time. Check that it is under 10 seconds.
|
|
- `TEXT_START` / `TEXT_END` — verify the page has real content (not blank, not a generic error page).
|
|
- `post-deploy.jpg` and the annotated `post-deploy-annotated.png` are the evidence. Read the copied screenshot so the user sees it.
|
|
|
|
**Health assessment:**
|
|
- Page loads successfully with 200 status (`responseStatus` in `NAV=`) → PASS
|
|
- No critical console errors → PASS
|
|
- Page has real content (not blank or error screen) → PASS
|
|
- Loads in under 10 seconds → PASS
|
|
|
|
Assess only checks required by the selected depth; mark unperformed checks N/A.
|
|
All required checks pass: record HEALTHY for this target. Staging returns through
|
|
Step 5a's chosen A/C route; production goes to Step 9. Preserve deployment uncertainty.
|
|
|
|
If any fail: show the evidence (screenshot path, console errors, perf numbers). Use AskUserQuestion:
|
|
- **Re-ground:** "I found some issues on the live site after the deploy. Here's what I see: {specific issues}. This might be temporary (caches clearing, CDN propagating) or it might be a real problem."
|
|
- **RECOMMENDATION:** Choose based on severity — B for critical (site down), A for minor (console errors).
|
|
- A) Accept these observed issues for now — report DEGRADED, not healthy
|
|
- B) That's broken — revert the merge and roll back to the previous version
|
|
- C) Let me investigate more — open the site and look at logs before deciding
|
|
|
|
**A:** Record DEGRADED and the user's acknowledgment, then Step 9 (do not silently
|
|
continue from failed staging to production verification). **B:** Step 8, only with
|
|
explicit rollback approval. **C:** Inspect the page/evidence and read-only logs;
|
|
summarize findings, then ask for one recheck (repeat Step 7), rollback (Step 8), or
|
|
finish DEGRADED (Step 9). These investigations never modify or redeploy code.
|
|
When `ROLLBACK=true`, failures remain ROLLBACK PENDING; offer investigation or report,
|
|
not another revert. Keep staging/production screenshots distinct when checking both.
|
|
|
|
---
|
|
|
|
## Step 8: Revert (if needed)
|
|
|
|
Enter only after the user's explicit rollback choice. Explain that this adds inverse
|
|
commits; production is not restored until rollback deploys and health is checked.
|
|
Require a clean worktree, fetch `BASE_BRANCH` from `REPO`, switch to the local base
|
|
and fast-forward only to that fetched tip. Dirty, diverged, or occupied base: **STOP**
|
|
with ROLLBACK PENDING, never reset/force or discard work.
|
|
|
|
Inspect the actual landed commit, not just the requested merge method:
|
|
```bash
|
|
git show --no-patch --format='%H %P' "$MERGE_SHA"
|
|
```
|
|
- Two parents: verify parent 1 is the base-side history, then
|
|
`git revert -m 1 "$MERGE_SHA" --no-edit`.
|
|
- One-parent **confirmed squash**: `git revert "$MERGE_SHA" --no-edit`.
|
|
- **Rebase merge:** establish the exact landed commit range for this PR and revert
|
|
it newest-first. `mergeCommit.oid` alone is only the last commit, not the range.
|
|
Unknown range/method (including an external merge) or other parent shapes: **STOP**
|
|
with ROLLBACK PENDING and request manual rollback; do not guess.
|
|
|
|
Conflicts: stop, show `git status` and the attempted command, leave resolution to the
|
|
user. After a clean revert, record `REVERT_SHA` and push to the selected base:
|
|
`git push "https://github.com/$REPO.git" "HEAD:refs/heads/$BASE_BRANCH"`. If branch
|
|
protection rejects it, keep the commit, create `revert/pr-<number>-<timestamp>` there,
|
|
push that branch and open a revert PR against `BASE_BRANCH`. Report its URL and
|
|
ROLLBACK PENDING; never merge it without separate approval. Other push errors stop
|
|
with the error and pending status, not a protection bypass.
|
|
|
|
After a successful base push, set `ROLLBACK=true`, `TARGET=production`,
|
|
`DEPLOY_SHA=REVERT_SHA`, and reset production deployment/health to UNKNOWN/SKIPPED
|
|
for that revision. Keep original/staging evidence separately. Monitor via Steps 6-7
|
|
without resetting those values. Only a confirmed rollback deployment
|
|
and healthy production canary yields REVERTED (or a confirmed base revert where no
|
|
deploy is required). All incomplete, failed, skipped or PR-based rollback paths go
|
|
to Step 9 as ROLLBACK PENDING. Preserve the original merge SHA in the report.
|
|
|
|
---
|
|
|
|
## Step 9: Deploy report
|
|
|
|
Choose the first matching verdict; never infer deployment success from merge or HTTP 200:
|
|
|
|
| Evidence | Verdict |
|
|
|----------|---------|
|
|
| Rollback requested, not yet confirmed on base and live/healthy (when deploy applies) | ROLLBACK PENDING |
|
|
| Rollback confirmed as described in Step 8 | REVERTED |
|
|
| Any accepted target-health failure | DEGRADED |
|
|
| User chose staging-only and staging passed | STAGING VERIFIED — PRODUCTION UNVERIFIED |
|
|
| Explicit no-deploy confirmation or Step 5's docs-only skip | MERGED — NO DEPLOY NEEDED |
|
|
| Matching production deployment PASSED and production HEALTHY | DEPLOYED AND VERIFIED |
|
|
| Matching production deployment PASSED but canary skipped/unavailable | DEPLOYED (UNVERIFIED) |
|
|
| Everything else, including failed/pending/unknown deploy even with a healthy old site | MERGED (UNVERIFIED) |
|
|
|
|
Display **LAND & DEPLOY REPORT** and save `.gstack/deploy-reports/{date}-pr{number}-deploy.md`
|
|
(`date` = UTC YYYY-MM-DD). Include PR/title/repository, head → base, approved head,
|
|
merge timestamp/SHA/method/path, first-run status, CI/review status and warnings,
|
|
scope, separate deploy/staging/canary outcomes with evidence links/errors, console
|
|
count, load time, screenshot paths (N/A when not checked), verdict and next action.
|
|
Record dry-run, CI wait, queue, deploy, staging, canary and total durations in seconds;
|
|
skipped stages have duration 0 with a reason, never a fabricated pass. Inline review
|
|
is passed/skipped/not-needed; inline fixes stopped before merge and cannot appear here.
|
|
For rollback include revert SHA or PR URL and unresolved work.
|
|
|
|
```bash
|
|
mkdir -p .gstack/deploy-reports
|
|
{{SLUG_EVAL}}
|
|
mkdir -p ~/.gstack/projects/$SLUG
|
|
```
|
|
|
|
Pass one JSON entry to `~/.claude/skills/gstack/bin/gstack-review-log '<JSON>'` for
|
|
the dashboard's branch-scoped JSONL log. `status` is SUCCESS
|
|
only for DEPLOYED AND VERIFIED or MERGED — NO DEPLOY NEEDED, REVERTED for confirmed
|
|
rollback, otherwise INCOMPLETE. Keep the full `verdict` and independent evidence states:
|
|
```json
|
|
{"skill":"land-and-deploy","timestamp":"<ISO>","status":"<SUCCESS/REVERTED/INCOMPLETE>","verdict":"<verdict>","pr":<number>,"merge_sha":"<sha>","merge_path":"<auto/direct/queue/external>","first_run":<true/false>,"deploy_status":"<PASSED/FAILED/PENDING/UNKNOWN/NOT_NEEDED>","verification":"<HEALTHY/DEGRADED/SKIPPED>","staging_status":"<VERIFIED/DEGRADED/SKIPPED/N/A>","review_status":"<observed status>","dry_run_s":<N>,"ci_wait_s":<N>,"queue_s":<N>,"deploy_s":<N>,"staging_s":<N>,"canary_s":<N>,"total_s":<N>}
|
|
```
|
|
|
|
---
|
|
|
|
## Step 10: Suggest follow-ups
|
|
|
|
State the verdict in plain English. Verified: changes are live. Unverified/degraded:
|
|
name the missing evidence/issues and the exact workflow/status command or `/canary <url>`
|
|
to check next. No deploy needed: merged, verification skipped for the stated reason.
|
|
Staging-only: production remains unverified, not necessarily undeployed. Rollback
|
|
pending: identify who must resolve conflicts, merge the revert PR, or verify its deploy.
|
|
REVERTED: cite rollback evidence; do not claim the original branch survived cleanup.
|
|
Offer `/canary <url>` for extended monitoring, `/benchmark <url>` when performance
|
|
matters, and `/document-release` when docs need updating.
|
|
|
|
---
|
|
|
|
## Section self-check (before you finish)
|
|
|
|
You ran a carved skill. For your situation, list every section the Section index
|
|
named as applying, and confirm you issued a Read for each one (a CONFIRMED Step 1.5
|
|
correctly skips the dry-run section). Missing Read: STOP and read the source now.
|
|
Recheck read-only evidence; never redo a merge/deploy because a section was missed.
|
|
|
|
---
|
|
|
|
## Important Rules
|
|
|
|
- Never force-push, bypass CI, replay a confirmed merge, or hide missing evidence.
|
|
- Auto-detect facts; ask when unknown or when an explicit approval gate applies.
|
|
- Poll at 30-second intervals with the stated deadlines and progress messages.
|
|
- After merge failures, offer approved rollback when appropriate; never revert a rollback automatically.
|
|
- Verify once; `/canary` provides extended monitoring. Rechecks require the user's choice.
|
|
- Use `--delete-branch`; reconcile failed cleanup non-destructively with confirmation.
|