Skip to content

ci(release): move release validation to rc time — e2e lane, changelog button, promote gate, docs at promote - #3449

Merged
myasnikovdaniil merged 16 commits into
mainfrom
ci/release-checks-at-rc
Jul 29, 2026
Merged

ci(release): move release validation to rc time — e2e lane, changelog button, promote gate, docs at promote#3449
myasnikovdaniil merged 16 commits into
mainfrom
ci/release-checks-at-rc

Conversation

@myasnikovdaniil

@myasnikovdaniil myasnikovdaniil commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

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.yaml at all (persisted-credential bug, root-caused below).

The release lifecycle after this PR:

  1. cut-prerelease.yaml cuts an rc; tags.yaml builds, drafts, pushes the staging branch — and now runs a mandatory full e2e for every -rc.N tag via the new reusable e2e-tag.yaml lane. Alpha/beta (and any published tag) get the same validation as a manual button (workflow_dispatch).
  2. Optionally, a maintainer presses the changelog-rc.yaml button: the release changelog is generated from the rc tag (reusable changelog-generate.yaml) and committed to the rc staging branch, ready for review long before promotion.
  3. promote-rc.yaml dispatch is gated on green rc e2e (evidence: the rc's tags.yaml run containing a successful rc-e2e job, or a successful manual e2e-tag dispatch for the tag; skip_e2e_gate input for emergencies, always surfaced in the PR body as ✅/⚠️). Promote stages the release, picks up the rc-time changelog (or generates one), and opens the website docs PR on cozystack/website from the staging branch — parked with an explicit DO-NOT-MERGE-until-finalize contract.
  4. The promote PR no longer gets full-e2e automatically — 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.
  5. Finalize performs the irreversible side effects (tag, publish, retag, chart) — and with persist-credentials: false its tag push now actually authenticates as the CI app, so the tags.yaml backstops (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 persisted GITHUB_TOKEN http.extraheader silently defeats the app token injected via git remote set-url, and GITHUB_TOKEN pushes create no workflow runs.

Everything is pinned by executable contract tests (hack/promote-gate-contract.bats, extended hack/release-changelog-contract.bats / release-changelog-behaviour.bats — 48 tests total, including behaviour tests that execute the new hack/parse-rc-tag.sh and hack/select-changelog-source.sh for real), and docs/release.md is rewritten to describe the new lifecycle end-to-end.

Commit map (review commit-by-commit)

  • 75b5192f1dd308644c + 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.
  • 5aec70b3ae2e-tag.yaml: reusable published-tag e2e lane (prerelease → staging branch, stable → tag; draft releases fail fast; dispatch-only SSH debug breakpoint; tag-suffixed artifacts).
  • 025564d96tags.yaml::rc-e2e: mandatory full e2e for freshly cut rc tags (also on release_exists re-runs); fixes the cut-prerelease.yaml comment that falsely claimed e2e already existed.
  • 2dfbeea77changelog-generate.yaml (reusable core) + changelog-rc.yaml (rc-time button committing to the staging branch); promote-rc gains a top-level parse job 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_gate override + removal of the automatic full-e2e label (pull-requests.yaml release-path e2e now requires an explicit label; a post-open label now actually starts the run).
  • 936190d4bwebsite-docs job 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 before release-next; tags.yaml::update-website-docs stays as the backstop.
  • 01e1e7188 — the persist-credentials: false finalize fix (v1.6.0 root cause).
  • 717c8860fdocs/release.md coherence pass over the whole new lifecycle.

Dependency

The promote-time website docs job needs cozystack/website#631 (FETCH_REF + fail-loud fetch). Until that merges, the job's guard detects the missing support and degrades to a ⚠️ line in the promote PR body — promotion is unaffected and the tag-time backstop backfills, so this PR is safe to merge first.

Merge 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.yaml dispatch against an existing published rc tag before the gate is relied on for a real promotion.

Release note

NONE

Summary by CodeRabbit

  • New Features
    • Added reusable changelog generation with validation, “present” signaling, and safe artifact upload behavior (including RC-time support).
    • Added an E2E release-tag workflow with tag parsing, deterministic sandboxing, and comprehensive report collection.
    • Improved RC promotion with a stricter parse front-door, verified E2E green gate (optional bypass), and earlier parallel changelog/asset staging.
  • Documentation
    • Refreshed release-process docs to match the updated RC-to-stable, changelog, and website-docs timing rules.
  • Bug Fixes
    • Improved PR finalization to source release bodies from verified changelogs and warn/fallback on missing or empty content.
    • Tightened label gating so E2E runs only under the intended conditions.

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

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

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

Changes

Release automation

Layer / File(s) Summary
Changelog generation and validation
.github/workflows/changelog-generate.yaml, .github/workflows/changelog-rc.yaml, hack/*changelog*.sh, hack/parse-rc-tag.sh, docs/agents/changelog.md, .gitignore
Adds reusable and RC-time changelog generation with strict RC parsing, source selection, structural validation, Copilot fallback, artifact upload, preservation, and dispatch-ref tooling overlays.
RC promotion and promote PR integration
.github/workflows/promote-rc.yaml
Adds RC E2E evidence verification and bypass handling, changelog preservation and generation, website documentation updates, and promote PR status/body handling.
Release-tag E2E and trigger gating
.github/workflows/e2e-tag.yaml, .github/workflows/tags.yaml, .github/workflows/pull-requests.yaml, .github/workflows/cut-prerelease.yaml
Adds reusable/manual release-tag E2E validation, RC tag integration, label-gated PR E2E execution, and changelog backstop selection.
Release publication and authentication
.github/workflows/pull-requests-release.yaml
Sources published release bodies from validated changelogs, removes docs-only trigger filtering, and prevents persisted checkout credentials from affecting later pushes.
Workflow contracts and behavior coverage
hack/*bats, hack/testdata/*
Adds structural and runtime coverage for parsing, source selection, preservation, workflow conditions, E2E gates, artifact compatibility, and release-body behavior.
Release process documentation
docs/release.md, docs/agents/releasing.md
Documents RC E2E gates, promotion phases, changelog paths, website-docs timing, finalization behavior, and recovery procedures.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related issues

Possibly related PRs

Suggested labels: kind/feature, area/testing

Suggested reviewers: kvaps

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main shift to RC-time release validation and mentions the key affected areas.
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/release-checks-at-rc

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.

@github-actions github-actions Bot added area/release Issues or PRs related to release tooling (changelog, backport, release pipeline) size/XXL This PR changes 1000+ lines, ignoring generated files labels Jul 24, 2026
@myasnikovdaniil
myasnikovdaniil marked this pull request as ready for review July 27, 2026 06:52
@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.

@dosubot dosubot Bot added area/ci Issues or PRs related to CI workflows, GitHub Actions, automation release Releasing a new Cozystack version labels Jul 27, 2026
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>

@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: 5

🧹 Nitpick comments (9)
hack/changelog-preserve.sh (1)

46-50: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

The [ ! -s "$OUTFILE" ] && short-circuit makes a pre-populated <outfile> silently report as "preserved".

If $OUTFILE already exists non-empty, git show never 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 win

Evidence B walks every retained successful e2e-tag.yaml dispatch run with a jobs API call each — inside a 5-minute job that fails closed.

github.paginate over all successful dispatch runs, then one paginated listJobsForWorkflowRun per run sequentially, scales with 90 days of retention. A slow or rate-limited scan trips the timeout-minutes: 5 on parse (Line 63) and blocks a promotion that actually has green evidence. Consider bounding the scan (cap the number of runs examined, or filter by created: >=<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 value

Hardcoded workflow line number will rot.

pull-requests-release.yaml:163 drifts 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 value

Prefer bats skip over return 0 for 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 value

Stale header note: the docs job no longer runs only at tag push.

This PR moves the cozystack/website docs PR to promote time (promote-rc.yaml::website-docs), and hack/promote-gate-contract.bats now 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 value

Glob will drift past v1.9.

v1.[3-9]*.md silently stops covering new minors at v1.10.0 (and any v2.x). The checked -ge 10 anti-vacuum guard won't catch that, since older files keep the count up. Consider globbing all v*.md and filtering out the pre-v1.3 era 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 value

Consider grep for consistency/portability.

hack/release-changelog-contract.bats defines the same helper as grep -vE '^[[:space:]]*#', and every other assertion here could use POSIX grep. Depending on rg makes 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 -n call sites would need grep -cF/grep -qF/grep -n equivalents.

🤖 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 win

Consider surfacing (warn-only) the stronger hack/validate-changelog.sh checks before publishing the changelog as the public release body.

This only checks the file is non-empty/non-whitespace before using it as body for the published release. hack/validate-changelog.sh (already used by tags.yaml's backstop and presumably by promote-rc.yaml) additionally checks the version header, the Full Changelog compare 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 | 🔵 Trivial

Verify required-status-check behavior now that labeled is a trigger for every label, not just full-e2e.

The plan/resolve_assets guards correctly no-op this workflow's jobs for any labeled event other than full-e2e, and the e2e gating 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: since labeled now fires this workflow on every label addition to a PR (e.g. someone adding an unrelated triage label), and the guarded jobs then report as skipped for 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 a skipped run 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

📥 Commits

Reviewing files that changed from the base of the PR and between 436743e and 717c886.

📒 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
  • .gitignore
  • docs/agents/changelog.md
  • docs/agents/releasing.md
  • docs/release.md
  • hack/changelog-preserve.sh
  • hack/parse-rc-tag.sh
  • hack/promote-gate-contract.bats
  • hack/release-changelog-behaviour.bats
  • hack/release-changelog-contract.bats
  • hack/select-changelog-source.sh
  • hack/testdata/needs-semantics.yaml
  • hack/validate-changelog.sh

Comment thread .github/workflows/tags.yaml
Comment thread docs/agents/changelog.md
Comment thread docs/release.md
- 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.

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.

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

Suggested change
- 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

Comment thread hack/release-changelog-behaviour.bats Outdated
Comment thread hack/testdata/needs-semantics.yaml Outdated
@myasnikovdaniil myasnikovdaniil removed the release Releasing a new Cozystack version label Jul 27, 2026
…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>

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

📥 Commits

Reviewing files that changed from the base of the PR and between ee83826 and 1eded27.

📒 Files selected for processing (7)
  • .github/workflows/changelog-rc.yaml
  • .github/workflows/e2e-tag.yaml
  • .github/workflows/tags.yaml
  • docs/agents/changelog.md
  • hack/promote-gate-contract.bats
  • hack/release-changelog-behaviour.bats
  • hack/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

Comment on lines +266 to +271
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'

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.

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

Suggested change
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.

@myasnikovdaniil
myasnikovdaniil merged commit cc44f6c into main Jul 29, 2026
107 of 111 checks passed
@myasnikovdaniil
myasnikovdaniil deleted the ci/release-checks-at-rc branch July 29, 2026 12:14
@myasnikovdaniil myasnikovdaniil added backport Should change be backported on previous release backport-previous Backport target — previous release line labels Aug 18, 2026
@github-actions

Copy link
Copy Markdown

Git push to origin failed for release-1.5 with exitcode 1

@github-actions

Copy link
Copy Markdown

Git push to origin failed for release-1.6 with exitcode 1

1 similar comment
@github-actions

Copy link
Copy Markdown

Git push to origin failed for release-1.6 with exitcode 1

@github-actions

Copy link
Copy Markdown

Git push to origin failed for release-1.5 with exitcode 1

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) backport Should change be backported on previous release backport-previous Backport target — previous release line size/XXL This PR changes 1000+ lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants