ci(release): move release validation to rc time — e2e lane, changelog button, promote gate, docs at promote - #3449
Conversation
Changelog generation and the website-docs PR both lived in tags.yaml, gated on the stable tag push. Under the promote flow that tag is created by pull-requests-release.yaml finalize, which also publishes the release — so both jobs could only ever run once the release was already public. A reader hitting the release in that window saw an empty body, and the site lagged until someone merged the docs PR. Move both to promote time: - promote-rc.yaml gains a `changelog` job that runs in parallel with `promote` and hands the file to a new `open-pr` join job, which commits it onto the release-X.Y.Z branch so it rides the promote PR. Generation is AI-driven and slow while `promote` spends its wall clock on the Talos assets; neither needs the other, so both start at once. - The website-docs job moves to promote-rc.yaml too, depending only on `promote`. Different repo, so it cannot ride the PR — it is opened early and finalize merges it after the release publishes. - finalize reads docs/changelogs/vX.Y.Z.md from the merge commit and uses it as the published release body. update-releasenotes.yaml is no longer on the critical path: it triggers on the same push and would race this job, and if it won it would find no published release, skip, and never fire again. It also only watches main, so a patch release promoted onto release-X.Y was never synced at all. - tags.yaml keeps generate-changelog as a backstop only. It self-skips when the changelog is already on main, and covers the two cases where promotion does not put it there: generation failed (non-blocking), or the promote PR targeted a maintenance line. Generation stays non-blocking, so a flaky AI step cannot wedge a promotion. When it fails the promote PR body says so explicitly, and the escape hatch is to commit docs/changelogs/vX.Y.Z.md to the branch — which also makes a hand-written changelog first-class: if the file is already in the rc tree, generation is skipped entirely. Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
The promote PR now carries docs/changelogs/vX.Y.Z.md, and Prepare stable branch explicitly tolerates producing no tag-string changes when the digests are already stable. Together those can yield a docs-only promote PR, which `paths-ignore: docs/**/*` would filter out — finalize never runs, so the stable tag is never cut and the release is never published, with no error raised anywhere. The `if:` gate (merged + `release` label + release-bot author) is what actually decides whether this workflow does anything, so let it do the filtering. The cost is a skipped job on unrelated closed PRs. Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
Dual-model review (Claude + Codex) of the previous two commits found seven substantive defects, two of them destructive. Both models independently reached three of them; the split is noted per item. Descope the website half (Claude, verified by reproduction). The docs job was moved to promote time, but the website Makefile pins the fetch ref with `override BRANCH := $(RELEASE_TAG)`, which defeats the command-line BRANCH assignment. `make -p` resolves BRANCH to v1.6.0 — a tag that cannot exist at promote time, since promote's own validate step hard-fails if it does. update_apps.sh logs the failed fetch and exits 0 AFTER truncating each destination file, so every app doc would become a stub, `changed=true` would open the PR, and the new auto-merge would land it unreviewed. Not fixable from this repo. update-website-docs returns to tags.yaml where RELEASE_TAG resolves to a real tag, and the finalize auto-merge is dropped. Opening it earlier needs a cozystack/website change first and is left as a follow-up. This also removes the unordered-sibling race Codex found, where the release PR could merge before the docs job opened its PR, leaving finalize nothing to merge and no retry. Fix the non-blocking guarantee (both models). Only the AI step carried continue-on-error; the changelog JOB did not, so a flaky `npm i -g @github/copilot` failed it and `needs` skipped open-pr entirely — staging branch pushed, release drafted, no promote PR. Gate open-pr on promote alone. Generate from a checkout the contract permits (both models). The job checked out the rc staging branch, which docs/agents/changelog.md §2 rejects — a compliant agent stops and writes nothing, so the normal path produced no changelog at all. Check out the rc tag detached and document that configuration in §2. The checkout ref now comes from the validated parse output rather than raw dispatch input. Port, don't regenerate, in the backstop (both models). For a patch release promoted onto release-X.Y the changelog never reaches main, so the backstop fired and ran a second AI pass; once its PR merged, update-releasenotes.yaml overwrote the published body with unreviewed text. Detect the file at the tag and port it verbatim; the AI runs only when it is absent from both main and the tag. Survive re-dispatch (Codex). promote force-pushes the staging branch, erasing a hand-written changelog — the documented recovery when generation fails. Preserve an existing one across the rebuild, and stop open-pr overwriting it with generated output. Validate structurally, not by size (Codex). A truncated or whitespace-only AI run passed `[ -s ]`, was committed, advertised as valid in the PR body, and published verbatim as release notes. Assert the header, the compare link and a plausible line count; finalize additionally refuses whitespace-only content rather than blanking the notes. Add hack/release-changelog-contract.bats — nine tests pinning each link in the chain, mutation-checked: removing open-pr's if:, breaking finalize's body read, and restoring paths-ignore each fail the suite. Update docs/release.md (~10 sites) and docs/agents/changelog.md §2, which described the old flow throughout. Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
Two more review passes (Claude + Codex) on the previous commit. Codex found the one that would have broken the very next promotion. The agent reads its own instructions from the working tree, which is the rc tag. Every rc cut before this change carries the old §2 of docs/agents/changelog.md — two valid configurations, "stop" on anything else — and a detached rc-tag checkout is exactly that "anything else". So promoting v1.6.0-rc.4 would have presented a compliant agent with instructions telling it to refuse, silently, producing no changelog on every promotion of an existing rc. Verified against the tag. The same applies to hack/validate-changelog.sh, absent from those trees entirely, which would have failed the Verify step with "command not found" and, in open-pr, taken the promote PR down with it. Both are now overlaid from the dispatch ref, best-effort, in both jobs. The structural validation added last round rejected every PATCH changelog. It asserted a `# Cozystack vX.Y.Z` header and a 20-line floor; patch releases use `# vX.Y.Z (date)` and v1.5.1 is a complete 19-line release. Good AI output would have been discarded and the promote PR mislabelled "generation failed". Measured across the tree: the header form holds for 23 of 115 changelogs, so it was never an invariant. The predicates now live in hack/validate-changelog.sh, tested directly against real changelogs rather than through YAML greps — the reason the defect shipped is that the suite pinned finalize's guard but nothing on the promote-side validator, and stayed green when the validator was mutated away. It accepts both header conventions, has no line floor, and checks the version in the header, the release-link comment and the compare link, so a changelog generated for the wrong version is caught. All 16 current-era changelogs pass; whitespace, empty, truncated and wrong-version inputs are rejected. Also fixed: - A fatal artifact download in open-pr would abort the job before the PR step — the same wedge as a skipped job, one step lower. Download is now best-effort and the commit and PR body key off what is on disk, not an upstream job output that says nothing about whether the transfer worked. - A pre-existing changelog (hand-written, or preserved across the force-push) reached the PR without ever being validated, was advertised with ✅ and published verbatim. It now goes through the same validator, as does the tags.yaml backstop. - `gh pr view <branch>` matches closed PRs, so re-dispatching after closing an abandoned promote PR edited the dead PR and opened nothing. Branch on state explicitly. - `gh pr edit --body` unconditionally overwrote the PR body, destroying the upstream breaking-change notes docs/release.md instructs reviewers to paste there. Refresh only while the body is still the generated one. - open-pr consumed raw dispatch input while its siblings used the validated parse; the changelog job's bash parse did not trim, so an input with a stray space passed promote and failed there. Both fixed. - The prompt and §2 said "trailing release-link comment"; it is the first thing in every changelog. Tests grow to 16, each mutation-checked: dropping the overlay, making the download fatal, reverting the existing-changelog check to `-s`, and removing the backstop validator each fail the suite. Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
…rep it The existing suite asserts on workflow YAML. That catches a rename or a deleted `if:` and nothing else: it cannot tell whether the shell inside a `run:` block does what the step name claims, and it stayed green while the logic underneath was wrong — twice, in the two preceding rounds. Add hack/release-changelog-behaviour.bats, which runs things for real. The GitHub Actions semantics the whole non-blocking design rests on are now verified by executing them. hack/testdata/needs-semantics.yaml mirrors the promote / changelog / open-pr shapes with a fatal (non-AI) step in the changelog job, and `act` runs it. Result: open-pr DOES run when a job it needs has failed, `needs.changelog.result` is `failure`, and — the part that could not be read off the YAML — a failed job's outputs come back EMPTY, while a successful job's propagate. That empty-output behaviour is precisely why open-pr re-establishes changelog presence from disk instead of trusting needs.changelog.outputs. Deleting the `if:` from the fixture makes open-pr get skipped, so the gate is demonstrably load-bearing. The git logic moves into hack/changelog-preserve.sh and is exercised against real bare remotes: a branch that exists, one that does not, one with no changelog, one with an empty changelog, and the full preserve/checkout -B/restore/force-push cycle promote actually performs. The last of these directly reproduces the regression the preserve step exists to prevent. An empty changelog is deliberately NOT preserved — it would suppress generation and then publish as blank notes. finalize's release-body selection runs under node across all three inputs (good, whitespace-only, absent) and the resulting decision table is compared as a whole. Replace the shell overlay with a sparse second actions/checkout of the dispatch ref at .release-tooling. The overlay had a bootstrap problem I introduced: the script that fetches missing tooling was itself missing from the rc tree, so it could never have worked for exactly the trees that need it. actions/checkout comes from the runner, not the repo, so it has no such circularity. .release-tooling is gitignored because promote runs `git add -A`, and the contract suite pins that. All 21 tests mutation-checked. Breaking changelog-preserve.sh, making it return an empty file, removing the fixture's `if:`, dropping the tooling checkout, and renaming its path each fail the suite and pass on restore. Not covered, and stated in the suite header rather than implied: the AI generation step, the real GitHub API calls, and whether the sparse checkout resolves on GitHub's runners. Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
… checks Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
The release bits currently get their only e2e on the promote PR, which couples validation to promotion and leaves alpha/beta tags with no e2e path at all. This lane decouples them: a workflow_call + dispatch workflow that validates any PUBLISHED release tag — the digest-pinned tree (staging branch for prereleases, the tag itself for stable), the release's nocloud disk asset, and the full Chainsaw suite, mirroring the proven release-path e2e from pull-requests.yaml (no TIA, no PR context). Draft releases fail fast with a clear message: checks in the new release model happen at rc time, not at promote time. A dispatch-only debug input exposes the same SSH breakpoint as the PR lane; workflow_call runs can never trip it. Artifacts are tag-suffixed. Wiring it as the mandatory post-rc-cut validation lands separately in tags.yaml; for alpha/beta/stable this workflow is the manual button. Assisted-By: GPT-5 <noreply@openai.com> Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
Wire the published-tag e2e lane into tags.yaml as a leaf rc-e2e job: needs prepare-release, fires only for -rc. tags (alpha/beta stay button-only via e2e-tag.yaml dispatch), and calls the reusable lane with the pushed tag. The staging branch and published rc release both exist before the job starts because needs waits for the whole prepare-release job (Publish rc release + Create release branch). rc-e2e deliberately also runs when prepare-release short-circuits via release_exists: that flag means skip-rebuild, not skip-validation, and revalidating a published rc is idempotent. The caller grants only contents: read — the lane's checks: write is needed solely by its dispatch-only breakpoint, unreachable under workflow_call. Also correct the cut-prerelease handoff comment, which claimed tags.yaml already ran e2e (it never did). Assisted-By: GPT-5 <noreply@openai.com> Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
Extract the promote-time changelog generation into a reusable workflow (changelog-generate.yaml, workflow_call) and add a dispatchable wrapper (changelog-rc.yaml) so a maintainer can generate the release changelog the moment an rc goes green, while there is still time to review it. The reusable core keeps promote-rc's observable behaviour: detached rc-tag checkout, read-only app token for the AI step, the validate-changelog.sh gate, non-blocking AI failure, and the same changelog-vX.Y.Z artifact. .release-tooling is overlaid from github.ref_name, which under workflow_call is the caller's dispatch ref (and coincides with the reusable workflow's own ref for a local uses:), so "current tooling against an old rc tag" is preserved. Secrets are declared explicitly rather than inherited, so a caller that forgets one fails at parse time. The rc tag is parsed by a shared hack/parse-rc-tag.sh that REJECTS whitespace outright (a stripped 'v1. 6.0-rc.2' would silently become a different tag). promote-rc parses it ONCE in a cheap `parse` job that both `promote` and `changelog` depend on — so they still run in parallel with each other while consuming one validated tag, and a whitespace tag fails the whole promotion rather than letting promote succeed while the changelog job rejects the same input and the PR opens empty. The reusable core mirrors the reject-whitespace policy inline because its parse must run before any checkout. changelog-rc.yaml verifies the rc staging branch release-X.Y.Z-rc.N exists UP FRONT (in parse, which generate depends on), so a missing branch fails in seconds rather than after a 30-minute AI run, then commits the result there as docs/changelogs/vX.Y.Z.md idempotently (compare-before-push). promote-rc's changelog job is a thin uses: call. Source selection — validate-then-fall-through across the rc tag tree, the rc staging branch (the rc-time pickup), then AI generation, discarding any invalid source with a loud warning so it never suppresses a valid lower-priority one — lives in hack/select-changelog-source.sh, executed after the tooling overlay. open-pr carries the picked-up file onto release-X.Y.Z and finalize publishes it. Contract and behaviour bats are repointed and extended: the shared parser, the source-selection script, and promote's parse policy are all exercised by execution on adversarial inputs; the remaining grep pins match executable lines with exact counts so a comment or dead block cannot satisfy them. docs/release.md lists the new workflow and the rc-time entry point. actionlint and zizmor (default and pedantic) are clean. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
…full-e2e label Promotion now assumes checks passed at rc time instead of re-running them. At dispatch, before any staging happens, promote-rc verifies the rc tag has a green full e2e via either evidence path: the rc's tags.yaml push run containing a successful 'rc-e2e / E2E <tag> (full suite)' workflow_call job, or a successful manual e2e-tag.yaml dispatch correlated by its exact tag-bearing job name (immutable rc digests make past evidence valid). No evidence fails the dispatch with both paths and the remediation named. A skip_e2e_gate boolean input bypasses the gate loudly for emergencies; the promote PR body always carries the verification state (verified or bypassed), and 'verified' can only originate in the gate's green branch. The promote PR keeps the release label but no longer gets full-e2e automatically: its tree is the rc's already-validated digests with only tag strings rewritten, so pull-requests.yaml now runs e2e on the release path only when a maintainer opts in by adding full-e2e. The regular-PR path and the ordinary full-e2e forcing are untouched, and the new labeled trigger makes a post-open label actually start the run. A new promote-gate contract suite pins the gate's DAG position, the override default, the label set, the body lines, and the label-gate condition with comment-proof executable-line counts. Assisted-By: GPT-5 <noreply@openai.com> Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
The cozystack/website 'update managed apps reference' PR used to be opened only at tag time by tags.yaml::update-website-docs — after finalize pushed the stable tag, i.e. after the release was already published (too late to review) — and it fetched README content from that stable tag (BRANCH=RELEASE_TAG), which finalize only creates post-merge. Add a promote-time website-docs job to promote-rc.yaml (needs [parse, promote]) that opens the same update-docs-vX.Y.Z PR from the release-X.Y.Z staging branch via the website Makefile's FETCH_REF variable, so the docs are reviewable alongside the promote PR while the stable tag is still unborn. It refreshes the trunk pins from the staging ref before release-next so a new minor inherits fresh Talos/cozystack pins (the v1.6 talos-pin incident class), then runs update-all with FETCH_REF pinned to the staging branch. The job is non-blocking: a FETCH_REF-support guard fails loudly (warn + exit 1) when cozystack/website main predates the FETCH_REF Makefile variable — otherwise make would silently ignore the unknown FETCH_REF override, fall back to the not-yet-created stable tag, and update_apps.sh would warn-and-continue into STUB docs. Any failure surfaces as a website-docs warning line in the promote PR body (plumbed through open-pr via needs.website-docs.result) and never blocks promotion; the tag-time job stays as the backstop and no-ops when promote already did the work. MERGE-TIMING CONTRACT: the website PR must NOT be merged before finalize publishes the release — doing so flips the site's latest-version pointer to a version whose docs and openapi are not on GitHub yet, 404ing every version-pin. The website PR body and the promote PR body both say so in bold. Pin the contract in hack/promote-gate-contract.bats (needs shape, FETCH_REF invocation, the guard, non-persisted checkout credentials, the DO-NOT-MERGE body wording, the promote-PR status line, and the tags.yaml backstop comment) and relax the release-changelog-contract needs pin to tolerate the appended website-docs dependency. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
…ggers tags.yaml pull-requests-release.yaml's finalize job checked out the repo with the default persist-credentials, leaving GITHUB_TOKEN embedded as http.extraheader. Every later tag/branch push re-injects the CI app token via `git remote set-url`, but a persisted extraheader silently wins over the URL credential — so those pushes authenticated as GITHUB_TOKEN. A GITHUB_TOKEN-authenticated push creates no workflow run (GitHub anti-recursion), which is exactly why v1.6.0's stable tag never triggered tags.yaml: the generate-changelog and update-website-docs backstops never ran. Set persist-credentials: false on the finalize checkout, so the set-url app token actually authenticates the pushes — the same handling promote-rc.yaml already uses. Audited every git/gh step in the job: both tag steps set-url the app token before pushing, and Ensure-maintenance-branch / Publish-draft-release / the retag+chart steps use the app token via github-script or a separate registry login — none depends on the persisted GITHUB_TOKEN extraheader. Pin it in hack/promote-gate-contract.bats. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
Final coherence pass over docs/release.md (and the releasing.md pointer) after several incremental patches to the release-cycle rework left the whole-document narrative uneven. - Rewrite the regular and patch release sequences so a maintainer reading top-to-bottom performs the actual flow: cut rc -> watch mandatory rc E2E -> [optional Generate RC Changelog button] -> promote dispatch (E2E gate; changelog generated or picked up; website docs PR opened and parked) -> review the promote PR (no E2E by default; E2E/changelog/website-docs lines) -> merge -> finalize -> merge the parked website PR, then announce. - Purge the last old-model sentence: "vX.Y.0 is tagged from the last RC or a later commit in main" -> promoted from the rc by digest, tag cut write-once at the promote PR merge commit. - State the alpha/beta contract where prereleases are discussed: built and published, button-only E2E (no mandatory rc-e2e), and no promotion path (promote-rc accepts vX.Y.Z-rc.N alone). - Fix the Phase 4 heading (was "chore(release): cut vX.Y.Z"; the PR is "chore(release): promote ... -> ...") and add the missing website-docs line to the reviewer checklist; add a keep-parked-until-publish item to the pre-release checklist. - Add cut-prerelease.yaml, changelog-rc.yaml, and changelog-generate.yaml to "See also" so the workflow inventory is complete and consistent with the "seven workflows + reusable changelog-generate" count. - releasing.md: replace the stale "typically involves ... creating git tags / building artifacts" list (invalidated by the promote-not-rebuild model) with the real high-level sequence. Every workflow/job/step name cited was verified against the YAML on this branch. hack/release-changelog-contract.bats stays green (24/24); no test pins docs/release.md strings, so no test change was needed. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
📝 WalkthroughWalkthroughThis PR adds reusable and RC-time changelog generation, strict changelog validation and sourcing, RC promotion E2E gates, release-tag E2E workflows, promote-time website documentation updates, release-body sourcing, and extensive workflow contract and behavior tests. ChangesRelease automation
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related issues
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 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. |
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
The rc-E2E gate contract used ripgrep for every pin. The unit-test runner has no ripgrep, so `code_lines()` hit "rg: not found", the `|| true` on it swallowed the 127, and every pin degraded to "0 matches" — the suite failed on the first assertion with no hint that the tool, not the workflow, was missing. No other hack/*.bats uses ripgrep. Swap all pins to POSIX grep and make the filter fail closed: grep exits 1 when nothing is selected (legitimate for an empty block) and 2 on an actual error, so only the latter propagates instead of silently yielding no lines. Verified 13/13 green both normally and with `rg`/`fd` shimmed out of PATH to reproduce the runner, plus the eleven suites the aborted CI run never reached (all green, zero shim hits). Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (9)
hack/changelog-preserve.sh (1)
46-50: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winThe
[ ! -s "$OUTFILE" ] &&short-circuit makes a pre-populated<outfile>silently report as "preserved".If
$OUTFILEalready exists non-empty,git shownever runs, the emptiness check at Line 52 passes, and the script exits 0 claiming the branch content was preserved — while the caller actually gets whatever was already at that path. Both current callers happen to pass an empty/absent file (mktemp, or a just-rm -f'd$CL), so this is latent rather than live, but the contract in the header ("On success the changelog is written to<outfile>") is only true by accident.♻️ Make the fetch unconditional
-if [ ! -s "$OUTFILE" ] && ! git show "FETCH_HEAD:${CL}" > "$OUTFILE" 2>/dev/null; then +if ! git show "FETCH_HEAD:${CL}" > "$OUTFILE" 2>/dev/null; then echo "Could not read ${CL} from ${BRANCH}; nothing to preserve." >&2 rm -f "$OUTFILE" exit 1 fi🤖 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 `@hack/changelog-preserve.sh` around lines 46 - 50, Update the fallback around OUTFILE so git show always fetches FETCH_HEAD:${CL} into OUTFILE, regardless of whether OUTFILE already exists or is non-empty. Preserve the existing failure message, cleanup, and exit behavior when the fetch fails, ensuring successful execution always writes the branch content to OUTFILE..github/workflows/promote-rc.yaml (1)
184-228: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winEvidence B walks every retained successful
e2e-tag.yamldispatch run with a jobs API call each — inside a 5-minute job that fails closed.
github.paginateover all successful dispatch runs, then one paginatedlistJobsForWorkflowRunper run sequentially, scales with 90 days of retention. A slow or rate-limited scan trips thetimeout-minutes: 5onparse(Line 63) and blocks a promotion that actually has green evidence. Consider bounding the scan (cap the number of runs examined, or filter bycreated: >=<rc cut date>) and reporting "scanned N of M" in the failure message so the operator can tell "no evidence" from "gave up".🤖 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 @.github/workflows/promote-rc.yaml around lines 184 - 228, The Evidence B scan in the workflow’s dispatch-run verification block is unbounded and sequential, risking the five-minute job timeout. Bound the successful workflow_dispatch runs examined using an appropriate cap or rc-date filter, track the total and scanned counts, and include “scanned N of M” in the failure message while preserving successful evidence detection and fail-closed behavior.docs/release.md (1)
560-560: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueHardcoded workflow line number will rot.
pull-requests-release.yaml:163drifts on the next edit to that file; referencing the step name (Publish draft release) alone is enough to locate it.🤖 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 560, Update the release troubleshooting table entry for “Draft release for vX.Y.Z not found” to replace the hardcoded pull-requests-release.yaml line number with the stable workflow step name “Publish draft release,” preserving the surrounding remediation guidance.hack/release-changelog-behaviour.bats (1)
393-393: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valuePrefer bats
skipoverreturn 0for unavailable tooling.
skip "node unavailable"reports these as skipped rather than green, which matters for the act/Docker case the header itself calls out as "a silent skip … indistinguishable from a green result".Also applies to: 442-442, 523-526
🤖 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 `@hack/release-changelog-behaviour.bats` at line 393, Replace the `return 0` fallback after the `command -v node` check with bats’ `skip` mechanism, using the existing “node unavailable” reason. Apply the same change to the additional unavailable-tooling checks at the referenced locations, preserving their current detection conditions.hack/release-changelog-contract.bats (2)
68-70: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueStale header note: the docs job no longer runs only at tag push.
This PR moves the
cozystack/websitedocs PR to promote time (promote-rc.yaml::website-docs), andhack/promote-gate-contract.batsnow pins exactly that. The "deliberately still runs at tag push / moving it earlier needs a change in that repo first" wording will mislead the next reader.🤖 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 `@hack/release-changelog-contract.bats` around lines 68 - 70, Update the stale header comment in the release changelog contract to reflect that the cozystack/website docs job now runs at promote time via promote-rc.yaml::website-docs, as covered by hack/promote-gate-contract.bats. Remove the outdated tag-push-only and “needs a change in that repo” wording while preserving the note’s purpose.
341-358: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueGlob will drift past v1.9.
v1.[3-9]*.mdsilently stops covering new minors atv1.10.0(and anyv2.x). Thechecked -ge 10anti-vacuum guard won't catch that, since older files keep the count up. Consider globbing allv*.mdand filtering out the pre-v1.3era by version comparison instead.🤖 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 `@hack/release-changelog-contract.bats` around lines 341 - 358, Update the changelog iteration in the release contract test to match all v*.md files, then filter entries by parsed semantic version so only versions at or above v1.3 are validated. Preserve the existing checked count and rejection reporting, while ensuring future v1 minors and v2.x changelogs are included.hack/promote-gate-contract.bats (1)
33-35: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider
grepfor consistency/portability.
hack/release-changelog-contract.batsdefines the same helper asgrep -vE '^[[:space:]]*#', and every other assertion here could use POSIXgrep. Depending onrgmakes this suite fail (rather than assert) on any runner without ripgrep installed.♻️ Optional alignment with the sibling suite
-code_lines() { - rg -v '^[[:space:]]*#' || true -} +code_lines() { grep -vE '^[[:space:]]*#' || true; }Note
rg -cF/rg -qF/rg -ncall sites would needgrep -cF/grep -qF/grep -nequivalents.🤖 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 `@hack/promote-gate-contract.bats` around lines 33 - 35, Replace the ripgrep dependency in the test helpers and assertions with POSIX grep for portability. Update code_lines() to use the sibling suite’s grep -vE pattern, and convert the visible rg count, fixed-string, quiet, and line-number call sites to equivalent grep options while preserving existing assertion behavior..github/workflows/pull-requests-release.yaml (1)
331-372: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winConsider surfacing (warn-only) the stronger
hack/validate-changelog.shchecks before publishing the changelog as the public release body.This only checks the file is non-empty/non-whitespace before using it as
bodyfor the published release.hack/validate-changelog.sh(already used bytags.yaml's backstop and presumably bypromote-rc.yaml) additionally checks the version header, theFull Changelogcompare link, and section presence — a changelog that's non-empty but structurally wrong (e.g. truncated, or generated for the wrong version) would pass this check and become the permanent public release body.Given the comment's own rationale (never block publication over changelog quality), a warn-only invocation — log
::warning::and fall through to the current existing-body behavior on failure — would preserve the intended leniency while catching more classes of bad changelog before they go public.♻️ Proposed enhancement
if (raw.trim().length === 0) { console.log(`::warning::${changelogPath} is empty or whitespace-only — refusing to use it as the release body for ${tag}. Publishing with the draft's existing body instead.`); } else { - body = raw; - console.log(`📝 Release body from ${changelogPath} (${body.length} bytes)`); + const { execFileSync } = require('child_process'); + try { + execFileSync('hack/validate-changelog.sh', [changelogPath, tag.replace(/^v/, '')], { stdio: 'pipe' }); + body = raw; + console.log(`📝 Release body from ${changelogPath} (${body.length} bytes)`); + } catch (e) { + console.log(`::warning::${changelogPath} failed structural validation (${e.stderr || e.message}) — publishing ${tag} with the draft's existing body instead.`); + } }🤖 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 @.github/workflows/pull-requests-release.yaml around lines 331 - 372, Before assigning a non-empty changelog to body in the release publication flow, invoke hack/validate-changelog.sh for the target tag/file and capture its result. On validation failure, emit a ::warning:: with the validation details and leave body unset so updateRelease preserves the draft’s existing body; retain the current missing and whitespace-only handling, and do not block publication..github/workflows/pull-requests.yaml (1)
8-11: 🎯 Functional Correctness | 🔵 TrivialVerify required-status-check behavior now that
labeledis a trigger for every label, not justfull-e2e.The
plan/resolve_assetsguards correctly no-op this workflow's jobs for anylabeledevent other thanfull-e2e, and thee2egating logic (finalize-success OR resolve_assets-success-with-both-labels) is internally consistent for every opened/synchronize/labeled combination traced through this file. One thing worth confirming operationally: sincelabelednow fires this workflow on every label addition to a PR (e.g. someone adding an unrelated triage label), and the guarded jobs then report asskippedfor that specific run/commit, please confirm this doesn't interact awkwardly with branch-protection required-status-check evaluation on the same commit SHA (i.e. that askippedrun from an unrelated label doesn't need a subsequent push to re-establish a passing check).Also applies to: 31-31, 451-453, 542-555
🤖 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 @.github/workflows/pull-requests.yaml around lines 8 - 11, Verify operationally that adding any non-full-e2e label triggers the workflow without causing its skipped plan/resolve_assets jobs to leave required status checks pending or failing for the current commit SHA. Inspect the required-check configuration and, if needed, adjust the workflow’s status/check reporting around the plan, resolve_assets, and e2e gating logic so unrelated labeled events do not require a subsequent push while preserving full-e2e behavior.
🤖 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 @.github/workflows/tags.yaml:
- Around line 314-333: Update the rc-e2e job in .github/workflows/tags.yaml at
lines 314-333 to add checks: write alongside contents: read in its permissions
block, matching the permission declared by the reusable workflow. The referenced
workflow in .github/workflows/e2e-tag.yaml at lines 123-133 requires checks:
write; no direct change is needed there.
In `@docs/agents/changelog.md`:
- Around line 57-73: Update the Step 2 checklist entry to state that the working
tree must be in one of the three valid configurations described in Section 2,
rather than requiring the main branch. Keep the checklist wording aligned with
the interactive, release-tag CI, and release-candidate promotion cases.
In `@docs/release.md`:
- Line 231: Remove the trailing space inside the inline code span containing the
section-header example in the changelog validation documentation, while
preserving the intended reference to a level-two Markdown heading.
In `@hack/release-changelog-behaviour.bats`:
- Around line 434-437: Scope the `.trim()` assertion in the release changelog
behavior test to the `job_block parse` section of `$PROMOTE`, matching the
existing scoped assertion in `hack/release-changelog-contract.bats`; keep the
whitespace rejection check unchanged and ensure unrelated `.trim()` calls in
other github-script blocks do not fail the test.
In `@hack/testdata/needs-semantics.yaml`:
- Around line 1-4: Correct the header comment in the needs-semantics fixture to
reference hack/release-changelog-behaviour.bats instead of
hack/release-changelog-contract.bats; leave the fixture content and its
description unchanged.
---
Nitpick comments:
In @.github/workflows/promote-rc.yaml:
- Around line 184-228: The Evidence B scan in the workflow’s dispatch-run
verification block is unbounded and sequential, risking the five-minute job
timeout. Bound the successful workflow_dispatch runs examined using an
appropriate cap or rc-date filter, track the total and scanned counts, and
include “scanned N of M” in the failure message while preserving successful
evidence detection and fail-closed behavior.
In @.github/workflows/pull-requests-release.yaml:
- Around line 331-372: Before assigning a non-empty changelog to body in the
release publication flow, invoke hack/validate-changelog.sh for the target
tag/file and capture its result. On validation failure, emit a ::warning:: with
the validation details and leave body unset so updateRelease preserves the
draft’s existing body; retain the current missing and whitespace-only handling,
and do not block publication.
In @.github/workflows/pull-requests.yaml:
- Around line 8-11: Verify operationally that adding any non-full-e2e label
triggers the workflow without causing its skipped plan/resolve_assets jobs to
leave required status checks pending or failing for the current commit SHA.
Inspect the required-check configuration and, if needed, adjust the workflow’s
status/check reporting around the plan, resolve_assets, and e2e gating logic so
unrelated labeled events do not require a subsequent push while preserving
full-e2e behavior.
In `@docs/release.md`:
- Line 560: Update the release troubleshooting table entry for “Draft release
for vX.Y.Z not found” to replace the hardcoded pull-requests-release.yaml line
number with the stable workflow step name “Publish draft release,” preserving
the surrounding remediation guidance.
In `@hack/changelog-preserve.sh`:
- Around line 46-50: Update the fallback around OUTFILE so git show always
fetches FETCH_HEAD:${CL} into OUTFILE, regardless of whether OUTFILE already
exists or is non-empty. Preserve the existing failure message, cleanup, and exit
behavior when the fetch fails, ensuring successful execution always writes the
branch content to OUTFILE.
In `@hack/promote-gate-contract.bats`:
- Around line 33-35: Replace the ripgrep dependency in the test helpers and
assertions with POSIX grep for portability. Update code_lines() to use the
sibling suite’s grep -vE pattern, and convert the visible rg count,
fixed-string, quiet, and line-number call sites to equivalent grep options while
preserving existing assertion behavior.
In `@hack/release-changelog-behaviour.bats`:
- Line 393: Replace the `return 0` fallback after the `command -v node` check
with bats’ `skip` mechanism, using the existing “node unavailable” reason. Apply
the same change to the additional unavailable-tooling checks at the referenced
locations, preserving their current detection conditions.
In `@hack/release-changelog-contract.bats`:
- Around line 68-70: Update the stale header comment in the release changelog
contract to reflect that the cozystack/website docs job now runs at promote time
via promote-rc.yaml::website-docs, as covered by
hack/promote-gate-contract.bats. Remove the outdated tag-push-only and “needs a
change in that repo” wording while preserving the note’s purpose.
- Around line 341-358: Update the changelog iteration in the release contract
test to match all v*.md files, then filter entries by parsed semantic version so
only versions at or above v1.3 are validated. Preserve the existing checked
count and rejection reporting, while ensuring future v1 minors and v2.x
changelogs are included.
🪄 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: c6a96e4b-ccac-42a9-96ab-b3f72474ce5d
📒 Files selected for processing (20)
.github/workflows/changelog-generate.yaml.github/workflows/changelog-rc.yaml.github/workflows/cut-prerelease.yaml.github/workflows/e2e-tag.yaml.github/workflows/promote-rc.yaml.github/workflows/pull-requests-release.yaml.github/workflows/pull-requests.yaml.github/workflows/tags.yaml.gitignoredocs/agents/changelog.mddocs/agents/releasing.mddocs/release.mdhack/changelog-preserve.shhack/parse-rc-tag.shhack/promote-gate-contract.batshack/release-changelog-behaviour.batshack/release-changelog-contract.batshack/select-changelog-source.shhack/testdata/needs-semantics.yamlhack/validate-changelog.sh
| - Copilot quota exhausted (`COPILOT_GITHUB_TOKEN` 402). Refill or rotate, then re-dispatch. | ||
| - AI step timeout (hard 30-min cap). | ||
| - Output file empty — caught by the next step's `[ -s ]` check, fails loudly. | ||
| - Output truncated or whitespace-only — caught by the `Verify changelog` step (`hack/validate-changelog.sh`), which accepts either header convention (`# Cozystack vX.Y.Z` for a minor or `# vX.Y.Z (<date>)` for a patch), and requires the leading release-link comment pointing at `releases/tag/vX.Y.Z`, a `compare/...vX.Y.Z` link ending in this version, and at least one `## ` section — deliberately with no line-count floor, since a complete short patch changelog (v1.5.1 ships in 19 lines) must not be rejected as a fragment. It downgrades to the missing-changelog path rather than shipping a fragment as release notes. |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
markdownlint MD038: trailing space inside the `## ` code span.
CommonMark also strips the padding space, so the span renders as ## anyway.
🔧 Proposed fix
-... and at least one `## ` section — deliberately with no line-count floor, ...
+... and at least one `##` section heading — deliberately with no line-count floor, ...📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| - Output truncated or whitespace-only — caught by the `Verify changelog` step (`hack/validate-changelog.sh`), which accepts either header convention (`# Cozystack vX.Y.Z` for a minor or `# vX.Y.Z (<date>)` for a patch), and requires the leading release-link comment pointing at `releases/tag/vX.Y.Z`, a `compare/...vX.Y.Z` link ending in this version, and at least one `## ` section — deliberately with no line-count floor, since a complete short patch changelog (v1.5.1 ships in 19 lines) must not be rejected as a fragment. It downgrades to the missing-changelog path rather than shipping a fragment as release notes. | |
| - Output truncated or whitespace-only — caught by the `Verify changelog` step (`hack/validate-changelog.sh`), which accepts either header convention (`# Cozystack vX.Y.Z` for a minor or `# vX.Y.Z (<date>)` for a patch), and requires the leading release-link comment pointing at `releases/tag/vX.Y.Z`, a `compare/...vX.Y.Z` link ending in this version, and at least one `##` section heading — deliberately with no line-count floor, since a complete short patch changelog (v1.5.1 ships in 19 lines) must not be rejected as a fragment. It downgrades to the missing-changelog path rather than shipping a fragment as release notes. |
🧰 Tools
🪛 markdownlint-cli2 (0.23.0)
[warning] 231-231: Spaces inside code span elements
(MD038, no-space-in-code)
🤖 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, Remove the trailing space inside the inline
code span containing the section-header example in the changelog validation
documentation, while preserving the intended reference to a level-two Markdown
heading.
Source: Linters/SAST tools
…ferences Three review nits, all verified against the tree, plus a tighter scope than the first attempt at them. The behaviour suite asserted "promote-rc does not trim the rc tag" against the whole file. promote-rc.yaml has no .trim() today, so it passed, but any unrelated .trim() added anywhere in the workflow would have failed a test about tag policy. Scoping it to the parse JOB was not enough either: that job is ~180 lines, most of them the e2e gate's github-script, whose string handling has nothing to do with tag policy. Both greps are now scoped to the parse STEP, and run through a code_lines filter copied from the contract suite so a YAML comment mentioning .trim() cannot fail them. Mutation-checked in both directions: a .trim() injected into the parse step fails the test, one injected into the e2e-gate step of the same job does not. - The Step 2 checklist entry still said "must be main" while Step 2 itself now lists three valid starting points (main, detached at the release tag, detached at the rc tag being promoted). The checklist is what an agent ticks off, so it was the half that mattered; it now points at Step 2 rather than re-encoding the count, so a fourth configuration cannot desync it. The heading said "Checking current branch", which two of the three states do not have. - hack/testdata/needs-semantics.yaml named release-changelog-contract.bats as its consumer; the only consumer is release-changelog-behaviour.bats. 50/50 tests green across the three suites (11 + 24 + 15). Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
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-gate-contract.bats`:
- Around line 266-271: Update the assertions in the relevant promote-gate
contract test to verify that the called workflow block, represented by e2e_job
and checked through code_lines, explicitly contains “contents: read” in addition
to its existing “checks: write” assertion. Keep the caller workflow assertions
for rc_e2e unchanged.
🪄 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: 44895206-375c-451d-bb9a-78652e62b84e
📒 Files selected for processing (7)
.github/workflows/changelog-rc.yaml.github/workflows/e2e-tag.yaml.github/workflows/tags.yamldocs/agents/changelog.mdhack/promote-gate-contract.batshack/release-changelog-behaviour.batshack/testdata/needs-semantics.yaml
🚧 Files skipped from review as they are similar to previous changes (4)
- .github/workflows/e2e-tag.yaml
- docs/agents/changelog.md
- .github/workflows/changelog-rc.yaml
- hack/release-changelog-behaviour.bats
| e2e_job="$(job_block e2e "$E2E_TAG")" | ||
| [ -n "$e2e_job" ] | ||
| printf '%s\n' "$e2e_job" | code_lines | grep -qF 'checks: write' | ||
|
|
||
| printf '%s\n' "$rc_e2e" | code_lines | grep -qF 'contents: read' | ||
| printf '%s\n' "$rc_e2e" | code_lines | grep -qF 'checks: write' |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Pin contents: read in the called workflow too.
The test asserts only the child’s checks: write; removing its contents: read declaration would still pass despite violating the stated caller/callee permission contract.
Proposed fix
e2e_job="$(job_block e2e "$E2E_TAG")"
[ -n "$e2e_job" ]
+ printf '%s\n' "$e2e_job" | code_lines | grep -qF 'contents: read'
printf '%s\n' "$e2e_job" | code_lines | grep -qF 'checks: write'📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| e2e_job="$(job_block e2e "$E2E_TAG")" | |
| [ -n "$e2e_job" ] | |
| printf '%s\n' "$e2e_job" | code_lines | grep -qF 'checks: write' | |
| printf '%s\n' "$rc_e2e" | code_lines | grep -qF 'contents: read' | |
| printf '%s\n' "$rc_e2e" | code_lines | grep -qF 'checks: write' | |
| e2e_job="$(job_block e2e "$E2E_TAG")" | |
| [ -n "$e2e_job" ] | |
| printf '%s\n' "$e2e_job" | code_lines | grep -qF 'contents: read' | |
| printf '%s\n' "$e2e_job" | code_lines | grep -qF 'checks: write' | |
| printf '%s\n' "$rc_e2e" | code_lines | grep -qF 'contents: read' | |
| printf '%s\n' "$rc_e2e" | code_lines | grep -qF 'checks: write' |
🤖 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 `@hack/promote-gate-contract.bats` around lines 266 - 271, Update the
assertions in the relevant promote-gate contract test to verify that the called
workflow block, represented by e2e_job and checked through code_lines,
explicitly contains “contents: read” in addition to its existing “checks: write”
assertion. Keep the caller workflow assertions for rc_e2e unchanged.
|
Git push to origin failed for release-1.5 with exitcode 1 |
|
Git push to origin failed for release-1.6 with exitcode 1 |
1 similar comment
|
Git push to origin failed for release-1.6 with exitcode 1 |
|
Git push to origin failed for release-1.5 with exitcode 1 |
What this PR does
Moves release validation from promote time to rc time, making promotion itself a pure retag operation. This is the follow-up to the v1.6.0 retrospective: the promote flow's checks ran on the promote PR (coupled to promotion, skippable by never promoting), the changelog and website docs were produced only after the release went public, and the finalize tag push silently failed to trigger
tags.yamlat all (persisted-credential bug, root-caused below).The release lifecycle after this PR:
cut-prerelease.yamlcuts an rc;tags.yamlbuilds, drafts, pushes the staging branch — and now runs a mandatory full e2e for every-rc.Ntag via the new reusablee2e-tag.yamllane. Alpha/beta (and any published tag) get the same validation as a manual button (workflow_dispatch).changelog-rc.yamlbutton: the release changelog is generated from the rc tag (reusablechangelog-generate.yaml) and committed to the rc staging branch, ready for review long before promotion.promote-rc.yamldispatch is gated on green rc e2e (evidence: the rc'stags.yamlrun containing a successfulrc-e2ejob, or a successful manuale2e-tagdispatch for the tag;skip_e2e_gateinput for emergencies, always surfaced in the PR body as ✅/full-e2eautomatically — its tree is the rc's already-validated digests with only tag strings rewritten; a maintainer can still add the label manually to force the suite.persist-credentials: falseits tag push now actually authenticates as the CI app, so thetags.yamlbackstops (generate-changelog,update-website-docs) fire for promoted tags. This is the one-line fix for the v1.6.0 incident where the tag triggered nothing: the checkout's persistedGITHUB_TOKENhttp.extraheadersilently defeats the app token injected viagit remote set-url, andGITHUB_TOKENpushes create no workflow runs.Everything is pinned by executable contract tests (
hack/promote-gate-contract.bats, extendedhack/release-changelog-contract.bats/release-changelog-behaviour.bats— 48 tests total, including behaviour tests that execute the newhack/parse-rc-tag.shandhack/select-changelog-source.shfor real), anddocs/release.mdis rewritten to describe the new lifecycle end-to-end.Commit map (review commit-by-commit)
75b5192f1…dd308644c+6fc970851— the pre-existing changelog-before-publication rework (changelog generated at promote, committed to the staging branch before the PR opens, finalize publishes it as the release body; tags.yaml job becomes a verbatim-port backstop), rebased onto main.5aec70b3a—e2e-tag.yaml: reusable published-tag e2e lane (prerelease → staging branch, stable → tag; draft releases fail fast; dispatch-only SSHdebugbreakpoint; tag-suffixed artifacts).025564d96—tags.yaml::rc-e2e: mandatory full e2e for freshly cut rc tags (also onrelease_existsre-runs); fixes thecut-prerelease.yamlcomment that falsely claimed e2e already existed.2dfbeea77—changelog-generate.yaml(reusable core) +changelog-rc.yaml(rc-time button committing to the staging branch); promote-rc gains a top-levelparsejob with a strict no-whitespace tag policy (hack/parse-rc-tag.sh); source selection is validate-then-fallthrough (hack/select-changelog-source.sh) so an invalid higher-priority changelog can never silently win.ff7ddd3c9— the promote e2e gate +skip_e2e_gateoverride + removal of the automaticfull-e2elabel (pull-requests.yaml release-path e2e now requires an explicit label; a post-open label now actually starts the run).936190d4b—website-docsjob at promote time behind a FETCH_REF-support guard (see dependency below), with the DO-NOT-MERGE-until-finalize contract and a pin-freshness pass beforerelease-next;tags.yaml::update-website-docsstays as the backstop.01e1e7188— thepersist-credentials: falsefinalize fix (v1.6.0 root cause).717c8860f—docs/release.mdcoherence pass over the whole new lifecycle.Dependency
The promote-time website docs job needs cozystack/website#631 (⚠️ line in the promote PR body — promotion is unaffected and the tag-time backstop backfills, so this PR is safe to merge first.
FETCH_REF+ fail-loud fetch). Until that merges, the job's guard detects the missing support and degrades to aMerge instructions
Please merge-commit, do not squash: the 13 commits are deliberately atomic (the credential root-cause fix is separable from the feature work) and squashing would collapse ~3k lines into one commit, hurting bisectability.
First live run
The e2e lane is verified by lint, contract tests and dry-traces, but has not yet executed against a real tag; the intended first live run is a manual
e2e-tag.yamldispatch against an existing published rc tag before the gate is relied on for a real promotion.Release note
Summary by CodeRabbit