The map of nuxt-crouton's GitHub workflows and the deploy-pipeline anatomy — what each workflow gates, how a merge reaches Cloudflare, which secrets/tokens exist where, and how the agent-pipeline workflows chain. Use when a CI check fails and you need to know what it is, when asked "which workflow deploys X / why did staging update / what does this check gate / where is that secret / why didn't my label trigger anything", when wiring a new app into CI, or when debugging a deploy that never fi...
Scanned 9/12/2026
Install to Claude Code
npx -y skills add FriendlyInternet/nuxt-crouton --skill crouton-ci-and-deploy-map --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Crouton Ci And Deploy Map?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/friendlyinternet-crouton-ci-and-deploy-map)More formats (shields.io, HTML) on the badges page.
---
name: crouton-ci-and-deploy-map
layer: stack
description: The map of nuxt-crouton's GitHub workflows and the deploy-pipeline anatomy — what each workflow gates, how a merge reaches Cloudflare, which secrets/tokens exist where, and how the agent-pipeline workflows chain. Use when a CI check fails and you need to know what it is, when asked "which workflow deploys X / why did staging update / what does this check gate / where is that secret / why didn't my label trigger anything", when wiring a new app into CI, or when debugging a deploy that never fired. Trigger phrases: "what runs on merge to main", "map of the workflows", "WORKER_SECRETS_JSON", "why is the daily sweep not running", "deploy-apps vs deploy-app", "which token does this workflow use".
---
# CI & Deploy Map
One-line purpose: the ground-truth inventory of `.github/workflows/` (count it: `ls .github/workflows | wc -l`) plus the anatomy of how code reaches Cloudflare Workers — so you can answer "what fires when, gated by what, authenticated how" without re-reading every workflow.
Context for newcomers: this monorepo deploys Nuxt apps to **Cloudflare Workers** (never Pages — older docs lie). Trunk is `main`; merging to `main` auto-deploys **staging** (`<app>.pmcp.dev`); **production** (`<app>.friendlyinter.net`) is reachable only via a deliberate `workflow_dispatch` (#318). Much of CI is an agent pipeline: GitHub-issue-driven Claude runs that decompose epics, work leaves, and gate PRs.
## When to use / when NOT to use
| You want to... | Use instead |
|---|---|
| Actually deploy an `apps/*` app to staging (steps, bootstrap, troubleshooting) | `deploy` skill |
| Ship to production | `deploy-production` skill |
| Give a `pocs/*` app a preview URL | `poc-deploy` skill |
| Tear an app + its CF resources down | `remove-app` skill |
| Generate/apply DB migrations, "No schema files found" | `db-migrations` skill |
| Mirror a D1 database between envs | `db-clone` skill |
| Know which sign-off gate applies to your change and how approval works | `crouton-change-control` sibling |
| Observe a deployed app (tail, smoke, screenshots) | `crouton-run-and-operate` sibling |
| Look up a specific error string | `crouton-diagnostics-index` sibling |
This skill is the **map**: inventory, anatomy, secrets, cadences, and the drift between docs and reality. It carries no deploy runbook steps.
## 1. Workflow inventory
Grouped by role; the file count drifts — count it fresh (`ls .github/workflows | wc -l`).
### CI checks (push / PR to main)
| Workflow | Trigger | Gates what |
|---|---|---|
| `ci.yml` | push/PR, paths `packages/**, apps/**, scripts/**`, lockfile | Jobs: `typecheck-mcp` (MCP server; was the misnamed `lint-and-typecheck`, renamed under [#1097](https://github.com/FriendlyInternet/nuxt-crouton/issues/1097)), `typecheck-apps` (**the full `pnpm typecheck` app sweep — the CI backing for CLAUDE.md's mandatory-typecheck rule**, #1097), `build-fanfare` (matrix of 2 Nitro presets; "the build is the smoke test"), `docs-check`, `sync-validation`, `mcp-server-tests`, `test` (`pnpm test` after building core dists), `changeset-check` (warn-only), `package-check` (publint + attw), `fallow-audit` (diff-scoped `npx fallow audit` vs merge-base — fails only on NEW dead code/duplication/dep drift in the changed files, never the baseline, #1145) |
| `e2e.yml` | push/PR (paths `packages/**, fixtures/**, e2e/**`, lockfile) + nightly cron full matrix + dispatch | Playwright fixture smoke. Smart selection (#622): PRs run only fixtures affected per each fixture's `e2e.manifest.json`; push-to-main/nightly run all. Regenerates fixtures with the current CLI, so a generator regression = red PR (#197) |
| `guard-package-approval.yml` | PR to main | Fails the PR if `.claude/.package-edit-approved` is committed (would disable the packages/ HARD GATE, #350). Epic-scoped approval uses the `CROUTON_PACKAGE_EDIT_APPROVED` env var instead |
| `skills-doc.yml` | push/PR touching `.claude/skills/**` | `writeups/architecture/skills-and-triggers.html` must be regenerated (`node scripts/gen-skills-doc.mjs`) |
| `routing-registry.yml` | push/PR touching `.claude/routing.json`, `.claude/agents/**` | Model-routing registry drift check (#864) |
### LLM review gates (per-PR, agent-run)
All three share one pattern: a claude-code-action run writes a `<name>-verdict.json` at repo root + a sticky PR comment; a deterministic step then fails the check on the worst severities. Missing verdict file = **inconclusive pass** (fail-open by design; see §5 honesty hardening).
| Workflow | Fires on PRs touching | Fails check on |
|---|---|---|
| `red-team.yml` | `**/server/**`, `**/app/**`, `**/nuxt.config.ts`, plus all of `packages/**`, `apps/**`, `pocs/**` — i.e. effectively every code PR (skips docs/config-only PRs; drafts and bot-authored PRs are skipped by the job `if`) | high/critical security finding |
| `a11y.yml` | `**/*.vue` | axe critical/serious |
| `frontend-review.yml` | `**/*.vue` | 🔴 critical (e.g. Nuxt UI v3 component names) |
### Deploys
| Workflow | Trigger | Role |
|---|---|---|
| `deploy-app.yml` | `workflow_call` only | THE reusable pipeline (#114) — see §2 |
| `deploy-apps.yml` | push to main + PR (paths `apps/**, packages/**`, lockfile) + `workflow_dispatch(app, environment, review_pr)` | **The one generic caller for every `apps/*` app** (epic #481 WS2). An app opts in by committing `apps/<name>/deploy.config.json`. Replaced the per-app `deploy-<app>.yml` callers. Its `report-bumped-preview` job comments on a PR whose preview deploys were *cancelled* by queue-bumping (a sibling run took the shared per-app staging slot) — cancelled ≠ failed (#1150) |
| `deploy-pocs.yml` | PR touching `pocs/**` + dispatch(app, mode) | POC staging previews; `mode: version` = immutable Workers-version preview URL. Steps: `poc-deploy` skill |
| `deploy-docs.yml` | push to main (`docs/**`) / PR build-smoke / dispatch | Docs site — self-contained, NOT via deploy-app.yml |
| `deploy-sandbox.yml` | dispatch | Manual `sandboxes/*` deploy |
| `report-failed-deploy.yml` | `workflow_run[completed]` of the deploy workflows | On failure on `main`, opens exactly one issue per failing run (idempotency marker, #340). Note: its watch list still names "Deploy Blog (POC preview)", a workflow that no longer exists — harmless dangling reference |
### Agent pipeline (see §5 for the chain)
`claude.yml` (@claude mention) · `decompose-on-issue.yml` (`delegate` label) · `comment-dispatch.yml` (`/delegate` comment) · `resume-on-comment.yml` (reply on `status:blocked`) · `close-epic-on-comment.yml` (`/close-epic`) · `schedule-waves.yml` (baton pass) · `automerge-epic-subpr.yml` · `fix-ci-on-failure.yml` · `pipeline-pr-status.yml` · plus the pi.dev variants: `decompose-on-issue-pidev.yml` (**LIVE** — promoted past dispatch-only in #1017: fires when a human applies the `delegate-pi` label, plus manual dispatch; default model tuned in #1019; has run for real under `delegate-pi`) and `a11y-daily-pidev.yml` (still dispatch-only + `AGENT_HARNESS` gated); both need a funded `PI_PROVIDER_KEY` to complete. Also `mac-mini-smoke.yml` (self-hosted-runner proof, #610).
### Digests & scheduled reports (real cadence in §4)
`epic-digest.yml` · `housekeeping.yml` · `skills-digest.yml` · `eval-scoreboard.yml` · `label-ready-epics.yml` · `loop-station-advisor.yml` · `loop-station-inventory.yml` (push-triggered; **commits** to `writeups/loop-station/history.jsonl`) · `sync-changelogs.yml` · `unlighthouse.yml` (weekly staging crawl, never gates) · `red-team-daily.yml` / `a11y-daily.yml` (**dispatch-only**, see §4).
### Housekeeping / ops (event-driven or dispatch-only)
| Workflow | Trigger | Does |
|---|---|---|
| `labels.yml` | push touching `.github/labels.yml` | Syncs labels (`skip_delete: true` — never removes) |
| `strip-status-on-close.yml` | issue closed | Strips `status:*` labels (#641) |
| `cleanup-epic-branches.yml` | PR closed | Deletes `epic/*` head branch only when merged into main (#613) |
| `cleanup-merged-branches.yml` | weekly cron + dispatch | Dry-run unless `vars.CLEANUP_BRANCHES_APPLY=true` |
| `project-status.yml` / `backfill-project.yml` | PR/issue events / dispatch | Projects-v2 board automation |
| `db-clone.yml` | dispatch | D1 mirror between envs (`db-clone` skill) |
| `db-counts.yml` | dispatch | Read-only row counts of any app DB — the safe "what's in there" check (#384) |
| `teardown-app.yml` | dispatch | CF/branch/issue teardown (`remove-app` skill) — runs in CI because the interactive agent has no Cloudflare creds |
## 2. Deploy pipeline anatomy
### The caller pattern — deploy-apps.yml + deploy.config.json (NOT per-app callers)
**Root CLAUDE.md's deploy section and the `deploy` skill's Step 4 describe this pattern too (fixed under [#1093](https://github.com/FriendlyInternet/nuxt-crouton/issues/1093) — older copies described retired per-app `deploy-<app>.yml` callers).** Reality (epic #481): one generic `deploy-apps.yml` whose `detect` job matrixes over every `apps/<name>/deploy.config.json` whose `watchPaths` match the changed files. Rationale (from the workflow header): the agent pipeline's App token lacks the `workflows` scope, so bots cannot push `.github/workflows/` files — but they CAN push `apps/<name>/deploy.config.json`. Trust the workflow files.
`deploy.config.json` exists for the launched apps plus several pocs — list them fresh (`ls apps/*/deploy.config.json pocs/*/deploy.config.json`). Velo's fields (the shape): `stagingUrl`, `productionUrl`, `layerPackages` (space-separated `@fyit/*` list), `watchPaths` (app dir + each extended package + lockfile). Optional: `"smoke": { "required": true }` makes the post-deploy smoke gate the run (default is report-only; velo doesn't set it); `"secrets": { "required": true }` fails the deploy if `WORKER_SECRETS_JSON` resolves empty (§3 trap, #1094).
**Prod gating is structural, not conventional:** `deploy-apps.yml` hard-codes `environment=staging` for `push`/`pull_request` events; only `workflow_dispatch` with the explicit `production` choice produces a production matrix entry (#318/#347). Corrected (#2140 — the previous copy here predated #2020 and was wrong): **a PR deploy is ISOLATED, not the same staging Worker/DB as merge-to-main.** `deploy-app.yml` rewrites the built wrangler config for a `pull_request` event (`scripts/lib/pr-preview-wrangler.mjs`, #2020) into a per-PR Worker (`<app>-pr<n>`), strips every D1/KV/R2 id so wrangler auto-provisions fresh (migrated + seeded) resources, and drops the custom-domain route so it publishes to its own `*.workers.dev` URL — `<app>.pmcp.dev` is untouched by any PR. Two PRs on the same app never collide. This is not POC-only; it applies to any `apps/*` PR.
**`pull_request` deploys only the changed app's own paths — with one carve-out (#1297, #2140).** `scripts/deploy-detect.mjs` matches a PR's diff against `apps/<app>/**` only, never the app's shared-package `watchPaths` (a `packages/*` PR fanning out redeploys to every consuming app once saturated all 3 mac-mini runners). The exception: a PR carrying the literal HTML-comment marker `<!-- deploy-preview: full-watch -->` in its body (`UI_SIGNOFF_MARKER` in that file — `work-issue-pidev.yml`'s pi-lane UI-sign-off hold appends it, #1748) opts back into the full `watchPaths`, since that PR is exactly one known app and exists specifically for a human to look at it live. A `push` to `main` always uses the full `watchPaths` regardless.
### Inside deploy-app.yml (the reusable pipeline)
Inputs: `app`, `workspace` (apps|pocs), `environment`, `staging-url`/`production-url`, `layer-packages`, `review-pr`. Key steps, in order (all verified in the file):
1. `pnpm install --frozen-lockfile --ignore-scripts` → `pnpm rebuild better-sqlite3 || true`.
2. Layer-dist cache keyed on **build-set hash + `hashFiles('packages/**/*.ts')`** (the #745 cross-app cache-collision fix). `@fyit/crouton-devtools` is always appended to the layer set.
3. `nuxt prepare`, then **the one real step** — which script runs depends on the trigger: `pnpm run cf:deploy` (prod) or `cf:staging` (a `push`/`workflow_dispatch` staging deploy — the source of truth is the app's `package.json`, so CI and a dev's laptop run the identical script) — but a `pull_request` deploy runs neither: it builds directly, then rewrites the config into the isolated per-PR preview (`pr-preview-wrangler.mjs`, #2020) before its own inline `wrangler deploy`, so the shared staging Worker is never touched by a PR.
4. Optional `wrangler secret bulk` from `WORKER_SECRETS_JSON` (see §3 trap).
5. Preview-review bridge (#607): staging + PR-tied only — pushes `NUXT_CROUTON_REVIEW_*` Worker secrets so in-page overlay comments post to the PR as `nuxt-harness[bot]`.
6. `BETTER_AUTH_SECRET` auto-generated (`openssl rand -hex 32`) if the Worker has none; never rotates an existing one; skips (with `::warning::`) if `wrangler secret list` fails.
7. Review login seeded (staging only, #608): deterministic `review+<app>-pr<N>@example.com` via `scripts/seed-review-login.mjs`.
8. Deployed smoke: `scripts/smoke-deployed.mjs` (login + CRUD + screenshot, artifact `smoke-<app>`). Report-only unless `smoke.required` in `deploy.config.json`.
9. Sticky PR comment (marker `<!-- deploy-staging:<app> -->`) with staging URL + test login.
Concurrency: `deploy-<app>-<env>`, `cancel-in-progress: false` (a cancel mid-migration is unsafe).
### wrangler.jsonc anatomy (verified against `apps/velo/wrangler.jsonc`)
- **Workers, not Pages**: no `pages_build_output_dir`; `compatibility_flags: ["nodejs_compat"]`. `name`/`main`/`assets` injected at build by Nitro's `cloudflare_module` preset into `.output/server/wrangler.json`.
- **Two domains (#133)**: top-level `routes` → `<app>.friendlyinter.net` (prod, `custom_domain: true`); `env.staging.routes` → `<app>.pmcp.dev`.
- **Bindings do not inherit across envs**: `env.staging` redeclares D1 (`<app>-staging-db`) and blob/KV with separate ids. Velo binds R2 as `BLOB` (no KV); other apps may bind `KV`.
- **Id-less auto-provisioning**: wrangler auto-creates missing D1/KV on first deploy; each app's `scripts/sync-wrangler-ids.mjs` then writes the provisioned ids back into the source `wrangler.jsonc` (remote `d1 migrations apply` reads the SOURCE config and needs real ids). Rule: commit the synced ids.
- **The nitro#3429 env-stripping workaround**: the build drops named environments from the generated config, so each app's `scripts/inject-wrangler-env.mjs` re-merges the `env` block. Velo's `cf:staging` runs inject **twice** — before deploy and again before migrate (sync:ids rewrites files in between).
Velo's exact scripts (verified in `apps/velo/package.json`):
```bash
# cf:deploy (prod)
NITRO_PRESET=cloudflare_module nuxt build && npx wrangler --cwd .output deploy \
&& node scripts/sync-wrangler-ids.mjs && npx wrangler d1 migrations apply velo-db --remote
# cf:staging
NITRO_PRESET=cloudflare_module nuxt build && node scripts/inject-wrangler-env.mjs \
&& npx wrangler deploy --config .output/server/wrangler.json --env staging \
&& node scripts/sync-wrangler-ids.mjs && node scripts/inject-wrangler-env.mjs \
&& npx wrangler d1 migrations apply velo-staging-db --env staging --remote \
&& (pnpm db:seed:staging || true)
```
### Migrations in CI + the #138 rule
Flow: build → deploy → sync ids → `wrangler d1 migrations apply <db-name> [--env staging] --remote`. `migrations_dir: server/db/migrations/sqlite` is declared per-binding in `wrangler.jsonc`. **The #138 rule**: the `wrangler deploy` step may use `--config .output/server/wrangler.json`, but the `d1 migrations apply` step must NOT pass `--config .output/...` — it doubles the migrations path ("no migrations" symptom) and must read the source `wrangler.jsonc` (which is why sync:ids has to run first). Canonical wording lives only in root CLAUDE.md ("The pattern, end to end"); the `deploy` skill doesn't restate it. Local/"No schema files found" migration gotchas belong to the `db-migrations` skill.
## 3. Secrets / tokens / vars
### Actions secrets (repo level unless noted)
| Secret | Used by | Notes |
|---|---|---|
| `ANTHROPIC_API_KEY` | all agent workflows | Headless CI MUST use an API key, never a subscription `CLAUDE_CODE_OAUTH_TOKEN` (workflow comments cite Anthropic terms) |
| `HARNESS_APP_ID` + `HARNESS_APP_PRIVATE_KEY` | many workflows (grep for it) | The "Nuxt Harness" GitHub App — see the cascade rule below |
| `CLOUDFLARE_ACCOUNT_ID` + `CLOUDFLARE_API_TOKEN` | deploy/db workflows | Token scopes: account Workers Scripts/D1/KV/R2 Edit + zone Workers Routes + DNS Edit (details: `deploy` skill, Credentials) |
| `WORKER_SECRETS_JSON` | `deploy-app.yml` | Optional bundle of Worker secrets — **see the trap below** |
| `RESEND_API_KEY` | digests, red-team email | Email rail; steps skip green when unset |
| `REVIEW_SEED_SECRET` | deploy-app.yml | Optional salt for review-login passwords |
| `PI_PROVIDER_KEY` | pi.dev variants | pi's model-provider key; required by `decompose-on-issue-pidev.yml` (live on the `delegate-pi` label, #1017) and `a11y-daily-pidev.yml` (dispatch-only). Funding state not verifiable from the repo — if a pi run errors at the agent step, check this key's credit first |
### The WORKER_SECRETS_JSON trap (fixed under [#1094](https://github.com/FriendlyInternet/nuxt-crouton/issues/1094))
`WORKER_SECRETS_JSON` **MUST be a *repository-level* Actions secret, NOT an *Environment* secret** — the deploy job is reached via `secrets: inherit` from callers whose jobs declare no `environment:`, so Environment-scoped secrets resolve to EMPTY with no error. Symptom of getting this wrong: deploys go green but Worker secrets never update, silently — unless the app sets `"secrets": { "required": true }` in `deploy.config.json`, which makes an empty resolution fail the run (guard added in #1094; the workflow also rejects a set-but-invalid/empty-object bundle).
### Actions vars
`AGENT_RUNNER` (swap agent jobs to the self-hosted mac-mini; default ubuntu-latest) · `AGENT_HARNESS` (`pi` opt-in) · `CLEANUP_BRANCHES_APPLY` · `LABEL_READY_EPICS_APPLY` (write-gates for otherwise dry-run jobs) · `DIGEST_REPORT_EMAIL` / `DIGEST_EMAIL_FROM` / `RESEND_FROM` / `RED_TEAM_REPORT_EMAIL` (email rails) · `PROJECT_NAME` · `UNLIGHTHOUSE_SITE`.
### Three auth rules every agent workflow obeys
1. **The token-cascade rule** (the single most repeated fact across these files): GitHub suppresses workflow triggers for events initiated by the built-in `GITHUB_TOKEN`. Any mutation that must trigger a downstream workflow (apply `delegate` → decompose fires; merge → schedule-waves; close → strip-status; push → CI re-runs) is done with a **Harness App installation token**. Read-only or chain-terminal steps deliberately use `GITHUB_TOKEN`.
2. **The ruleset-bypass rule** (#1133): the `main` ruleset requires a PR, so **any workflow that pushes directly to `main` must push with the Harness App token** — the App is on the ruleset's bypass list, and `GITHUB_TOKEN` cannot be. GitHub Actions is not an installable App in the org, so adding it as a bypass actor is rejected outright (`422 — Actor GitHub Actions integration must be part of the ruleset source or owner organization`). The direct-to-`main` writers are `loop-station-inventory` · `loop-station-usage` · `loop-station-findings` · `bundle-budget`; all four commit with `[skip ci]`, which is what stops the App-token push (unlike a `GITHUB_TOKEN` push) from re-triggering the very workflow that made it.
3. **Job-level permissions only**: job-level `permissions` fully OVERRIDES (does not merge with) a workflow-level block, so `id-token: write` must sit on the job or the claude-code-action OIDC request fails. Stated verbatim in several workflows (e.g. `resume-on-comment.yml`).
Also: `anthropics/claude-code-action` is pinned to one SHA across several workflows with "keep in sync when bumping" — a manual multi-file sync liability; grep for the pin and check all of them when bumping (re-verify block).
## 4. Scheduled things — the REAL cadence
Do not trust "daily" labels; the truth is `on.schedule` + `.github/digests.yml`. **Snapshot 2026-07-02 — regenerate with the cron grep in the re-verify block before relying on it.**
| Workflow | Cron in file | Real cadence |
|---|---|---|
| `e2e.yml` | `0 4 * * *` | Nightly full-matrix backstop — genuinely daily |
| `epic-digest.yml` | `0 5 * * *` | Daily wake-up; real send day from `digests.yml` via `schedule.mjs` (retrofit #1139) |
| `label-ready-epics.yml` | `0 5 * * *` | Daily (labels only when `vars.LABEL_READY_EPICS_APPLY` allows) |
| `skills-digest.yml` | `0 5 * * *` | **Monthly (1st)** — cron is only a wake-up; `.github/digests.yml` `monthly:1` gates the send |
| `housekeeping.yml` | `0 6 * * *` | Daily per digests.yml (`schedule: daily`) |
| `sync-changelogs.yml` | `0 6 * * *` | Daily |
| `cleanup-merged-branches.yml` | `30 6 * * 1` | Weekly Mon (dry-run by default) |
| `eval-scoreboard.yml` | `0 7 * * *` | **Weekly Mon** per digests.yml (`weekly:mon`) |
| `loop-station-advisor.yml` | `23 7 * * 1` | Weekly Mon |
| `unlighthouse.yml` | `23 7 * * 1` | Weekly Mon |
| `red-team-daily.yml`, `a11y-daily.yml` | **none — `workflow_dispatch` only** | Daily schedules dropped to cut cost (#823). Root CLAUDE.md still calls the red-team sweep a live "daily deep sweep" — stale, tracked [#1097](https://github.com/FriendlyInternet/nuxt-crouton/issues/1097). Coverage now relies on the per-PR gates |
**Cadence-as-data rule**: for the digests, never edit a workflow's `cron` to change cadence — edit `.github/digests.yml` (the daily cron is a cheap wake-up; `.claude/skills/housekeeping/schedule.mjs` exits early on non-send days).
## 5. The agent-pipeline chain
How one `delegate` label becomes merged code (entry/steps verified in the named workflows; full theory in `.claude/agents/CLAUDE.md`):
1. **Entry** — `decompose-on-issue.yml` fires only on `issues: [opened, labeled]` with `label.name == 'delegate'` (gating on the specific label avoids re-fires when workers add other labels, #535). Runs claude-code-action with `/task-decompose #NN`. The pi.dev twin `decompose-on-issue-pidev.yml` fires the same way on the `delegate-pi` label (#1017). Mobile entry: comment `/delegate` → `comment-dispatch.yml` (OWNER/MEMBER/COLLABORATOR only) removes+re-adds the label **with the App token** so the labeled event actually triggers downstream. `claude.yml` separately answers `@claude` mentions on issues/PRs.
2. **Artifact gates** (#461/#603) — deterministic post-steps fail the run if the agent produced nothing observable (comment / PR / sub-issues / `status:blocked` hold); #661 classifies WHY from the execution log.
3. **Sign-off resume** — `resume-on-comment.yml`: any human (non-Bot) comment on an issue labelled `status:blocked` resumes the pipeline; if the comment matches `\b(approve|approved|lgtm)\b` a deterministic step finds the linked draft gate-PR, marks it ready (GraphQL), and merges it — merge commit, never squash (#572). This is why reactions and labels resume nothing.
4. **Auto-merge** — `automerge-epic-subpr.yml`: non-draft bot PRs targeting `epic/*` merge when green; any sub-PR touching `packages/` is held with `status:blocked` — shared code never lands unreviewed even onto an epic branch (#586/#339). Two distinct gates, and the difference matters: the **green gate** blocks a RED check, while the **presence gate** (#1698) additionally requires that `CI Gate` has *reported* `success` for the head SHA. Absence of failure is not evidence of success — on #1668 `CI` never started, so nothing was red and an unchecked build break merged. A **missing** `CI Gate` now leaves the PR open with a one-time explanatory comment (push an empty commit to fire `synchronize`); a **pending** one waits for the `check_suite` re-fire. The `main` ruleset can't cover this path — it only targets the default branch.
5. **Baton pass** — `schedule-waves.yml` (#283): on PR merge, closes its `Closes #NN` issues (auto-close only works on the default branch); on issue close, releases dependents whose `Blocked-by: #NN` body lines are ALL closed by applying `delegate` (fan-in aware).
6. **Fix-bot** — `fix-ci-on-failure.yml` (#338): `workflow_run` failure of CI/E2E on `claude/issue-*` branches only; attempt cap via `ci-fix-attempts:N` label (max 3); forbidden from `packages/`; pushes with the App token so CI re-runs; exhaustion → `status:blocked` + owner mention.
7. **Status & close** — `pipeline-pr-status.yml` (no-LLM sticky comments per PR + epic rollup); `label-ready-epics.yml` marks finished epics; `close-epic-on-comment.yml` (#856) closes on a human `/close-epic` comment only when the epic already carries `status:ready-to-close` (postmortem-before-close holds structurally; server-side re-checks + maintainer permission check).
### Gate-honesty hardening state
The three per-PR review gates shipped **fail-open** (missing tool grant meant the agent could never write its verdict file; outage was indistinguishable from a clean scan — #1034). Grants fixed in #1031/#1033 (per the workflow comments the fail-open comment lines remain as warnings); #1035 added a known-bad fixture smoke per gate. Follow-ups: [#1036](https://github.com/FriendlyInternet/nuxt-crouton/issues/1036) (bake the tool grant into the workflow standard) and [#1037](https://github.com/FriendlyInternet/nuxt-crouton/issues/1037) (distinguish agent-outage from clean scan) — issue state is live data, check GitHub before relying on it. Practical consequence until #1037 lands: **a green review-gate check can still mean "the agent never ran"** — on a security-sensitive PR, open the check and confirm the verdict comment exists. (This paragraph is the owner of the #1034-hardening status; siblings citing it should defer here.)
Also volatile: `.claude/routing.json` `_known_gaps` states the top-level workflow loop (claude.yml, decompose, fix-ci, the sweeps) runs on the claude-code-action **default model, unpinned** — pinning is an acknowledged follow-up.
## 6. Known doc drift in this area
Workflow files beat all prose for CI/deploy mechanics (the doc-vs-doc trust order is owned by `crouton-docs-trust-map` §1). The known-stale claims are tracked — read the issue for current fix state:
- ~~Per-app `deploy-<app>.yml` callers~~ / ~~`WORKER_SECRETS_JSON` as an Environment secret~~ — both fixed ([#1093](https://github.com/FriendlyInternet/nuxt-crouton/issues/1093), [#1094](https://github.com/FriendlyInternet/nuxt-crouton/issues/1094)): CLAUDE.md + the `deploy` skill now describe `deploy-apps.yml` + `deploy.config.json` and the repo-level secret (§2, §3)
- **"Daily deep sweep" red-team/a11y still live** (root CLAUDE.md) — dispatch-only since #823 (§4). (The other half of this row — CI not backing the mandatory-typecheck rule — was fixed under [#1097](https://github.com/FriendlyInternet/nuxt-crouton/issues/1097): `ci.yml` now runs the full app sweep as `typecheck-apps`.)
- `report-failed-deploy.yml` watches "Deploy Blog (POC preview)", a workflow that no longer exists — harmless dangling reference (untracked)
- Folk knowledge: the `ui-approved` label resumes nothing — inert by design; only a reply comment resumes (#572)
When this skill and a workflow file disagree, the workflow file wins — then fix this skill.
## Provenance and maintenance
verified: 2026-07-03
```bash
ls .github/workflows | wc -l # inventory size
grep -rn "cron:" .github/workflows/*.yml # real schedules (cross-check .github/digests.yml gating)
ls apps/*/deploy.config.json pocs/*/deploy.config.json # opted-in apps
grep -n "repository-level" .github/workflows/deploy-app.yml # the WORKER_SECRETS_JSON rule (#1094)
grep -rln "HARNESS_APP_ID" .github/workflows # App-token workflow set
grep -rn "claude-code-action@" .github/workflows/*.yml # the SHA-pin sync set
grep -n "_known_gaps" .claude/routing.json # unpinned-model note
for n in 1093 1094 1097 1036 1037; do gh issue view $n --json number,state -q '"\(.number) \(.state)"'; done # tracked-drift + gate-honesty states
```
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!