Skip to content

ci(release): run on push to main and deploy the released head - #153

Merged
musingfox merged 4 commits into
mainfrom
deploy-after-release-in-ci
Nov 3, 2026
Merged

musingfox merged 4 commits into
mainfrom
deploy-after-release-in-ci

Conversation

@musingfox

Copy link
Copy Markdown
Owner

Why

Every production deploy needed two dispatches, and the second one (deploy.yml) had to be run by the owner, because Claude Code's auto mode refuses an agent's production deploy. The owner decided on 2026-11-03 that a release should deploy on its own (ADR 0024).

What

  • release-image.yml runs on every push to main and on dispatch. A dispatch has a boolean deploy input, default true.
  • After release succeeds, a current job checks that the released commit is still the head of the ref the run was for (git ls-remote). Then a deploy job calls deploy.yml through workflow_call on the same commit, so it deploys the digest recorded in that commit's image/<sha7> tag.
  • The deploy condition is (push || inputs.deploy) && current == 'true' && attempt == run_attempt.
    • On a push, inputs.deploy evaluates to an empty string, which compares equal to false. So inputs.deploy != false would have skipped every push; the tests pin this.
    • A moved ref skips the deploy with a notice naming both commits, and the run stays green. A failed read of the ref fails the job.
  • deploy.yml is still dispatchable with image_tag for rollback by digest, and that path does not go through the guard. Its concurrency moved from workflow level to its job, with the same group and no cancelling.
  • scripts/trial-wizard.sh reports production's last deploy from either path. Before, it read only deploy.yml runs, and a called workflow has no run of its own.
  • Docs: ADR 0024, architecture.md §6, operations.md, trial-deployment.md, the release-image-built-in-ci spec, CLAUDE.md (the owner approved the line) and the CHANGELOG. §7 follows on main after the merge.

Owner decisions (2026-11-03)

  1. A release deploys by default; a dispatch can untick deploy.
  2. The release runs on push to main.
  3. Deploys do not avoid the hourly cron.
  4. Only the head of its ref deploys automatically. This was added after review found that re-running an old run, or the concurrency queue running out of order, could put an older commit over a newer one.

Consequences to know

  • Any push to main deploys production, and main has no branch protection; scripts/check.sh in the release job is the only gate.
  • The trial is now checked after production, not before.
  • A digest pin lasts until the next push.
  • A queued dispatch of the release replaces a waiting push run. To recover, dispatch the release on main.
  • To retry a failed deploy, re-run all jobs or dispatch deploy.yml. Re-running only the failed deploy job skips it.

