Skip to content

ci(release): make the registry promotion a re-runnable job - #3456

Open
myasnikovdaniil wants to merge 3 commits into
mainfrom
ci/registry-finalize-rerunnable
Open

ci(release): make the registry promotion a re-runnable job#3456
myasnikovdaniil wants to merge 3 commits into
mainfrom
ci/registry-finalize-rerunnable

Conversation

@myasnikovdaniil

@myasnikovdaniil myasnikovdaniil commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

What this PR does

Finalize's registry side effects — retag the rc images to stable, publish the stable cozy-installer chart — 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-run skopeo copy invocations plus a manual helm package/helm 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.

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, which needs: finalize. Replaying it does not re-enter tag creation or release publication, and make_latest is passed in as a job output instead of being recomputed, so the release's latest flag and the images' :latest tag 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 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 applies it to the unchanged release tree.

hack/promote-publish-chart.sh 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=1. Its existence probe uses skopeo 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_latest rule, 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: actionlint clean; zizmor clean; make bats-unit-tests 334/334. hack/promote-publish-chart_test.bats is new and covers tag validation, the stable stamps in the packaged chart, identical-chart skip, differing-chart refusal and FORCE_CHART override, and the :latest gate — 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.sh resolves its helper relative to $0 and reads the digest pins from the working directory.

Release note

NONE

Summary by CodeRabbit

  • New Features
    • Added a separate, re-runnable registry promotion step for stable releases, including stable retagging and publishing the cozy-installer OCI chart.
    • Added a repeatable chart promotion flow that validates stable vX.Y.Z tags, skips publishing when content is unchanged, allows forced overwrites, and updates :latest only when enabled.
  • Documentation
    • Updated the release runbook to split “Finalize Release” from registry promotion and expanded manual recovery guidance.
  • CI / Chores
    • Refined release workflow outputs, permissions, and credential handling to avoid side effects during promotion.
  • Tests
    • Added Bats tests covering validation, rerun safety, drift/overwrite behavior, failure handling, :latest gating, and retag script path robustness.

@dosubot dosubot Bot added area/ci Issues or PRs related to CI workflows, GitHub Actions, automation area/release Issues or PRs related to release tooling (changelog, backport, release pipeline) labels Jul 27, 2026
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 70c26902-d5c1-43f6-b64e-26fee1bdfe8d

📥 Commits

Reviewing files that changed from the base of the PR and between d94cd18 and eff66a7.

📒 Files selected for processing (2)
  • docs/release.md
  • hack/promote-retag_test.bats
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/release.md

📝 Walkthrough

Walkthrough

The 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.

Changes

Release promotion

Layer / File(s) Summary
Chart promotion script
hack/promote-publish-chart.sh
Validates stable tags, stamps and packages the chart, compares existing registry content, supports forced replacement, and optionally moves :latest.
Release workflow integration
.github/workflows/pull-requests-release.yaml, docs/release.md
Separates finalization from a stable-only registry job, passes tag and make_latest outputs, checks out pinned release inputs with promotion scripts from main, and documents recovery behavior.
Promotion behavior tests
hack/promote-publish-chart_test.bats, hack/promote-retag_test.bats
Tests validation, initial publishing, reruns, content drift, probe failures, latest-tag gating, and independent script and release-tree paths.

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
Loading

Possibly related issues

Possibly related PRs

  • cozystack/cozystack#3449: Modifies the same release workflow’s checkout credential persistence and finalization behavior.

Suggested labels: kind/feature

Suggested reviewers: androndo, sircthulhu, kvaps

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: making registry promotion re-runnable as part of the release workflow.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ci/registry-finalize-rerunnable

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

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 66c96b4 and 2a1e981.

📒 Files selected for processing (4)
  • .github/workflows/pull-requests-release.yaml
  • docs/release.md
  • hack/promote-publish-chart.sh
  • hack/promote-publish-chart_test.bats

Comment thread hack/promote-publish-chart.sh Outdated
@github-actions github-actions Bot added the size/XL This PR changes 500-999 lines, ignoring generated files label Jul 27, 2026
myasnikovdaniil added a commit that referenced this pull request Jul 28, 2026
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>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  1. hack/promote-publish-chart.sh stamps packages/core/installer/values.yaml in 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. The EXIT trap a few lines up already exists, so restoring that one file costs two lines.

  2. The registry job runs hack/ scripts fetched from main at run time while holding packages: write against 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.

  3. 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 denied or unauthorized rather 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.

  4. Pre-existing and outside this diff, so not yours to fix here, but it wants a home. The troubleshooting table in docs/release.md blames Draft release for vX.Y.Z not found on Publish draft release having lost its github-token, and points at pull-requests-release.yaml:163. At the merge base that step is at line 218 and does carry github-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.

@myasnikovdaniil
myasnikovdaniil force-pushed the ci/registry-finalize-rerunnable branch from 5b28a92 to d94cd18 Compare July 29, 2026 13:41

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Remove 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

📥 Commits

Reviewing files that changed from the base of the PR and between 5b28a92 and d94cd18.

📒 Files selected for processing (4)
  • .github/workflows/pull-requests-release.yaml
  • docs/release.md
  • hack/promote-publish-chart.sh
  • hack/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

Comment thread docs/release.md Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@myasnikovdaniil

Copy link
Copy Markdown
Contributor Author

Aleksei Sviridkin (@lexfrei) both remaining blockers are fixed — eff66a75b.

B2, invocation split. hack/promote-retag_test.bats gains 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, so the helper can only resolve from the script dir while the scan resolves from the working dir. Mutation-checked with your exact scenario: rewriting the source line to . hack/lib/image-refs.sh previously kept all eleven green and now fails this case, and only this case. I used the cd "$FIX" + "$ROOT/..." shape from promote-publish-chart_test.bats you pointed at.

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. kubernetes-latest and kubernetes-previous fail identically on #3276, #3262, #3456 and #3294, and today's nightly on main is 43/43 green including both. Same mechanism everywhere — node-join failed: fewer than 2 tenant nodes Ready, tenant cilium never installing, dependents cascading. That points at the PR e2e lane rather than any PR's content; the structural difference from nightly is that PR runs reconstruct their image set via pr.patch plus hack/overlay-main-images.sh. #3471 happens to rework exactly that file and has a fresh run in flight, which should be informative. Flagging rather than asserting — I have not confirmed the cause.

Noting #3485 overlaps docs/release.md with this PR, so whoever lands second should expect a touch-up — happy to fold it in here if you would rather it went with the other release-docs changes.

myasnikovdaniil added a commit that referenced this pull request Aug 4, 2026
## 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
```
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/ci Issues or PRs related to CI workflows, GitHub Actions, automation area/release Issues or PRs related to release tooling (changelog, backport, release pipeline) size/XL This PR changes 500-999 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants