Skip to content

Gate production promotion and rollback behind a GitHub-identity allowlist - #5

Merged
gpmayorga merged 6 commits into
mainfrom
claude/gate-prod-promotion
Aug 18, 2026
Merged

Gate production promotion and rollback behind a GitHub-identity allowlist#5
gpmayorga merged 6 commits into
mainfrom
claude/gate-prod-promotion

Conversation

@gpmayorga

@gpmayorga gpmayorga commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

What

Production promotion moves from "a Cloudflare admin runs wrangler by hand" to a GitHub-gated dispatch flow mirroring centrifuge/backend's activate-production model:

  1. released eventapp-build-deploy-release.yml still does not deploy. It posts a Slack notification carrying the promotion command (gh workflow run promote-production.yml --ref main -f tag=<TAG>) and a link to the dispatch page.
  2. New app-promote-production.yml (workflow_call, dispatched via each app's thin promote-production.yml caller):
    • authorize job: must be dispatched from main (the workflow YAML on main is the trust root), and the actor must appear in the caller's AUTHORIZED_DEPLOYERS repository variable, passed as the authorized-deployers input — empty fails closed. Failed attempts are announced to Slack.
    • promote job (environment: production): verifies the tag's commit is on main, resolves the Worker version staged for the tag (versions list by workers/tag annotation), shifts traffic (versions deploy <id>@100%), announces to Slack, and notifies on failure with a reason.
  3. app-rollback.yml gains the same allowlist gate — previously any repo write user could shift production traffic by dispatching a rollback. authorized-deployers is declared optional purely for parse-compat with callers already on main; an empty value refuses to roll back at runtime. Optional slack-webhook-url secret for failed-attempt alerts.

Also adds a CODEOWNERS (deployment/secret-sensitive paths → @gpmayorga / @hieronx / @gregory-kf) and updates the README for the new model. Security-focused CLAUDE.md guidance lives in #11.

Why

GitHub environment protection (required reviewers) is unavailable on private non-Enterprise repos, and the Cloudflare token cannot be scoped to forbid version deploys. The previous notify-only approach removed the unattended path but left rollback ungated and promotion un-audited. This model:

  • ties promotion to a GitHub identity with a native audit trail (workflow runs + Slack alerts on failed attempts)
  • tightens the rollback path (was: any write user; now: allowlist, fail-closed)
  • removes the need for humans to hold Cloudflare prod tokens for routine releases

It remains a process control, not a hard control: anyone holding a Worker-deploy-capable token can promote out-of-band, and repo admins can edit the allowlist variable. The hard boundary — repo access + branch protection + CODEOWNERS on .github/workflows/** — is handled repo-settings-side.

Contract changes (callers must update)

  • app-build-deploy-release.yml: new required secret slack-webhook-url; new optional inputs production-worker-name, production-url; production-url output removed; the notification links to the caller's promote-production.yml (name the caller exactly that).
  • app-promote-production.yml (new): inputs tag, app-name, production-worker-name, cloudflare-account-id, authorized-deployers, optional production-url; secrets cloudflare-api-token, slack-webhook-url.
  • app-rollback.yml: new input authorized-deployers (parse-optional, runtime fail-closed), new optional secret slack-webhook-url. ⚠️ After this merges, rollback dispatches from callers still on the old contract fail closed until the consumer PRs land and AUTHORIZED_DEPLOYERS is created.

Rollout / merge order

  1. Merge this first. Consumer PRs (centrifuge/apps-invest#268, centrifuge/apps-management#1151) reference this branch while under review; they flip to @main before merging.
  2. Each app repo needs the AUTHORIZED_DEPLOYERS repository variable (comma-separated GitHub usernames) before the first promotion or rollback.
  3. Docs PR Add CLAUDE.md with security and deployment guidance #11 can merge after (or in parallel); it documents the model this PR ships.

Verification

  • yamllint (relaxed, line-length off — matches lib-ci) clean; caller with:/secrets: keys cross-checked against the workflow_call contracts of both consumer PRs.
  • Live validation of the promote flow needs a throwaway prerelease→release after merge (dispatch touches real Cloudflare); the notify path posts to Slack only.

On a `released` event the release pipeline no longer runs
`wrangler versions deploy`; it posts a Slack message asking a Cloudflare
admin to promote the already-staged version by hand, with the release link,
dashboard link, and exact CLI commands.

Production promotion cannot be gated by GitHub environment protection
(unavailable on private non-Enterprise repos) and the Cloudflare token
cannot be scoped to forbid version deploys, so removing the automated path
is the available control. Documented as a process control, not a hard one:
app-rollback.yml's prod path still carries the same deploy capability by
design (emergency traffic shift).

Also adds a security-focused CLAUDE.md and a CODEOWNERS assigning the
deployment/secret-sensitive paths to a designated reviewer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017jj24BNHVr8gQX2mhgHWLT
…otion

# Conflicts:
#	.github/workflows/app-build-deploy-release.yml
Adopts centrifuge/backend's activate-production model. On `released` the
pipeline still only notifies, but the Slack message now carries a
`gh workflow run promote-production.yml -f tag=...` command instead of
raw wrangler instructions: promotion happens through the new
app-promote-production.yml reusable workflow, dispatched by an actor in
the caller repo's AUTHORIZED_DEPLOYERS variable (empty fails closed),
from main only, with the tag verified to be on main, and both failed
attempts and successful promotions announced to Slack.

app-rollback.yml gains the same allowlist gate — previously any repo
write user could shift production traffic by dispatching a rollback.
Its authorized-deployers input is optional only for parse-compat with
callers already on main; empty refuses to roll back at runtime, so
rollback fails closed until the app callers pass the variable.

This ties promotion to a GitHub identity with a native audit trail and
removes the need for humans to hold Cloudflare prod tokens for routine
releases. Still a process control: token holders can promote out-of-band
and repo admins can edit the allowlist — documented in the workflow
headers, README, and CLAUDE.md.
@gpmayorga gpmayorga changed the title Gate production promotion behind a manual Slack notification Gate production promotion and rollback behind a GitHub-identity allowlist Aug 17, 2026
@gpmayorga
gpmayorga marked this pull request as ready for review August 18, 2026 11:20
Keep the prod-promotion gate focused on workflows, CODEOWNERS, and
README; docs land separately in #11.
@gpmayorga

Copy link
Copy Markdown
Collaborator Author

Split CLAUDE.md out to #11 so docs can review separately from the gate/workflows/CODEOWNERS changes.

@gpmayorga
gpmayorga merged commit 8985095 into main Aug 18, 2026
3 checks passed
@gpmayorga
gpmayorga deleted the claude/gate-prod-promotion branch August 18, 2026 13:25
@gpmayorga
gpmayorga restored the claude/gate-prod-promotion branch August 18, 2026 13:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants