ci(release): make the registry promotion a re-runnable job - #3456
ci(release): make the registry promotion a re-runnable job#3456myasnikovdaniil wants to merge 3 commits into
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughThe release workflow separates Git finalization from stable registry promotion. A chart promotion script validates, packages, compares, and publishes charts with rerun handling, supported by Bats coverage and updated recovery documentation. ChangesRelease promotion
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Finalize
participant RegistryJob
participant PromotionScripts
participant OCIRegistry
Finalize->>RegistryJob: provide tag and make_latest
RegistryJob->>PromotionScripts: run retag and chart promotion
PromotionScripts->>OCIRegistry: inspect, publish, and copy artifacts
Possibly related issues
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@hack/promote-publish-chart.sh`:
- Around line 93-126: Update the skopeo inspect probe before the existing
published-chart comparison so its output and exit status are captured. Proceed
to the “not published; pushing” branch only when the registry response clearly
indicates the stable chart manifest is missing; for authentication, network,
timeout, or other probe failures, print the captured diagnostic and abort
instead of running helm push. Preserve the current diff and FORCE_CHART behavior
for charts confirmed to exist.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 6a501880-d84a-4719-ba85-1ff6acc3b298
📒 Files selected for processing (4)
.github/workflows/pull-requests-release.yamldocs/release.mdhack/promote-publish-chart.shhack/promote-publish-chart_test.bats
The probe that decides whether cozy-installer:X.Y.Z is already published treated every non-zero skopeo exit as "not published yet". skopeo exits 1 for everything — an absent manifest, a 403, an expired token, a DNS failure, a registry 5xx — so a transient registry error fell straight through to an unconditional helm push, bypassing the content comparison and moving the stable chart tag onto freshly packaged bytes. That is the opposite of what this script exists to guarantee, and it would fire under exactly the flaky conditions that make a promotion need re-running. Classify the failure instead: an explicitly missing manifest, or a repository that does not exist yet (the first-ever push), counts as absent; anything else aborts with the registry's own diagnostic and pushes nothing. The patterns match the wording skopeo actually emits, checked against GHCR for a missing tag, a forbidden repository and an unreachable registry. Reported by CodeRabbit on #3456. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
Finalize's registry side effects run after the stable tag and the GitHub release are already irreversible, and v1.6.0 — the first release to execute them — died partway through, leaving 32 of 43 repositories untagged and the chart unpublished. Recovery took 64 hand-run skopeo copies plus a manual helm package/push, because re-running the job checks out the same merge commit and therefore the same broken script: a fix landed on main could never reach it. Split the registry side effects out of the finalize job so that GitHub's own "Re-run failed jobs" is the recovery path. As its own job it replays without re-entering tag creation or release publication, and the make_latest decision is passed in as a job output rather than recomputed, so the release's `latest` flag and the images' :latest tag cannot drift apart. Two changes make that re-run able to carry a fix rather than only a retry. The chart publish moves out of the workflow into hack/promote-publish-chart.sh, so both registry operations are now scripts; and the job checks the scripts out from main at run time while the release tree stays pinned to the promoted merge commit. Landing a correction on main and pressing re-run then applies it to the unchanged release tree. The new script also makes the chart step idempotent, which the inline version was not: helm package is not byte-reproducible, so re-pushing an already-published version would move the stable chart tag onto new bytes. An existing chart is pulled and compared by extracted content — identical is a no-op, different is a hard failure unless FORCE_CHART says otherwise. Its existence probe uses `skopeo inspect --raw`, because the image-shaped inspect rejects a Helm OCI artifact on media type. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
The probe that decides whether cozy-installer:X.Y.Z is already published treated every non-zero skopeo exit as "not published yet". skopeo exits 1 for everything — an absent manifest, a 403, an expired token, a DNS failure, a registry 5xx — so a transient registry error fell straight through to an unconditional helm push, bypassing the content comparison and moving the stable chart tag onto freshly packaged bytes. That is the opposite of what this script exists to guarantee, and it would fire under exactly the flaky conditions that make a promotion need re-running. Classify the failure instead: an explicitly missing manifest, or a repository that does not exist yet (the first-ever push), counts as absent; anything else aborts with the registry's own diagnostic and pushes nothing. The patterns match the wording skopeo actually emits, checked against GHCR for a missing tag, a forbidden repository and an unreachable registry. Reported by CodeRabbit on #3456. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
Aleksei Sviridkin (lexfrei)
left a comment
There was a problem hiding this comment.
NOT LGTM. The branch conflicts with main in both files it shares with it, so the diff that would actually merge does not exist yet. Two smaller things below are worth folding into the same rebase pass.
Business context: the registry side effects of a promotion run after the stable tag and the GitHub release are already irreversible. v1.6.0 died partway through them, and re-running was no help, because the job re-entered tag creation and re-executed the same broken script.
The design itself is right, and I want to say that before the findings. Splitting the registry work into its own job, moving the logic into scripts, and taking those scripts from main at run time is the combination that makes a failed promotion both replayable and fixable. I checked the recovery path rather than assuming it: GitHub reuses the outputs of jobs that already succeeded when you re-run only the failed ones, so a replayed registry job still reads the tag and the latest decision finalize computed on the first attempt instead of falling through to an empty tag.
Blockers
B1: conflicts with main in .github/workflows/pull-requests-release.yaml and docs/release.md
The structural change survives, so this is rebase work. Splitting Promote Registry Artifacts out of Finalize Release merges cleanly, nothing on main re-implemented it, and the premise still holds, since main still runs the retag and the chart publish as inline steps of finalize.
Three hunks, two kinds. The workflow hunk is comment-only and convergent. Main landed the same persist-credentials: false fix independently, so the key merges and only the two comment blocks collide. That also makes the last commit on this branch redundant with what main already carries, so drop it on rebase.
The two docs/release.md hunks are a real semantic collision. Main rewrote the workflow inventory from five entries to seven, and rewrote step 4 for the changelog-as-release-body change. This branch edits item 4 of the five-item list, which no longer exists in that shape, and inserts the recovery section next to a step 5 whose surrounding text has moved.
So resolving those two hunks mechanically gives you docs that contradict themselves. Main's new item 5 still says finalize is what "retags the rc images to stable by digest and publishes the stable chart". After this change that is a separate job, so the rebase has to update main's item 5 as well, not only re-apply this branch's edit to the old item 4.
B2: the new invocation split for promote-retag.sh is asserted in a comment and pinned by no test
The diff changes how that script runs in production: from ./hack/promote-retag.sh with the working directory being the script's own tree, to ./.promote-scripts/hack/promote-retag.sh with the working directory being a different tree. Correctness now rests on two path resolutions staying independent, . "$(dirname "$0")/lib/image-refs.sh" landing in .promote-scripts/hack/lib/, and collect_image_refs packages globbing relative to the working directory. The workflow states that requirement in prose, "keep that property if either moves", which is a comment sitting where a test belongs.
hack/promote-retag_test.bats already exists and invokes hack/promote-retag.sh relatively from the repo root in all five cases, so script directory and scanned root are the same place and the split is never exercised. Someone tidying that source line to . hack/lib/image-refs.sh keeps the suite green and breaks the next release, after the tag and the release are irreversible.
The remedy is small and this PR already contains the pattern: promote-publish-chart_test.bats invokes "$ROOT/hack/promote-publish-chart.sh" after cd "$FIX", which is exactly the split. One case in promote-retag_test.bats doing the same, copying a couple of packages/*/*/values.yaml into a temp dir and asserting the dry run still selects owned refs, covers it.
B3: the manual-recovery fallback in docs/release.md names only half the promotion
The new recovery section correctly frames step 5 as both the retag and the chart publish, and then the fallback for a failure outside the 30-day re-run window says to run cd <tag-tree> && /path/to/main/hack/promote-retag.sh vX.Y.Z by hand.
A maintainer following that literally retags the images and never publishes the chart, so the documented helm upgrade --install --version X.Y.Z path still returns manifest unknown. That is precisely the half-finished state this PR exists to make recoverable, reached by following the PR's own recovery instructions. The snippet also omits the registry login both scripts require, and REGISTRY when promoting a fork.
Non-blocking follow-ups
-
hack/promote-publish-chart.shstampspackages/core/installer/values.yamlin place and never restores it. Harmless on an ephemeral runner, but the manual-recovery path the new docs endorse runs it in an operator's own checkout, and there it silently leaves a modified tracked file, which is the leak the comment above that line warns about. TheEXITtrap a few lines up already exists, so restoring that one file costs two lines. -
The registry job runs
hack/scripts fetched from main at run time while holdingpackages: writeagainst the release registry. The workflow comment and the PR body both name the tradeoff, but only as a correctness cost, "a broken promotion script on main breaks the next release". It is also a trust widening, since the code that can write release tags is no longer confined to the reviewed release tree. Worth stating in that comment, because it is the block the next person weighing this design will read. -
The published-chart probe greps an empirical set of registry wordings. Failing closed is the right default, but GHCR can answer for a genuinely nonexistent repository with
deniedorunauthorizedrather than a name-unknown code, so the first-ever push of a brand new chart repo may abort and need a human. A line in the script header would stop the next reader filing that as a bug. -
Pre-existing and outside this diff, so not yours to fix here, but it wants a home. The troubleshooting table in
docs/release.mdblamesDraft release for vX.Y.Z not foundonPublish draft releasehaving lost itsgithub-token, and points atpull-requests-release.yaml:163. At the merge base that step is at line 218 and does carrygithub-token: ${{ steps.app-token.outputs.token }}, so both halves are already wrong, and the release checklist a few lines above repeats the claim. It sends a release-day maintainer chasing a regression that does not exist. Separately, the tag-immutability table's row for this workflow lists the image retag but never the chart publish.
What else I checked
promote-retag.sh inspects each destination tag, skips one already at the expected digest, refuses to move one pointing elsewhere, and re-verifies after copying, so a half-finished promotion genuinely replays. The chart script's content comparison is the other half of that and is correct, including DIFF_STATUS separating diff's "files differ" from a real comparison error, which I ran under sh, dash and bash rather than assuming.
make_latest is set unconditionally as a string before any branch, so needs.finalize.outputs.make_latest == 'true' reads the same decision the release got. Both jobs pin to github.sha, so the chart is still packaged from the tagged tree. The registry job cannot outrun its needs gate, since its if: carries no status function.
The bats suite pierces every guard instead of asserting one exists. The prerelease and malformed-tag tests check that no packaging and no registry traffic happened, the drift test forges a published chart whose content differs, and the probe test injects three real skopeo failure strings and checks nothing was pushed. Seven declared, seven run, seven green, no trap in the file to swallow a failure. The shared image-ref enumeration is not in play, since the chart script publishes one named chart and enumerates nothing.
E2E is the one red check. This diff touches a workflow that only fires when a release PR merges, two files only that workflow invokes, and a maintainer doc, so it cannot reach the E2E path. I am treating it as unrelated.
5b28a92 to
d94cd18
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
docs/release.md (1)
231-231: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winRemove whitespace inside the inline code span
Markdownlint reports MD038 for
`## `. Use`##`with explanatory prose such as “a level-2 section,” or place the exact syntax in a code block.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/release.md` at line 231, Update the inline Markdown example in the changelog validation description to remove the trailing whitespace from the code span, using `##` and prose describing it as a level-2 section; preserve the surrounding explanation and validation behavior.Source: Linters/SAST tools
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/release.md`:
- Line 276: Update the manual recovery guidance in the release procedure to run
both promote-retag.sh and promote-publish-chart.sh, using the same
MOVE_LATEST/latest decision for each command. Keep the existing release-tag
checkout context and clarify that both artifacts are restored when reruns are
unavailable.
---
Outside diff comments:
In `@docs/release.md`:
- Line 231: Update the inline Markdown example in the changelog validation
description to remove the trailing whitespace from the code span, using `##` and
prose describing it as a level-2 section; preserve the surrounding explanation
and validation behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 7d9b11b8-d3f8-403e-a9ce-44157ca889d1
📒 Files selected for processing (4)
.github/workflows/pull-requests-release.yamldocs/release.mdhack/promote-publish-chart.shhack/promote-publish-chart_test.bats
🚧 Files skipped from review as they are similar to previous changes (3)
- hack/promote-publish-chart.sh
- hack/promote-publish-chart_test.bats
- .github/workflows/pull-requests-release.yaml
Aleksei Sviridkin (lexfrei)
left a comment
There was a problem hiding this comment.
NOT LGTM. The conflict is resolved and that finding is closed. The two other blockers from my previous review are both still live, and the rebase did not touch either.
On what the rebase changed, since it bears on how much I re-read: the merge against main is clean now, the credentials commit dropped out as redundant with what main already carried, and the probe-fix commit is byte-identical to before. The only content change is the docs conflict resolution, and it is the right one. The workflow inventory entry now sits on main's seven-item list and keeps the changelog-as-release-body wording while adding the separate-job clause, instead of re-applying the old edit to a list that no longer exists in that shape. Both new files are unchanged, so I did not repeat the full read of them.
Blockers
B2 (still open): the invocation split for promote-retag.sh is asserted in a comment and pinned by no test
The production call is ./.promote-scripts/hack/promote-retag.sh with the working directory pointing at the release tree. That works only because the script sources its helper as . "$(dirname "$0")/lib/image-refs.sh" while scanning via collect_image_refs packages relative to the working directory. Two path resolutions that have to stay independent of each other, stated in a workflow comment and held by nothing executable.
hack/promote-retag_test.bats is not touched by this PR, and none of its eleven cases runs the script from a working directory other than the repo root, so script directory and scanned root remain the same place in every one of them. Main has grown that suite by roughly two hundred lines since this branch forked, adding digest-verification and transient-failure cases, so the file is actively maintained and still does not cover the one property this change newly depends on. Someone tidying that source line to . hack/lib/image-refs.sh keeps all eleven green and breaks the next release, after the tag and the release are already irreversible.
The remedy has a template in this PR: promote-publish-chart_test.bats invokes "$ROOT/hack/promote-publish-chart.sh" after cd "$FIX", which is the split. One case in promote-retag_test.bats copying a couple of packages/*/*/values.yaml into a temp dir, changing into it, invoking by absolute path and asserting the dry run still selects owned refs would pin it.
B3 (still open): the manual-recovery fallback names only half the promotion
The paragraph is unchanged on this head: cd <tag-tree> && /path/to/main/hack/promote-retag.sh vX.Y.Z. That is the retag alone. A maintainer following it outside the 30-day re-run window retags the images and leaves the chart unpublished, which is the half-finished state this PR exists to make recoverable, reached by following this PR's own recovery instructions. The paragraph directly above correctly describes both scripts, which is what makes the omission easy to read past. Adding the promote-publish-chart.sh invocation and the registry login both scripts require would close it.
Non-blocking
The four follow-ups from my earlier review are unchanged and still non-blocking: restoring packages/core/installer/values.yaml from the existing EXIT trap, naming the trust widening as well as the correctness cost in the comment about running scripts from main, a line in the script header about registry wordings for a nonexistent repository, and the two stale claims in the troubleshooting and tag-immutability tables, which this PR does not touch and which are now tracked separately.
Verification on this head
The chart suite is seven declared, seven run, seven green, with no trap in the file. The merge against main is clean.
…th halves Two gaps in the recovery story this PR is built around. The workflow runs ./.promote-scripts/hack/promote-retag.sh with the working directory on the release tree, which works only because the script sources its helper relative to $0 while scanning packages relative to the working directory. Those two resolutions have to stay independent, and that was stated in a workflow comment and held by nothing executable: every existing case runs from the repo root, where script dir and scanned root are the same place. Add a case that copies two package values.yaml into a stand-in tree containing no hack/, changes into it, and invokes the script by absolute path. Mutation check: rewriting the source line to `. hack/lib/image-refs.sh` previously kept all eleven cases green and would have broken the next promotion after the tag and release were already irreversible; it now fails this case, and only this case. The manual fallback named only the retag. A maintainer past the 30-day re-run window would follow it and stop with the images retagged and the chart unpublished — precisely the half-finished state this job exists to make recoverable, reached by following these instructions. Name both scripts and the registry login they need, and record that both are invoked by absolute path from the release tree, as the job runs them. Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
|
Aleksei Sviridkin (@lexfrei) both remaining blockers are fixed — B2, invocation split. B3, half a recovery. The manual fallback now runs both scripts in a code block with the registry login they need, and records that both are invoked by absolute path from the release tree, as the job runs them. The red e2e is not this PR. Noting #3485 overlaps |
## Why `release-1.6` was created at v1.6.0's merge commit on 2026-07-22, and the finalize fixes landed on `main` after that. Workflow files run from the ref they fire on, so a promote PR based on `release-1.6` runs **this** copy of `pull-requests-release.yaml`, not `main`'s. Shipping v1.6.1 from this line today would repeat two defects v1.6.0 already hit. ## What this carries Three upstream commits, after which this file is **byte-identical to `main`'s copy** — so later backports touching it will not conflict. | Upstream | Change | | --- | --- | | `01e1e7188` | `persist-credentials: false` on the checkout | | `f49d54a68` | publish the release with the merged changelog as its body | | `ba67fea7d` | drop `paths-ignore` from the trigger | ### persist-credentials Without it the checkout persists `GITHUB_TOKEN` as an `http.extraheader`, which silently wins over the app token injected by `git remote set-url`. The stable tag then pushes as `GITHUB_TOKEN`, which creates no workflow run, so `tags.yaml` never fires and its `generate-changelog` and `update-website-docs` backstops stay silent. That is exactly what happened to v1.6.0. ### Changelog as release body Without it the release publishes with the draft's own body, `"Promoted from vX.Y.Z-rc.N"`. **These two compound into a permanent defect on a maintenance line.** A patch's changelog is committed to `release-1.6.1`, merged into `release-1.6`, and never reaches `main` — and `update-releasenotes.yaml` only watches `main`, so nothing would ever sync it. The tag-time backstop that would have ported it is dead because of the credential bug. v1.6.1 would ship with placeholder release notes permanently, with no error anywhere. ### paths-ignore Latent rather than active. A promote PR carrying only `docs/changelogs/vX.Y.Z.md` would be dropped by the filter, producing no finalize run, no tag and no error. v1.6.1 will carry tag-string rewrites so it would not have fired, but the filter has no remaining purpose now that the promote PR always carries a changelog. ## On the cherry-pick policy `docs/release.md`'s skip rule says CI-only changes do not belong in a patch. That rule governs release *contents*; backporting release *machinery* so the line is releasable at all is a different thing, and there is precedent already on this branch — #3473, #3474 and #3514 are all CI/release backports. ## Verification `actionlint` clean. `zizmor` clean, no findings. YAML parses. No bats suite references this workflow, and `hack/promote-gate-contract.bats` does not exist on this line, so there is no stale contract test to trip. The diff against `origin/main`'s copy of the file is empty. ## Not included Three known gaps on this line are deliberately out of scope, none of which needs a code change to ship v1.6.1: - `e2e-tag.yaml` is absent and `tags.yaml` has no `rc-e2e` job, so no E2E runs on the rc. Handled by dispatching **E2E Release Tag from `main`** against the published rc tag, which also supplies the promote gate's alternate evidence. - `pull-requests.yaml` has no `labeled` trigger, so adding `full-e2e` to the promote PR starts nothing. Backporting that needs the label-event guards too, and rc-time validation makes it unnecessary here. - finalize is still the pre-#3456 monolith, so a mid-registry failure is not re-runnable and needs the documented hand recovery. ```release-note NONE ```
What this PR does
Finalize's registry side effects — retag the rc images to stable, publish the stable
cozy-installerchart — run after the stable tag and the GitHub release are already irreversible. v1.6.0, the first release to execute that code, died partway through: 32 of 43 repositories untagged, the chart unpublished. Recovery took 64 hand-runskopeo copyinvocations plus a manualhelm package/helm push, because re-running the job checks out the same merge commit and therefore the same broken script — a fix landed onmaincould never reach it.This makes GitHub's own "Re-run failed jobs" the recovery path, rather than adding a workflow to duplicate the tail.
The registry steps become their own job,
Promote Registry Artifacts, whichneeds: finalize. Replaying it does not re-enter tag creation or release publication, andmake_latestis passed in as a job output instead of being recomputed, so the release'slatestflag and the images':latesttag cannot drift apart.Two changes let that re-run carry a fix, not only a retry. The chart publish moves out of the workflow into
hack/promote-publish-chart.sh, so both registry operations are scripts; and the job checks the scripts out frommainat run time while the release tree stays pinned to the promoted merge commit. Landing a correction onmainand pressing re-run applies it to the unchanged release tree.hack/promote-publish-chart.shalso makes the chart step idempotent, which the inline version was not:helm packageis not byte-reproducible, so re-pushing an already-published version would move the stable chart tag onto new bytes. An existing chart is pulled and compared by extracted content — identical is a no-op, different is a hard failure unlessFORCE_CHART=1. Its existence probe usesskopeo inspect --raw, since the image-shaped inspect rejects a Helm OCI artifact on media type.Supersedes #3438, which solved the same problem with a 295-line dispatchable workflow. That lane carried a second implementation of the max-semver
make_latestrule, plus copies of the toolchain bootstrap, the GHCR login and the chart stamping — four things that must never drift from finalize, with nothing in CI to catch it if they did. It would also have been exercised at most once or twice a year, first reached for during an incident.Verification:
actionlintclean;zizmorclean;make bats-unit-tests334/334.hack/promote-publish-chart_test.batsis new and covers tag validation, the stable stamps in the packaged chart, identical-chart skip, differing-chart refusal andFORCE_CHARToverride, and the:latestgate — the idempotency assertion is mutation-checked (making the script always push turns it red). The script/tree split was verified locally: invoked by absolute path from a separate tree,promote-retag.shresolves its helper relative to$0and reads the digest pins from the working directory.Release note
Summary by CodeRabbit
cozy-installerOCI chart.vX.Y.Ztags, skips publishing when content is unchanged, allows forced overwrites, and updates:latestonly when enabled.:latestgating, and retag script path robustness.