Verification

  • scripts/check.sh: 4851 passed on the branch tip. Each intermediate commit passes the workflow, trial and doc tests.
  • The trigger and guard tests were red against the old workflows (7, then 13 failing) and are green now.
  • Two independent review rounds passed every clause. The second checked the guard against a real git remote: a branch head, annotated and lightweight tags, a moved or missing ref, and an unreadable remote.
  • Not yet verified until a real run: the guard on GitHub, whether a called job's deploy-app group serialises with a manual deploy, and the called job's name (deploy / deploy, seen on another project's run) that the wizard matches.

🤖 Generated with Claude Code

musingfox and others added 4 commits November 3, 2026 19:09
The release now runs on every push to main as well as on dispatch, and a
release that succeeds deploys the commit it just tagged. A dispatch gets
a boolean `deploy` input, default true; unticking it publishes the image
without deploying it. Owner decision 2026-11-03 (ADR 0024): every deploy
needed the user, because Claude Code's auto mode refuses an agent's
deploy dispatch.

Shape: deploy.yml gains `on: workflow_call` and release-image.yml calls
it as a job after `release`. A local reusable workflow runs "from the
same commit as the caller workflow", and under workflow_call GITHUB_SHA
is "Same as the caller workflow", so the unchanged resolve step reads
image/<sha7> of the released commit, never :release. Build and deploy
stay separate jobs, so "cannot be made" and "cannot go live" are still
two red marks. Secrets are passed one by one, not `secrets: inherit`.

The deploy condition is
(push || inputs.deploy) && current == 'true' && attempt == run_attempt.
On a push the `deploy` input does not exist and evaluates to an empty
string, which GitHub's loose comparison equates with false, so
`inputs.deploy != false` would skip every push; the tests model the
documented comparison rules and pin that pitfall.

A `current` job between the two reads `git ls-remote origin $GITHUB_REF`
and deploys only while the released commit is still the head of its ref
(owner decision 2026-11-03). A re-run keeps its original GITHUB_SHA and
GitHub's concurrency queue says ordering is not guaranteed, so either
could otherwise put an older commit over a newer one. A moved ref skips
the deploy with a notice naming both shas and stays green; a failed read
fails red. The attempt term stops "re-run failed jobs" from reusing an
answer given while the old commit was still the head.

deploy.yml's concurrency moves to its job, same group, not cancelled.
That it holds for the call is unverified until a real run. The "timing
is chosen by a person" reason is gone: the hourly cron is not avoided.
No push filter: every push to main runs.

Rejected: workflow_run (GITHUB_SHA is the default branch's head, not the
released commit); an ancestry check (an older commit is always an
ancestor, so it stops nothing); the guard inside deploy.yml (it would
also gate a manual pin by digest).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stage 2's note read `gh run list --workflow deploy.yml`. A release now
deploys by calling deploy.yml as a job of its own run, and a called
workflow has no run of its own, so the note would show only the last
deploy started by hand. production_last_deploy reads the last 10 runs
of both workflows and keeps a run only if a job named `deploy` or ending
in `/ deploy` succeeded, so a release whose deploy was skipped does not
count.

seed_trial.py, reset_trial.py and deploy_trial.py said a trial is
checked before production; a push now deploys production first, and
their help text says the trial comes after, as operations.md does.

Rejected: asking production's /api/build, the better receipt. The
wizard holds only the trial's Access token at that stage and has no
production URL.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Records the owner's 2026-11-03 decisions: a push to main releases and
deploys, a dispatch may opt out of the deploy, the hourly cron is not
avoided, and only the head of its ref deploys automatically. It keeps
the surviving half of the old separation (build and deploy fail as
separate jobs) and retires the other (a person picks the hour).

Rejected: keeping both workflows dispatch-only, holding deploys away
from the cron hour, workflow_run, and an ancestry guard. Consequences
stated plainly: any push to main, which has no branch protection,
deploys production; a digest pin lasts until the next push; a queued
dispatch replaces a waiting push run; re-runs of old runs skip their
deploy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
architecture.md §6, operations.md, trial-deployment.md, the accepted
release-image-built-in-ci spec, CLAUDE.md and the CHANGELOG now say the
release runs on push and dispatch and deploys the released head unless a
dispatch opts out, that the cron is not avoided, and that a digest pin
lasts until the next push. operations.md also states what a queued run
replaces and how to recover, and that a failed deploy is retried by
re-running all jobs. "Staging first, production after" became "The trial
deployment, after production". §7 waits for the landed hash.

Rejected: rewriting the 2026 CHANGELOG entry that calls both workflows
dispatch-only; it records what shipped then.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@musingfox

Copy link
Copy Markdown
Owner Author

@codex review

@coderabbitai

coderabbitai Bot commented Nov 3, 2026

Copy link
Copy Markdown

Important

  • Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

Run configuration
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

CodeRabbit Verify

  • Run CodeRabbit Verify to test the changes in this pull request.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-03T11:15:31.975573Z 996037b Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@claude

claude Bot commented Nov 3, 2026

Copy link
Copy Markdown

Review of #153: I found no correctness bugs, data loss or security problems in the workflow changes. I only read the diff and deploy.yml's secret, var and input references. I did not run the workflows.

What I checked:

  • github.event_name == 'push' || inputs.deploy handles the empty inputs.deploy on a push correctly.
  • The current job's head check compares the peeled ref exactly, and a failed ls-remote fails the job rather than skipping quietly.
  • The attempt check stops a re-run of only the deploy job from reusing an older current answer.
  • deploy.yml reads inputs.image_tag, which is simply empty when the release calls it. Its CLOUDFLARE_* and CYRIS_ACCESS_* secrets are all passed through. The other values it reads are vars.*, which a called workflow still sees.
  • Pushing the image/<sha7> tag with the job token cannot re-trigger the push workflow, because the trigger is on branches only.

Risks the ADR already admits, which you may want to weigh before merging:

  • Unverified concurrency: the job-level deploy-app group may not hold a release's called deploy against a hand-dispatched deploy.yml. Until a real run shows it, two wrangler deploy runs could overlap.
  • Silent skips: the release-image queue keeps only one waiting run. A middle push can silently never release or deploy. A stale-head skip is also green, with only a notice. Nothing alerts on either.
  • No branch protection: main has none, so scripts/check.sh inside the release is the only gate before production.

Nothing here blocks the merge.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Another round soon, please!

Reviewed commit: 996037bbfe

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@musingfox
musingfox merged commit b71ef75 into main Nov 3, 2026
3 checks passed
@musingfox
musingfox deleted the deploy-after-release-in-ci branch November 3, 2026 11:52
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.

1 participant