Skip to content

feat(release): immutable tags & rc→stable promotion (#2677) - #3017

Merged
myasnikovdaniil merged 46 commits into
mainfrom
feat/release-promote-flow
Jul 7, 2026
Merged

myasnikovdaniil merged 46 commits into
mainfrom
feat/release-promote-flow

Conversation

@myasnikovdaniil

@myasnikovdaniil myasnikovdaniil commented Jun 23, 2026 •

Copy link
Copy Markdown
Contributor

What this PR does

Implements the immutable-tag + rc-promotion flow from #2677. A stable release becomes a renamed release-candidate: the bytes shipped as vX.Y.Z are bit-for-bit the bytes built and e2e-tested as vX.Y.Z-rc.N. No tag is ever force-moved, and stable is never rebuilt — it is promoted by retagging the rc's existing images.

Layered onto the build matrix from #2937/#2983 (this PR is stacked on refactor/build-matrix-2937 and must merge after it).

The five force-retag sites, removed

Site Before After
tags.yaml api/apps/v1alpha1 tag git tag -f / push -f write-once (create-if-absent, fail if it would move)
tags.yaml release-X.Y.Z branch git branch -f / push -f compare-before-force (no-op if unchanged; staging branch only)
pull-requests-release.yaml stable tag git tag -f / push -f write-once at the PR merge commit (force impossible by construction)
pull-requests-release.yaml maintenance branch updateRef force:true fast-forward-only
auto-release.yaml patch tags delete-recreate (cron) workflow deleted — stable only via explicit promote

Version decoupling (the enabler)

The operator baked its version into the image at build time, so an rc image self-reported the rc string — blocking retag-promotion. It now reads COZYSTACK_VERSION from the environment (threaded via cozystackOperator.platformVersion → Deployment env, stamped by make manifests), falling back to the build-time value when unset. The same image bits can report any release name. The only runtime reader is the telemetry metric cozy_cluster_info{cozystack_version=...}.

Promotion flow

promote-rc.yaml (workflow_dispatch, rc_tag=vX.Y.Z-rc.N):

  1. Validate the rc release exists and the stable tag does not.
  2. hack/promote-retag.sh reads the rc's digest-pinned image refs from packages/*/*/values.yaml and skopeo copys each — by digest — to :vX.Y.Z and :latest, verifying with skopeo inspect.
  3. Rewrite the cosmetic -rc.N substring in vendored tags to the stable version (the @sha256 wins regardless), restamp the version-stamped assets (operator manifests, cozypkg, openapi; the heavy Talos assets are copied verbatim from the rc draft), open a release-X.Y.Z PR.
  4. Merging the PR reuses the existing release-PR e2e and pull-requests-release.yaml finalize to cut the write-once stable tag at the merge commit and publish the release. Squash is disallowed (the tag needs a real merge commit).

nightly.yaml cuts write-once *-nightly.<date> tags (gated by NIGHTLY_ENABLED); retention.yaml keeps the newest 14 per line (dry-run by default).

Validation

Local end-to-end — the core claim is proven, not asserted. The whole design rests on "copy-by-digest to a new tag preserves the digest, so stable == the e2e-tested rc, bit-for-bit." This was exercised against a real registry (ttl.sh) with real skopeo:

  • Pushed a multi-arch image under a :v1.4.0-rc.2 tag, built a temp tree exercising all three values.yaml digest shapes (single image: string, split repository/tag/digest map, and the platformSourceRef OCI artifact), then ran the actual hack/promote-retag.sh v1.4.0.
  • Result: :v1.4.0-rc.2, :v1.4.0, and :latest all resolve to the identical digest (sha256:fd8d9aa6…), across all 17 platform manifests. The script's own skopeo inspect post-check passed, and an independent re-inspection confirmed it.

Local validation caught (and this PR fixes) two bugs that only surface against a real registry — never in static lint:

  1. skopeo copy --multi-arch all --all — mutually exclusive flags, fatal error; every retag would have failed on CI. Now --multi-arch all.
  2. The same digest was retagged twice when an image appeared in two value shapes; the ref set is now deduped on the canonical repo@digest.

Other local checks: go test ./pkg/version + go build/vet; helm template renders COZYSTACK_VERSION in all 3 operator variants (omitted when unset); make manifests stamps the version into the install assets; shellcheck clean on promote-retag.sh; actionlint clean and act -l resolves the job graph on all workflows; the rc-tag parser and nightly version-math unit-tested (accept/reject + minor-vs-patch bumps).

Still CI-only (cannot be exercised offline): the full workflow runtime — github-script API calls, OCIR auth, the rc-release / staging-branch preconditions, the nightly base_ref push mechanics, and self-hosted runners. The logic inside the steps is unit-tested; the orchestration is not. Treat it as unproven until the workflows run.

Out of scope / required follow-up (needs repo admin — not doable from a PR)

  • Tag-protection rules: v* and api/apps/v1alpha1/* create-only by the CI app, no delete/update; "limit branches/tags updated in a single push" = 1.
  • Enable NIGHTLY_ENABLED (and RETENTION_APPLY when ready) repo variables.
  • Registry-side pruning of *-nightly.* image tags (no OCIR delete parity yet — tracked TODO in retention.yaml).

Release note

NONE

Summary by CodeRabbit

  • New Features
    • Added nightly publishing (mirror-by-digest, disk build, e2e validation) and nightly retention pruning.
    • Added RC-to-stable promotion via digest retagging (no rebuild).
    • Operator/installer and console now expose version/platform metadata, including runtime override via COZYSTACK_VERSION.
  • Bug Fixes
    • Enforced write-once tag behavior and fast-forward-only maintenance updates across release workflows.
    • Made nightly mirroring/selection and retention pruning more selective and safer.
  • Tests
    • Added Bats coverage for nightly mirroring and RC retagging, plus new Helm/unit checks.
  • Documentation
    • Updated the release model to center RC promotion and tag immutability.

@coderabbitai

coderabbitai Bot commented Jun 23, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR replaces force-updated release handling with write-once tag publication, adds rc promotion and nightly artifact workflows, introduces nightly GHCR retention, and propagates runtime version data through installer, dashboard, and version code.

Changes

Release Pipeline Overhaul

Layer / File(s) Summary
Runtime version propagation
pkg/version/version.go, pkg/version/version_test.go, packages/core/installer/values.yaml, packages/core/installer/templates/cozystack-operator.yaml, packages/core/installer/tests/cozystack_version_test.yaml, packages/core/installer/Makefile, Makefile, packages/system/dashboard/images/console/apps/console/src/lib/config.ts, packages/system/dashboard/images/console/apps/console/src/App.tsx, packages/system/dashboard/templates/configmap.yaml
COZYSTACK_VERSION is resolved at runtime, threaded through installer rendering, exposed to the operator and dashboard config, and covered by template and package tests.
Write-once release tagging
.github/workflows/pull-requests-release.yaml, .github/workflows/tags.yaml
pull-requests-release.yaml changes tag publication and maintenance-branch updates to compare before writing, avoid force-moving existing tags, and gate stable-only steps with the new release-existence and stability outputs.
RC promotion workflow
hack/promote-retag.sh, hack/promote-retag_test.bats, .github/workflows/promote-rc.yaml
hack/promote-retag.sh retags digest-pinned images from RC to stable tags, and promote-rc.yaml validates the RC input, retags images, rewrites vendored values, restamps assets, publishes a draft stable release, and opens the promotion PR.
Nightly mirror, build, and e2e
hack/nightly-mirror.sh, hack/nightly-mirror_test.bats, .github/workflows/nightly.yaml
nightly.yaml adds mirror, build-disk, and e2e jobs for nightly artifacts, hack/nightly-mirror.sh mirrors digest-pinned images and rewrites baked trees, and its Bats tests cover the selection and failure paths.
Nightly retention and release docs
.github/workflows/retention.yaml, docs/release.md
retention.yaml prunes older dated-nightly GHCR package versions, and docs/release.md updates the release model, nightly-build description, tag immutability notes, and workflow references.

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

Possibly related issues

Possibly related PRs

Suggested labels: area/build

Suggested reviewers: kvaps, lllamnyp, lexfrei, androndo, sircthulhu, IvanHunters

🚥 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
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly captures the main release-flow change: immutable tags and rc-to-stable promotion.
✨ 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 feat/release-promote-flow

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.

@github-actions github-actions Bot added area/release Issues or PRs related to release tooling (changelog, backport, release pipeline) kind/feature Categorizes issue or PR as related to a new feature size/XL This PR changes 500-999 lines, ignoring generated files labels Jun 23, 2026
@myasnikovdaniil
myasnikovdaniil marked this pull request as ready for review June 23, 2026 11:38
@dosubot dosubot Bot added the area/ci Issues or PRs related to CI workflows, GitHub Actions, automation label Jun 23, 2026
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a robust, immutable release promotion mechanism for Cozystack. By decoupling the version reporting from the build-time binary and enabling runtime overrides via environment variables, the system can now promote release candidates to stable releases through atomic retagging of immutable image digests. This ensures that the exact bits validated during the release candidate phase are those deployed in the stable release, significantly improving reliability and auditability of the release process.

Highlights

  • Immutable Release Promotion: Implements a new promotion flow where stable releases are created by retagging existing, e2e-tested release-candidate images by digest, ensuring bit-for-bit parity.
  • Version Decoupling: The operator now supports reading the version from the COZYSTACK_VERSION environment variable, allowing the same image to report different release names without requiring a rebuild.
  • Automated Retagging Script: Added a new script, hack/promote-retag.sh, which automates the process of copying digest-pinned images from release candidates to stable tags.
  • Build Matrix Integration: Updated Makefile and Helm templates to inject the platform version as deploy-time data, enabling consistent version reporting across different deployment variants.
New Features

🧠 You can now enable Memory (public preview) to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Ignored Files
  • Ignored by pattern: .github/workflows/** (6)
    • .github/workflows/auto-release.yaml
    • .github/workflows/nightly-rc.yaml
    • .github/workflows/promote-rc.yaml
    • .github/workflows/pull-requests-release.yaml
    • .github/workflows/retention.yaml
    • .github/workflows/tags.yaml
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment Gemini (@gemini-code-assist) Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on Gemini (@gemini-code-assist) comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces a mechanism to promote release-candidate images to stable by retagging them instead of rebuilding. It adds a promotion script 'hack/promote-retag.sh' that copies images by digest using 'skopeo', updates the 'Makefile' to pass the platform version as deploy-time data, and updates the operator deployment template and Go code to support overriding the version via the 'COZYSTACK_VERSION' environment variable. Feedback on these changes highlights several issues: the 'ref_repo' function in the promotion script incorrectly strips registry ports when no tag is present; piping to the 'while' loop runs it in a subshell, which can prevent 'exit 1' from terminating the parent script; in the 'Makefile', an empty 'COZYSTACK_VERSION' evaluates to 'v' and incorrectly overrides the default version; and the script should explicitly verify that the mikefarah version of 'yq' is installed.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread hack/promote-retag.sh Outdated
}

# Split a "<repo>[:<tag>]@sha256:<digest>" ref into repo and digest.
ref_repo() { r="${1%@*}"; printf '%s' "${r%:*}"; } # strip @digest, then :tag

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.

critical

The current implementation of ref_repo uses ${r%:*} to strip the tag. However, if the repository reference contains a port (e.g., localhost:5000/cozystack/operator) but no tag (which is the case for shape 2 and shape 3 outputs from collect_refs), ${r%:*} will strip the port and the rest of the path, returning just localhost. This will break any deployment using a registry with a port.

We can fix this robustly by only stripping the tag from the last component of the path.

Suggested change
ref_repo() { r="${1%@*}"; printf '%s' "${r%:*}"; } # strip @digest, then :tag
ref_repo() {
r="${1%@*}"
img="${r##*/}"
if [ "$r" = "$img" ]; then
printf '%s' "${img%:*}"
else
printf '%s/%s' "${r%/*}" "${img%:*}"
fi
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch — fixed in 76c0369. ref_repo now strips the :tag from the last path component only, so a registry host that carries a :port (e.g. localhost:5000/cozystack/operator) on a no-tag ref is preserved instead of being truncated to the host. Verified against all three collect_refs shapes plus port/tag combinations; the top-of-file no-port caveat is removed since the split is now port-safe.

Comment thread hack/promote-retag.sh
Comment on lines +74 to +89
echo "$refs" | while IFS= read -r ref; do
[ -n "$ref" ] || continue
repo="${ref%@*}"
digest="${ref##*@}"
echo "▸ ${repo} ${digest}"
copy "$ref" "${repo}:${STABLE}"
copy "$ref" "${repo}:latest"
# Verify the stable tag now resolves to the exact rc digest (skip in dry-run).
if [ "$DRY_RUN" -eq 0 ]; then
got="$(skopeo inspect --format '{{.Digest}}' "docker://${repo}:${STABLE}" 2>/dev/null || echo '')"
if [ "$got" != "$digest" ]; then
echo "::error::${repo}:${STABLE} resolved to '${got}', expected '${digest}'" >&2
exit 1
fi
fi
done

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.

medium

Piping echo "$refs" to the while loop causes the loop to run in a subshell. If exit 1 is triggered inside the loop (e.g., when verification fails), it exits the subshell, but depending on the shell implementation and environment, it might not reliably terminate the parent script. Using a here-document avoids the subshell entirely and ensures consistent error propagation.

Suggested change
echo "$refs" | while IFS= read -r ref; do
[ -n "$ref" ] || continue
repo="${ref%@*}"
digest="${ref##*@}"
echo "▸ ${repo} ${digest}"
copy "$ref" "${repo}:${STABLE}"
copy "$ref" "${repo}:latest"
# Verify the stable tag now resolves to the exact rc digest (skip in dry-run).
if [ "$DRY_RUN" -eq 0 ]; then
got="$(skopeo inspect --format '{{.Digest}}' "docker://${repo}:${STABLE}" 2>/dev/null || echo '')"
if [ "$got" != "$digest" ]; then
echo "::error::${repo}:${STABLE} resolved to '${got}', expected '${digest}'" >&2
exit 1
fi
fi
done
while IFS= read -r ref; do
[ -n "$ref" ] || continue
repo="${ref%@*}"
digest="${ref##*}"
echo "▸ ${repo} ${digest}"
copy "$ref" "${repo}:${STABLE}"
copy "$ref" "${repo}:latest"
# Verify the stable tag now resolves to the exact rc digest (skip in dry-run).
if [ "$DRY_RUN" -eq 0 ]; then
got="$(skopeo inspect --format '{{.Digest}}' "docker://${repo}:${STABLE}" 2>/dev/null || echo '')"
if [ "$got" != "$digest" ]; then
echo "::error::${repo}:${STABLE} resolved to '${got}', expected '${digest}'" >&2
exit 1
fi
fi
done <<EOF
$refs
EOF

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Leaving as-is here: the script runs under set -eu, and echo "$refs" | while … is a standalone pipeline (not part of an if/while/&& condition), so when the subshell hits exit 1 the pipeline returns non-zero and set -e terminates the parent script. I confirmed this by running the exact pattern under set -e — the echo after the loop does not execute. The loop body doesn't need to persist any variables back to the parent shell, so the subshell is harmless and a here-doc rewrite would add no behavioural change.

Comment thread Makefile Outdated
# Talos variant (default)
helm template installer packages/core/installer -n cozy-system \
--set bareNamespace=true \
--set cozystackOperator.platformVersion=v$(COZYSTACK_VERSION) \

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.

medium

If COZYSTACK_VERSION is empty or unset, v$(COZYSTACK_VERSION) evaluates to v. This sets platformVersion to v, which is invalid and overrides the default baked-in version. We should guard against empty COZYSTACK_VERSION using GNU Make's $(if ...) function.

		--set cozystackOperator.platformVersion=$(if $(COZYSTACK_VERSION),v$(COZYSTACK_VERSION),) \

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 69448ec. Wrapped the --set in $(if $(COZYSTACK_VERSION),v$(COZYSTACK_VERSION),) so an empty COZYSTACK_VERSION passes an empty value instead of a bare v. With an empty value the installer template's {{- if .Values.cozystackOperator.platformVersion }} is false, so no COZYSTACK_VERSION env is rendered and the operator keeps its baked-in version. Applied to all three variant renders (lines 56/64/73).

Comment thread Makefile Outdated
helm template installer packages/core/installer -n cozy-system \
--set bareNamespace=true \
--set cozystackOperator.variant=generic \
--set cozystackOperator.platformVersion=v$(COZYSTACK_VERSION) \

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.

medium

If COZYSTACK_VERSION is empty or unset, v$(COZYSTACK_VERSION) evaluates to v. This sets platformVersion to v, which is invalid and overrides the default baked-in version. We should guard against empty COZYSTACK_VERSION using GNU Make's $(if ...) function.

		--set cozystackOperator.platformVersion=$(if $(COZYSTACK_VERSION),v$(COZYSTACK_VERSION),) \

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 69448ec — same $(if ...) guard applied to this generic-variant render; see the reply on line 56 for the rationale.

Comment thread Makefile Outdated
helm template installer packages/core/installer -n cozy-system \
--set bareNamespace=true \
--set cozystackOperator.variant=hosted \
--set cozystackOperator.platformVersion=v$(COZYSTACK_VERSION) \

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.

medium

If COZYSTACK_VERSION is empty or unset, v$(COZYSTACK_VERSION) evaluates to v. This sets platformVersion to v, which is invalid and overrides the default baked-in version. We should guard against empty COZYSTACK_VERSION using GNU Make's $(if ...) function.

		--set cozystackOperator.platformVersion=$(if $(COZYSTACK_VERSION),v$(COZYSTACK_VERSION),) \

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 69448ec — same $(if ...) guard applied to this hosted-variant render; see the reply on line 56 for the rationale.

Comment thread hack/promote-retag.sh
DRY_RUN=0
[ "${2:-}" = "--dry-run" ] && DRY_RUN=1

command -v yq >/dev/null || { echo "yq (mikefarah) is required" >&2; exit 1; }

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.

low

command -v yq only checks if any binary named yq is installed. If the user has python-yq or another variant installed, the script will fail with syntax errors because it uses mikefarah-specific syntax. It is safer to verify that the installed yq is indeed the mikefarah version, similar to the check in the Makefile.

Suggested change
command -v yq >/dev/null || { echo "yq (mikefarah) is required" >&2; exit 1; }
yq_version=$(yq --version 2>&1 || true)
case "$yq_version" in
*mikefarah*) ;;
*) echo "yq (mikefarah) is required" >&2; exit 1 ;;
esac

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 2f92149. Added a yq --version | grep -q mikefarah check after the command -v yq probe so python-yq and other variants are rejected — the collect_refs queries use mikefarah-specific syntax. This mirrors the existing build-deps check in the Makefile.

@myasnikovdaniil myasnikovdaniil self-assigned this Jun 23, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

NOT LGTM — the core promotion script aborts against the real package tree, the nightly job over-selects branches, and several release artifacts/docs drift from the new flow.

Business context: #2677 wants every git tag write-once and stable vX.Y.Z created only by promoting an already-built, e2e-tested rc — retagging the rc's images by digest so stable is bit-for-bit the rc, never a rebuild. This PR removes the five force-retag sites, adds promote-rc.yaml + hack/promote-retag.sh, decouples the operator's reported version from the image (COZYSTACK_VERSION env), and adds dark-by-default nightly + retention workflows. It is stacked on #2983 (refactor/build-matrix-2937) and must merge after it.

The version-decoupling half is solid: the pkg/version env override plus the Makefile platformVersion threading render correctly in all three operator variants with and without the version (I rendered all six combinations). The blockers are in the promotion script, the nightly job, and asset/doc consistency.

Blockers

B1 — hack/promote-retag.sh collects third-party and malformed image refs; promotion aborts before completing, or tries to push into registries it does not own.
File: hack/promote-retag.sh:37-47 (collect_refs), :53-61 (ref_repo), :87-102.
Evidence: the shape-1 query selects every string scalar containing @sha256:, and shape-2 every {repository,digest} map — not only releasable cozystack images. Against the real tree this yields, among others:

  • packages/system/postgres-operator/values.yaml:17-19 → docker.io/clastix/kubectl@sha256:… (third-party); the script runs skopeo copy … docker://docker.io/clastix/kubectl:vX.Y.Z and :latest → push to a registry the CI app cannot write.
  • packages/system/ingress-nginx/values.yaml:19 → ghcr.io/kvaps/…@sha256:… (foreign namespace) → same push failure.
  • packages/system/kamaji/values.yaml:39 → the CLI-arg string --migrate-image=ghcr.io/…@sha256:…; ref_repo returns the malformed repo --migrate-image=ghcr.io/…/kamaji, so skopeo copy docker://--migrate-image=… is an invalid reference.
  • packages/system/kubeovn/values.yaml:68, packages/system/keycloak-operator/values.yaml:5, packages/system/kilo/values.yaml:4 → bare tag: scalars like v1.15.10@sha256:… / latest@sha256:…; ref_repo yields a bare invalid repo (v1.15.10, latest).
    Under set -eu the first failing skopeo aborts the whole script, so stable promotion — the central feature of this PR — cannot complete. The local validation in the PR description used a synthetic single-image tree, so it never exercised the real mixed-registry values.
    Impact: promotion is broken in practice; no stable can be cut by this flow.
    Fix: restrict collection to cozystack-owned, fully-qualified <registry>/<repo>@<digest> refs (anchor the registry host and namespace; exclude tag:-only scalars and arg strings), or drive promotion from an explicit produced-images manifest instead of scraping all values.yaml. Add a test over the real package set.

B2 — nightly-rc.yaml target discovery matches release-X.Y.Z[-suffix] staging branches, not just release-X.Y maintenance branches.
File: .github/workflows/nightly-rc.yaml:57-59.
Evidence: the case glob origin/release-[0-9]*.[0-9]* matches origin/release-1.4.0, origin/release-1.4.0-rc.2, and origin/release-1.5.0-nightly.20260624 — all staging branches that tags.yaml:223-241 ("Create release branch", release-${GITHUB_REF#refs/tags/v}) creates and pushes for every rc/nightly/stable tag and never deletes (retention.yaml prunes nightly tags+releases, not branches). The step comment states the intent is "main + every release-X.Y maintenance branch"; the code selects more. For each spurious target the job cuts a v…-nightly.<date> tag at that branch's HEAD and pushes it, which then triggers further builds.
Impact: once NIGHTLY_RC_ENABLED is set, the nightly job emits bogus tags and spurious CI runs. Gated dark today, so not release-breaking yet, but it is a wrong-artifact bug in new code.
Fix: anchor the match to exactly two numeric components, e.g. select only refs matching ^origin/release-[0-9]+\.[0-9]+$.

B3 — the stable restamp leaves openapi.json with an rc-derived version.
File: .github/workflows/promote-rc.yaml:169-173, Makefile:87-89.
Evidence: the restamp runs make manifests assets-cozypkg openapi-json COZYSTACK_VERSION="${STABLE_VERSION}". manifests (operator env), assets-cozypkg (-ldflags … Version=v$(COZYSTACK_VERSION)) and check-readiness all honor COZYSTACK_VERSION, but openapi-json sets VERSION=$(shell git describe --tags --always …). At promote time the checkout is the rc staging branch and the stable tag does not exist yet (it is created only at PR merge), so git describe resolves to vX.Y.Z-rc.N-… and the stable release's openapi.json info.version reports the rc version. That asset is then uploaded via make upload_assets.
Impact: a published stable asset ships rc version metadata, defeating the restamp step's stated purpose.
Fix: make openapi-json honor COZYSTACK_VERSION (fall back to git describe only when it is unset), mirroring the other version-stamped targets.

B4 — docs/release.md still documents the removed force-retag flow as the current, load-bearing mechanism.
File: docs/release.md:83, 177, 239-241, 531-546 (not touched by this PR).
Evidence: the PR removes the force sites and adds the promote flow, but docs/release.md still says the finalize step "Moves the tag … by force-pushing a tag" (:83, :177), "Force-move the tag … Intentional and load-bearing for the digest-bake flow" (:239), maintenance branch "force-updated if newer" (:240), and keeps the entire "Force-retagging" section (:531-546) framing #2677 as an "open RFC" with only "Stage 1" landed. None of that holds after this PR.
Impact: the canonical release runbook now actively misdescribes how releases are cut; a maintainer following it would push a stable tag by hand (which the new flow and intended tag-protection are designed to reject) and would not learn about promote-rc.yaml.
Fix: update docs/release.md (and docs/agents/releasing.md where it references the flow) to the write-once + promote model implemented here.

Non-blocking follow-ups

  1. Nightly tags vX.Y.Z-nightly.<date> DO match the v*.*.* filter in tags.yaml/release-e2e.yaml (the * segments absorb the -nightly.<date> suffix), so a nightly triggers the full stable-prep machinery — including a write-once api/apps/v1alpha1/vX.Y.Z-nightly.<date> Go-module tag (tags.yaml:153-172) and an auto-opened release PR per nightly. That contradicts #2677 goal 3 (rc/nightly must not get Go-module tags) and would permanently pollute the module proxy — the exact failure mode #2677 exists to prevent. Scope the api-submodule tag and release-PR steps to stable tags only.
  2. promote-rc.yaml:147-148 rewrites the rc→stable substring with sed -i "s/${RC_VERSION}/${STABLE_VERSION}/g"; the . characters in RC_VERSION are unescaped regex metacharacters. Harmless for the current literal but imprecise — escape or anchor.
  3. packages/core/installer ships no helm-unittest covering the new COZYSTACK_VERSION env rendering (3 variants × version set/unset). Add coverage so the env-block restructure cannot silently regress.
  4. promote-rc.yaml:149-153 ("Prepare stable branch") pushes the stable branch without -f; a re-run after a partial failure where the remote branch already diverged would fail the push. Minor idempotency gap (the concurrency group mitigates concurrent runs).

This PR is stacked on #2983 (refactor/build-matrix-2937) and should merge after it.

@github-actions github-actions Bot added size/XXL This PR changes 1000+ lines, ignoring generated files and removed size/XL This PR changes 500-999 lines, ignoring generated files labels Jun 25, 2026
@myasnikovdaniil

Copy link
Copy Markdown
Contributor Author

Aleksei Sviridkin (@lexfrei) addressed across 3c184e9…aad2a1a:

Blockers

  • B1 (promote scrapes third-party refs) — collect_refs is now filtered to cozystack-owned images (repo under $REGISTRY/, default ghcr.io/cozystack/cozystack, matching common-envs.mk); third-party images, bare upstream tags, and the --migrate-image=… arg string are dropped, so the first skopeo copy no longer aborts the promotion. Added hack/promote-retag_test.bats — a --dry-run over the real packages/ tree asserts zero non-cozystack refs (auto-runs via make unit-tests).
  • B3 (openapi.json rc version) — the openapi-json target now honors COZYSTACK_VERSION, so the promote restamp produces a stable-versioned openapi.json instead of the rc version from git describe.
  • B2 (nightly glob) — target selection is anchored to ^release-[0-9]+\.[0-9]+$; the per-tag staging branches (release-1.4.0, -rc.2, -nightly.<date>) no longer match.
  • api-tag / release-PR scope — tags.yaml gates the write-once api/apps/v1alpha1 Go-module tag and the auto-opened release PR on a new is_stable output, so an rc/beta/alpha (is_stable=false) or a nightly (is_stable unset) never pollutes the module proxy or opens a release PR (RFC: immutable release tags & rc-promotion flow #2677 goal 3).
  • B4 (docs) — docs/release.md rewritten to the write-once + rc-promotion model: write-once tag at the PR merge commit (never force-moved), fast-forward-only maintenance branch, stable patches only via promotion (nightly-rc.yaml + promote-rc.yaml + retention.yaml), api submodule tag stable-only. The Automated patch tag and Force-retagging sections were replaced (the latter is now Tag immutability) and the See-also list refreshed.

Non-blocking

  • The promote sed now escapes regex metacharacters in the rc version (the dots) so the match is literal.
  • The stable staging branch uses git checkout -B for retry-safety.
  • Added a helm-unittest for the installer COZYSTACK_VERSION env rendering (talos/hosted × platformVersion set/unset).

One I did not fold in: nightly tags still hard-fail tags.yaml's Parse-tag (the regex rejects -nightly) even though nightly-rc.yaml intends them to drive the build/e2e path. The is_stable gate makes that safe (no api-tag/release-PR), but properly flowing nightly through build/e2e touches release-creation semantics — flagging it as a follow-up rather than bundling it here.

myasnikovdaniil added a commit that referenced this pull request Jun 25, 2026
Re-sync the release-promotion PR (#3017) onto the #2983 base, which now contains
main. The conflicts were release-pipeline files, resolved keeping #3017's
write-once + promotion logic and grafting main's hardening:

- tags.yaml: keep #3017's is_stable gate on the api-submodule tag and the
  auto-opened release PR; take main's SHA-pinned actions.
- auto-release.yaml: kept deleted (#3017 removes the nightly auto-bump; main's
  hardening of a soon-deleted file is moot).
- installer/values.yaml: keep #3017's platformVersion field + comment; take
  main's v1.5.0 operator image.

actionlint clean; installer + promote-retag unit tests pass.

Signed-off-by: Myasnikov Daniil <[email protected]>
@myasnikovdaniil
myasnikovdaniil force-pushed the feat/release-promote-flow branch from e074864 to 9826400 Compare June 25, 2026 11:43

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

NOT LGTM — all four prior blockers are resolved (with evidence and a real regression test), but the PR ships a broken nightly build path that should be fixed before merge.

The prior blockers are genuinely fixed, and I want to credit that:

  • B1 (promote-retag.sh scraped third-party/malformed refs): fixed. hack/promote-retag.sh filters every ref through an allowlist (case "$_repo" in "${REGISTRY}/"*), default ghcr.io/cozystack/cozystack), dedups on <repo>@<digest>, and aborts loudly on an empty result. Pinned by a new hack/promote-retag_test.bats that runs the selector --dry-run over the real tree and asserts no non-cozystack ref leaks, plus a REGISTRY-override case — and it runs under make unit-tests in CI.
  • B2 (nightly glob over-selected staging branches): fixed via grep -qE '^release-[0-9]+\.[0-9]+$'.
  • B3 (openapi.json restamped with the rc version): fixed — make openapi-json honors COZYSTACK_VERSION, and promote-rc.yaml passes STABLE_VERSION.
  • B4 (docs/release.md documented the removed force-retag flow): fixed.

Blocker

B1: the nightly build path is broken

nightly-rc.yaml cuts a v<maj>.<min>.0-nightly.<date> tag and its own header claims the tag "triggers the normal rc build/e2e path (tags.yaml + release-e2e.yaml)". But release-e2e.yaml no longer exists, and tags.yaml is the only tag-push-triggered workflow — and its Parse-tag regex ^v(\d+\.\d+\.\d+)(-(?:alpha|beta|rc)\.\d+)?$ rejects -nightly.<date>, so the run hits core.setFailed. The net effect: an enabled nightly fails tags.yaml at parse and never builds or runs e2e.

It's gated off by default (NIGHTLY_RC_ENABLED), so nothing runs today — but enabling nightlies is a documented next step, and shipping a path that is dead-on-arrival the moment it's turned on is the same class of nightly-targeting defect that was a blocker in the previous round. Fix: extend the Parse-tag regex to accept -nightly.<date> (the is_stable gating already keeps the Go-module tag and the release-PR stable-only), or correct the stale release-e2e.yaml reference and define the real nightly build/e2e path.

Minor (non-blocking): the installer env helm-unittest covers talos and hosted but not the generic variant; the COZYSTACK_VERSION block is variant-independent, so the decoupling is covered, but a generic case would close the gap.

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.

LGTM — the nightly blocker from my last review is resolved, the four earlier blockers haven't regressed, and the new mirror path is well-built and unit-tested.

Nightly path (the remaining blocker): resolved by redesign rather than a patch. nightly-rc.yaml is gone; nightlies no longer cut a git tag at all. nightly.yaml mints a non-tag version 0.0.0-nightly.<date>, mirrors the latest main build's images by digest (no rebuild), re-publishes the packages artifact + installer chart + nocloud disk to GHCR, and runs the full e2e against that closure. Because no git tag is pushed, tags.yaml's Parse-tag regex is never reached for a nightly — the core.setFailed path I reported is gone. is_stable gating is unchanged, so the Go-module tag and release PR stay stable-only. docs/release.md is updated accurately. Nice touches: the resolve step validates the source revision is a bare 40-hex sha before using it as a checkout ref, and the mirror reuses the proven allowlist/exclude selector with a digest round-trip verify after each copy.

One thing to fix before enabling nightlies (non-blocking for this PR): build-disk publishes the new cozystack-nocloud GHCR package via oras push without --annotation "org.opencontainers.image.source=https://github.com/cozystack/cozystack". No existing workflow publishes that package, so the first run creates it unlinked and the immediately-following oras tag can hit permission_denied: write_package. Adding the source annotation to the push avoids it.

Two minor notes: (1) in nightly-mirror.sh, ref collection globs */*/values.yaml (depth-2) while the host rewrite uses find -name values.yaml (any depth) — harmless for the current layout, but a deeper ref would be rewritten without being mirrored; (2) resolve reads the sha from cozystack-packages:main but the pull re-fetches :main by tag — pinning the pull to the resolved digest closes a small TOCTOU window.

(Merge-readiness, not code: the branch is stacked on its feature base and currently conflicting; it'll need that resolved before merge.)

Base automatically changed from refactor/build-matrix-2937 to main June 29, 2026 08:44
@myasnikovdaniil
myasnikovdaniil force-pushed the feat/release-promote-flow branch from 6308617 to 9f82616 Compare June 29, 2026 09:39

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

🧹 Nitpick comments (1)
packages/core/installer/tests/cozystack_version_test.yaml (1)

16-74: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a generic-variant regression case.

The shared env: refactor also changed the generic branch, but this suite only exercises talos and hosted. A regression in the generic KUBERNETES_SERVICE_* rendering would currently slip through.

Example test to add
+  - it: generic + platformVersion sets COZYSTACK_VERSION and the API endpoint env
+    set:
+      cozystackOperator.variant: generic
+      cozystackOperator.platformVersion: v1.4.0
+      cozystack.apiServerHost: api.example.test
+      cozystack.apiServerPort: "6443"
+    asserts:
+      - equal:
+          path: spec.template.spec.containers[0].env
+          value:
+            - name: COZYSTACK_VERSION
+              value: v1.4.0
+            - name: KUBERNETES_SERVICE_HOST
+              value: api.example.test
+            - name: KUBERNETES_SERVICE_PORT
+              value: "6443"
+        documentSelector:
+          path: kind
+          value: Deployment
🤖 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 `@packages/core/installer/tests/cozystack_version_test.yaml` around lines 16 -
74, The current test suite in cozystack_version_test.yaml only covers talos and
hosted, so add a regression case for the generic variant to validate the shared
env rendering. Extend the existing installer test cases around the
cozystackOperator.variant and cozystackOperator.platformVersion matrix with a
generic branch assertion that checks the Deployment container env output,
especially the KUBERNETES_SERVICE_* values and whether COZYSTACK_VERSION is
present or omitted as expected.
🤖 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/nightly.yaml:
- Around line 79-95: The nightly version in the Resolve source commit and
version step is still mutable because it only uses the date, so same-day reruns
reuse the same tag. Update the version generation logic to include a run-unique
component from the workflow context in addition to the date, and keep the
existing sha validation and output wiring intact so each nightly build gets a
distinct, write-once version.
- Around line 79-105: The nightly workflow resolves a commit SHA from
cozystack-packages:main, but Pull baked package tree still fetches the mutable
:main tag, which can drift between steps. Update the workflow to pin the
artifact pull to the same digest or immutable reference resolved by the Resolve
source commit and version step, and pass that pinned reference into the flux
pull artifact command so Checkout source commit and the baked tree always come
from the same build.
- Around line 97-101: The source checkout step is leaving persisted credentials
in the worktree, which can carry the token into copied sandboxes. Update each
actions/checkout use for the source repo, including the Checkout source commit
step, to set persist-credentials: false so .git/config does not retain
credentials before the checkout is copied to /tmp/$SANDBOX_NAME.

In @.github/workflows/promote-rc.yaml:
- Around line 101-107: The Checkout rc staging branch step is persisting the
GitHub token in the local git config even though git remote set-url later
supplies the app token explicitly. Update the actions/checkout@v4 configuration
in this workflow to disable persisted credentials by setting persist-credentials
to false, keeping the existing ref, fetch-depth, fetch-tags, and token behavior
otherwise unchanged.
- Around line 45-49: The `actions/create-github-app-token` step in the
promote-rc workflow is using an unpinned version and grants broader owner-wide
access than needed. Update that step to use the same SHA-pinned action reference
used elsewhere in the repo, and add a `repositories` restriction set to `${{
github.event.repository.name }}` so the token is scoped only to the current
repository.
- Around line 91-93: The promotion workflow only checks for an existing release,
so a pre-existing stable git tag can slip through and fail later during the
merge step. Update the logic around the stableTag check in the promote-rc
workflow to also verify whether refs/tags/vX.Y.Z already exists before
proceeding, and fail early if either the GitHub release or the git tag is
present.

In @.github/workflows/pull-requests-release.yaml:
- Around line 123-134: Only treat the GitHub non-fast-forward rejection as a
recoverable warning in the updateRef try/catch. In the fast-forward block that
uses github.rest.git.updateRef and logs via console.log, inspect ffErr and
rethrow or fail the workflow for auth, API, or malformed-ref errors, while
keeping the existing warning path only for the descendant-check/non-fast-forward
case so other update failures do not get swallowed.

In @.github/workflows/retention.yaml:
- Around line 82-90: The nightly retention logic in the package pruning step is
counting and slicing per-page JSON instead of the full result set because `gh
api --paginate --jq` emits one array per page. Update the `ids` pipeline in the
retention workflow to combine all pages first with `jq -s` or `--slurp`, then
compute `total` and `drop_ids` from the merged array so `KEEP`, `total`, and
`drop_ids` work correctly across multi-page package listings.

In @.github/workflows/tags.yaml:
- Around line 58-60: The release existence check in the tags workflow is using a
single skip output for two different meanings, which causes stable promotion
runs to skip downstream jobs like docs updates. Update the logic around the
release lookup in the tags workflow so it emits separate outputs from the
existing prepare-release step, such as release_exists for the draft-release
guard and skip_build for rebuild suppression, then change downstream job
conditions to depend on the correct output instead of
prepare-release.outputs.skip.

In `@docs/release.md`:
- Line 83: The release guide uses conflicting stable-tag workflows, so update
the release steps around the stable tag flow in the release doc to describe one
consistent model only. Align the numbered release process and the rc promotion
section so that vX.Y.Z is created only once via the merge/rc promotion path, and
remove any instructions that tell maintainers to push stable tags directly
before CI or otherwise bypass the promotion flow. Make sure the wording around
the release steps and the stable-tag policy references the same behavior across
the relevant sections.

In `@hack/promote-retag.sh`:
- Around line 105-111: The promotion loop in promote-retag.sh currently
overwrites ${repo}:${STABLE} unconditionally via copy, which can retag an
already-published stable image to different bytes. Update the loop around the
ref processing to inspect the current destination tag first, compare its digest
with the source digest, and skip the copy when they match or fail fast when they
differ; keep the existing mutable behavior for ${repo}:latest.

In `@pkg/version/version_test.go`:
- Around line 36-39: The version test setup only sets COZYSTACK_VERSION when
tt.set is true, so the unset case can still pick up an existing ambient value
and miss the fallback path. Update the t.Run block in version_test.go to
explicitly clear or unset COZYSTACK_VERSION for the tt.set false branch before
calling the version lookup, keeping the behavior isolated within the test table
case. Use the existing tt.set, tt.env, and t.Setenv usage in TestVersion to
place the fix.

---

Nitpick comments:
In `@packages/core/installer/tests/cozystack_version_test.yaml`:
- Around line 16-74: The current test suite in cozystack_version_test.yaml only
covers talos and hosted, so add a regression case for the generic variant to
validate the shared env rendering. Extend the existing installer test cases
around the cozystackOperator.variant and cozystackOperator.platformVersion
matrix with a generic branch assertion that checks the Deployment container env
output, especially the KUBERNETES_SERVICE_* values and whether COZYSTACK_VERSION
is present or omitted as expected.
🪄 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

Run ID: f4c27c7c-217b-4167-adad-4d7795441b86

📥 Commits

Reviewing files that changed from the base of the PR and between 80e3fa9 and 9f82616.

📒 Files selected for processing (21)
  • .github/workflows/auto-release.yaml
  • .github/workflows/nightly.yaml
  • .github/workflows/promote-rc.yaml
  • .github/workflows/pull-requests-release.yaml
  • .github/workflows/retention.yaml
  • .github/workflows/tags.yaml
  • Makefile
  • docs/release.md
  • hack/nightly-mirror.sh
  • hack/nightly-mirror_test.bats
  • hack/promote-retag.sh
  • hack/promote-retag_test.bats
  • packages/core/installer/Makefile
  • packages/core/installer/templates/cozystack-operator.yaml
  • packages/core/installer/tests/cozystack_version_test.yaml
  • packages/core/installer/values.yaml
  • packages/system/dashboard/images/console/apps/console/src/App.tsx
  • packages/system/dashboard/images/console/apps/console/src/lib/config.ts
  • packages/system/dashboard/templates/configmap.yaml
  • pkg/version/version.go
  • pkg/version/version_test.go
💤 Files with no reviewable changes (1)
  • .github/workflows/auto-release.yaml

Comment thread .github/workflows/nightly.yaml Outdated
Comment thread .github/workflows/nightly.yaml Outdated
Comment thread .github/workflows/nightly.yaml
Comment thread .github/workflows/promote-rc.yaml Outdated
Comment thread .github/workflows/promote-rc.yaml Outdated
Comment thread .github/workflows/retention.yaml
Comment thread .github/workflows/tags.yaml Outdated
Comment thread docs/release.md
Comment thread hack/promote-retag.sh Outdated
Comment thread pkg/version/version_test.go
@myasnikovdaniil

Copy link
Copy Markdown
Contributor Author

Aleksei Sviridkin (@lexfrei) The broken nightly build path no longer exists — the nightly flow was redesigned after your review. nightly-rc.yaml (which cut a -nightly.<date> git tag and leaned on tags.yaml/release-e2e.yaml) was removed, and nightly.yaml now mirrors the latest main build OCIR→GHCR by digest and runs the full e2e suite inline — it cuts no git tag, so nothing reaches the tags.yaml parse regex or the deleted release-e2e.yaml. CodeRabbit's follow-up review on the new flow is also addressed across 319553c…5885f6a48. The generic-variant installer helm-unittest gap you noted is still open — happy to add it here or as a follow-up, let me know your preference.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.github/workflows/promote-rc.yaml (1)

1-31: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Serialize promotions by stable version, not rc_tag.

The workflow-level concurrency described here still lets vX.Y.Z-rc.1 and vX.Y.Z-rc.2 run at the same time, even though both mutate the same stable refs (vX.Y.Z, release-X.Y.Z, and the draft stable release). That leaves a race where one promotion can overwrite the other before the merge-time write-once guard ever runs. A stable-version key, or even a single global promotion group, would avoid that.

🤖 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 1 - 31, The workflow
concurrency key in Promote RC is too specific to rc_tag and allows multiple rc
promotions for the same stable version to run in parallel. Update the
concurrency group in the workflow so it serializes by stable version (derived
from the rc_tag, or use a single global promotion group) rather than by the full
rc tag, ensuring only one promotion can mutate the same stable refs at a time.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/release.md`:
- Around line 174-181: Rename the section heading in docs/release.md so it
accurately reflects all the workflows it describes, not just tag pushes. Update
the title near the list of tags.yaml, promote-rc.yaml,
pull-requests-release.yaml, and update-releasenotes.yaml to a broader
trigger-based heading, and keep the wording aligned with those workflow names so
maintainers can find the right entry point quickly.

---

Outside diff comments:
In @.github/workflows/promote-rc.yaml:
- Around line 1-31: The workflow concurrency key in Promote RC is too specific
to rc_tag and allows multiple rc promotions for the same stable version to run
in parallel. Update the concurrency group in the workflow so it serializes by
stable version (derived from the rc_tag, or use a single global promotion group)
rather than by the full rc tag, ensuring only one promotion can mutate the same
stable refs at a time.
🪄 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

Run ID: 326ada2d-d24a-45be-888d-25ceb289366f

📥 Commits

Reviewing files that changed from the base of the PR and between 9f82616 and 5885f6a.

📒 Files selected for processing (8)
  • .github/workflows/nightly.yaml
  • .github/workflows/promote-rc.yaml
  • .github/workflows/pull-requests-release.yaml
  • .github/workflows/retention.yaml
  • .github/workflows/tags.yaml
  • docs/release.md
  • hack/promote-retag.sh
  • pkg/version/version_test.go
🚧 Files skipped from review as they are similar to previous changes (5)
  • pkg/version/version_test.go
  • .github/workflows/pull-requests-release.yaml
  • hack/promote-retag.sh
  • .github/workflows/retention.yaml
  • .github/workflows/tags.yaml

Comment thread docs/release.md Outdated
The console config ConfigMap stripped the leading v (trimPrefix "v") while
the operator's COZYSTACK_VERSION and the vX.Y.Z release tags keep it, so the
dashboard displayed 1.4.0 where the operator reported v1.4.0. Keep the tag
verbatim, and build the baked VITE_APP_VERSION fallback as v$(COZYSTACK_
VERSION) too so the deploy-time and build-time version strings stay
consistent with each other and with the operator.

Assisted-By: Claude <[email protected]>
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Signed-off-by: Myasnikov Daniil <[email protected]>
@myasnikovdaniil

myasnikovdaniil commented Jul 6, 2026 •

Copy link
Copy Markdown
Contributor Author

Aleksei Sviridkin (@lexfrei) thanks — both blockers were real; verified and fixed (5 commits on the rebased branch).

B1 (wrong registry) — 846626ba1. promote-rc.yaml now points REGISTRY at ghcr.io/cozystack/cozystack (matching promote-retag.sh's default and where tags.yaml actually pushes release images) and logs into GHCR via --password-stdin. The $REGISTRY/-prefix filter now matches the GHCR refs.

B2 (stable chart never published) — 846626ba1. New step repackages cozy-installer from the restamped stable tree (no rebuild — references the retagged images + cozystack-packages artifact by digest) with --version/--app-version=<stable> and helm pushes it (+ :latest), mirroring nightly.yaml. helm --install … --version X.Y.Z now resolves.

Follow-ups:

  1. c69f0f61c — oras push sets org.opencontainers.image.source so the first nightly links the package (no write_package 403).
  2. 6d3d4908d — added a generic-variant case to the version test (now 5/5).
  3. folded into B1: skopeo login now uses --password-stdin.
  4. 4f2530f9e — nightly-mirror.sh host rewrite now uses the same depth-2 glob as collect_refs.
  5. 727f4e745 — dashboard keeps the v prefix (configmap + baked fallback), matching the operator's COZYSTACK_VERSION.

myasnikovdaniil and others added 3 commits July 6, 2026 17:44
…o finalize

Promotion mutated the registry at dispatch time — retagging ~37 images to
vX.Y.Z (+:latest), publishing the stable cozy-installer chart — before the
promote PR's e2e ran and before any merge. An abandoned promotion then left
stable-named bytes for a version that never shipped, and the (correct) image
write-once check wedged any later re-promotion to the same version.

Move every irreversible registry side effect into pull-requests-release.yaml
finalize, which runs only after the promote PR merged (= after its e2e passed),
making promotion atomic. promote-rc.yaml now only stages the release-X.Y.Z
branch, drafts the stable release, and opens the PR; an abandoned promotion
leaves nothing but an overwritable draft + branch.

Folded into the same restructure:

- Cut the api/apps/v1alpha1/vX.Y.Z Go-module tag in finalize, at the stable
  tag's commit. It was gated in tags.yaml on `release_exists==false &&
  is_stable`, unsatisfiable under promotion (the draft always pre-exists), so
  promoted stables never got their submodule tag — a regression for Go
  consumers. Finalize is where the stable tag is born, so it cuts the matching
  submodule tag write-once (stable only).
- Gate :latest by max-semver. promote-retag.sh now moves :latest only when
  MOVE_LATEST=1 (default off); finalize passes it the same make_latest decision
  the release gets, so a patch on an older line moves neither the release's
  `latest` nor the images' :latest. Added MOVE_LATEST bats coverage.
- Stamp platformVersion into the published stable chart's default values
  (uncommitted, at package time) so `helm --install --version X.Y.Z` reports the
  stable version in telemetry instead of the rc version baked into the operator
  image. Not committed to the branch — that would leak platformVersion=vX.Y.Z
  onto main and mis-stamp the next rc.
- promote-rc: serialize all promotions (static concurrency group, not per-rc);
  label the PR full-e2e so the full suite runs; scope the rc->stable sed to the
  depth-2 values.yaml glob plus the kubernetes .tag files (the build's own write
  set); tolerate a leftover draft so re-promotion is not wedged.
- finalize: assert the PR author is cozystack-ci[bot], and run on an ephemeral
  oracle-vm runner (needs skopeo/helm).

Co-Authored-By: Claude Fable 5 <[email protected]>
Signed-off-by: Myasnikov Daniil <[email protected]>
…eject hand-pushed stables

The tag workflow assumed the old hand-pushed-stable flow in several places that
break (or misbehave) under rc-promotion:

- Drop PUBLISH_FLOATING from the Build step. This job now only ever builds a
  prerelease (a stable tag skips via its pre-existing draft, or is rejected —
  below), so moving :latest here would track the newest rc, and an rc on an old
  line would drag :latest backwards. :latest belongs to promotion (finalize).
- Publish rc releases. tags.yaml created the rc release as a draft and nothing
  ever published it, so the community could not see the rc page or download its
  nocloud-amd64.raw.xz — defeating the rc's stated purpose. Publish it
  (prerelease:true, make_latest:false) once assets are uploaded; rc tags are
  write-once, nothing to protect with draft status.
- Reject hand-pushed stable tags. A stable vX.Y.Z with no pre-existing draft is
  a hand-push down the legacy full-rebuild path (full rebuild, moved :latest,
  rogue release PR). Fail fast with a message pointing at promote-rc.yaml. This
  replaces two now-unreachable steps that shared the same gate: the submodule
  Go-module tag (moved to finalize in the previous commit) and "Create pull
  request if not exists" (promote-rc owns PR creation).
- Gate generate-changelog and update-website-docs on stable (ref_name has no
  '-'), so each rc no longer opens a changelog-vX.Y.Z-rc.N PR that never merges
  and an "update managed apps for vX.Y.Z-rc.N" website PR.
- Paginate the release lookups (check_release, draft-reuse): a promote-created
  draft can sit past the default first 30 and be missed.

Co-Authored-By: Claude Fable 5 <[email protected]>
Signed-off-by: Myasnikov Daniil <[email protected]>
… regex

- nightly.yaml: pass the registry tokens to `skopeo login` over stdin
  (--password-stdin) instead of `-p` on argv, matching promote-rc/finalize and
  keeping the token out of the process list.
- dashboard configmap: require digits-then-dot (^v?[0-9]+\.) when deriving the
  displayed platform version from the console image tag. The old ^v?[0-9] let a
  tagless, port-carrying ref (ghcr.io:443/.../console, whose last ":"-segment is
  "443/.../console") pass as a version. Verified: v1.5.0 / 1.5.0 still surface;
  the port ref and :latest no longer do.

Co-Authored-By: Claude Fable 5 <[email protected]>
Signed-off-by: Myasnikov Daniil <[email protected]>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

NOT LGTM — the transactional-promotion rework adds a "re-promotion never wedges" invariant that the stable-branch push does not actually honor.

Business context: implements #2677 — a stable vX.Y.Z becomes a digest-retagged rc (immutable tags, no rebuild); the operator's reported version is decoupled from the image via COZYSTACK_VERSION.

Blockers

B1: release-X.Y.Z staging-branch push wedges re-promotion

File: .github/workflows/promote-rc.yaml:200
Issue: The stable staging branch is built with git checkout -B + git commit -s (a fresh, timestamp-dependent SHA each run) and pushed with a plain, non-force git push origin HEAD:refs/heads/${STABLE_BRANCH}. On a second dispatch — a retry after a later step fails (Stage rc assets / Restamp / Create draft / Open PR), or promoting a newer rc to the same version — the new commit is a sibling of the already-pushed one (same parent = rc tip, different SHA), so the push is rejected non-fast-forward and promotion stops until the remote branch is deleted by hand.
Evidence: The validate step (lines 105–115) deliberately tolerates a leftover draft so as not to "wedge re-promotion of a newer rc to the same version," and the workflow header (lines 15–17) states an abandoned promotion "leaves nothing but an overwritable … branch: nothing to wedge a later re-promotion." The plain push contradicts both. The sibling step in tags.yaml Create release branch (lines 266–277) already implements the correct compare-before-force for its release-X.Y.Z-rc.N staging branch.
Impact: Fails safe — this runs at dispatch, before any registry side effect, so no stable-named bytes are produced — but a second promotion attempt errors at the push and needs manual branch deletion, breaking the re-promotion path this PR was designed to support.
Fix: Reuse the compare-before-force pattern from tags.yaml Create release branch (skip when unchanged, push -f + log when the branch genuinely moves); the header already treats this as a mutable/overwritable staging branch.

Non-blocking follow-ups

  1. PR description drift: the body names nightly-rc.yaml and NIGHTLY_RC_ENABLED, but the shipped workflow is nightly.yaml gated by NIGHTLY_ENABLED (nightly.yaml:43) — the "Out of scope" checklist points an admin at the wrong variable.
  2. packages/system/dashboard/Makefile:34 bakes APP_VERSION=v (bare) when COZYSTACK_VERSION is empty for local builds, unlike the $(if $(COZYSTACK_VERSION),v…,) guard the root Makefile uses. Dev-only cosmetics (the config.json path overrides it at deploy).

Comment thread .github/workflows/promote-rc.yaml Outdated
git add -A
git commit -s -m "Prepare release v${STABLE_VERSION} (promoted from ${RC_TAG})" \
|| echo "No tag-string changes to commit (digests already stable)"
git push origin "HEAD:refs/heads/${STABLE_BRANCH}"

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.

B1 (blocker): this push is non-force, but git checkout -B + git commit -s above produce a fresh SHA each run, so a second dispatch (retry after a later step fails, or promoting a newer rc to the same version) creates a sibling commit and this push is rejected non-fast-forward — the promotion then wedges until the remote branch is deleted by hand.

That contradicts the validate step (105–115), which tolerates a leftover draft precisely so a newer rc can be re-promoted, and the header's "overwritable … branch: nothing to wedge a later re-promotion." Fails safe (before any registry write), but breaks the re-promotion path by design.

Reuse the compare-before-force pattern already in tags.yaml's Create release branch step (skip when unchanged, push -f + log when it genuinely moves).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 8aebc34. The stable-branch push now mirrors tags.yaml's Create release branch step: after the commit it compares the local SHA against the remote release-X.Y.Z branch and force-pushes (with a ::notice::) only when they differ, skipping when they already match. A re-dispatch's sibling commit now overwrites the staging branch instead of being rejected non-fast-forward, so re-promotion no longer wedges — consistent with the validate step's leftover-draft tolerance and the header's overwritable-branch invariant.

myasnikovdaniil and others added 2 commits July 6, 2026 19:12
Address review feedback from lexfrei on .github/workflows/promote-rc.yaml:200.

The "Prepare stable branch" step built the release-X.Y.Z branch with
`git checkout -B` + `git commit -s` (a fresh, timestamp-dependent SHA every run)
and pushed it with a plain non-force push. On a re-dispatch — retrying after a
later step failed, or promoting a newer rc to the same version — the new commit
is a sibling of the already-pushed one (same parent, different SHA), so the push
is rejected non-fast-forward and promotion wedges until the branch is deleted by
hand. That contradicts both the validate step (which tolerates a leftover draft
precisely so a newer rc can be re-promoted) and the workflow header's
"overwritable branch, nothing to wedge a later re-promotion".

Reuse the compare-before-force pattern from tags.yaml's Create release branch
step: skip when the remote already matches, force-push + log when the branch
genuinely moves. It is a mutable staging branch, so overwriting it is correct.

Co-Authored-By: Claude Fable 5 <[email protected]>
Signed-off-by: Myasnikov Daniil <[email protected]>
…ION build-arg

Address non-blocking review feedback from lexfrei on
packages/system/dashboard/Makefile:34.

When COZYSTACK_VERSION is empty (a local build with no matching git tag) the
build-arg baked a bare `APP_VERSION=v` into the console bundle. Match the guard
the root Makefile already uses for platformVersion so an empty version yields an
empty APP_VERSION instead of "v". Dev-only cosmetics — the config.json path
overrides the displayed version at deploy time — but it removes the mismatch.

Co-Authored-By: Claude Fable 5 <[email protected]>
Signed-off-by: Myasnikov Daniil <[email protected]>
@myasnikovdaniil

Copy link
Copy Markdown
Contributor Author

Aleksei Sviridkin (@lexfrei) Thanks for the catch — all three addressed:

  • B1 (staging-branch push wedges re-promotion): fixed in 8aebc34. The Prepare stable branch push now uses the compare-before-force pattern from tags.yaml's Create release branch step — skip when the remote already matches, force-push + log when the branch genuinely moves — so a re-dispatch's sibling commit overwrites the staging branch instead of being rejected non-fast-forward.
  • Follow-up 2 (dashboard Makefile bare APP_VERSION=v): fixed in ff7c8fa — guarded with the same $(if $(COZYSTACK_VERSION),v…,) the root Makefile uses.
  • Follow-up 1 (PR description drift): updated the description to name nightly.yaml / NIGHTLY_ENABLED (the shipped workflow and variable).

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.

LGTM — the retag-by-digest core, the transactional promote→finalize split, and the version-decoupling half are all sound; every prior blocker is resolved and the one new model-flagged P1 does not reproduce.

Business context update: promotion is now transactional — promote-rc.yaml performs zero registry mutation at dispatch (it only pushes the release-X.Y.Z staging branch, drafts the release, and opens the PR); every irreversible side effect (retag rc→stable by digest, move :latest, publish the stable cozy-installer chart, cut the write-once vX.Y.Z + api/apps/v1alpha1/vX.Y.Z tags) runs in pull-requests-release.yaml finalize, post-merge. (previously: retag/chart/tag ran at promote-dispatch time.)

Verified resolved since the last round:

  • Staging-branch push wedge → promote-rc.yaml now compare-before-force (skip-if-unchanged, -f+log on a genuine move), matching tags.yaml's Create-release-branch step.
  • Registry mismatch (retag hit OCIR, images live on GHCR) → dissolved by the rework: finalize sets REGISTRY: ghcr.io/cozystack/cozystack and logs into ghcr.io; the script default matches.
  • Stable cozy-installer never published → finalize's "Publish stable cozy-installer chart" packages from the merged stable tree and helm pushes :vX.Y.Z (+:latest, gated by the same make_latest decision the release uses).

One reviewer flagged a P1: "gh release create --draft in the promote job auto-creates the vX.Y.Z git tag early, so finalize's write-once tag step then sees a tag at a different SHA and fails." Disproven empirically: a --draft release created against a non-existent tag yields an untagged-… release and no refs/tags/… ref (GET git/refs/tags/<tag> → 404); the tag is materialized only when the draft is published. Finalize creates the write-once tag at the merge commit before publishing the draft, so the tag correctly pins the tested merge commit. No early tag, no wedge.

Non-blocking follow-ups

  1. hack/promote-retag.sh silently omits 8 cozystack-namespace images that vendor their digest in the repository: + tag: <upstream-ver>@sha256:… shape (kamaji, kilo, linstor-csi, linstor-gui, metallb-controller/-speaker, piraeus-server, redis-operator). collect_refs shape-1 picks up the tag scalar, but ref_repo reduces v1.5.0@sha256:… to v1.5.0, which the ${REGISTRY}/* allowlist then drops (shape-2 needs a separate digest: key these values don't have). Functionally safe today — they carry upstream-version tags, not the cozystack release version, so there is no rc→stable tag to promote and deploys resolve by digest — but the exclusion is an artifact of ref parsing, not an intended filter, and the script comment/test rationale frames kilo (a ghcr.io/cozystack/cozystack/* image) as a "bare upstream tag" alongside the genuinely-foreign kube-ovn/keycloak. Normalizing those values to the repository+digest shape later would silently start creating kamaji:vX.Y.Z tags. Worth a test pinning the exact intended retag set, plus a comment correction.

  2. The dashboard chart has a helm-unittest suite (packages/system/dashboard/tests/), but the new configmap.yaml version-extraction branch — including the deliberate regexMatch "^v?[0-9]+\\." guard against a port-carrying tagless ref — ships without a unittest. Consistent with how the rest of this PR pins every new piece of logic; a 3-case test (normal tag sets version: vX.Y.Z; port-in-ref skips; v prefix preserved) closes it.

  3. The piped while in promote-retag.sh was flagged as swallowing exit 1 — investigated and dismissed: the while is the terminal pipeline stage, so its non-zero exit is the pipeline's status and set -eu aborts the script; the write-once and verify failures do abort correctly.

@myasnikovdaniil
myasnikovdaniil merged commit cece5c2 into main Jul 7, 2026
76 of 78 checks passed
@myasnikovdaniil
myasnikovdaniil deleted the feat/release-promote-flow branch July 7, 2026 12:46
myasnikovdaniil added a commit that referenced this pull request Jul 9, 2026
The zizmor "Audit workflows" gate (added in #3223) is red on main: five
actions/* references in the release workflows from #3017 are pinned to a
floating tag instead of a commit SHA (blanket unpinned-uses policy, High
severity):

- nightly.yaml (x3)  actions/checkout@v4
- promote-rc.yaml     actions/checkout@v4
- retention.yaml      actions/create-github-app-token@v1

Pin each to the SHA the rest of the repo already standardizes on:
- actions/checkout                -> 34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- actions/create-github-app-token -> d72941d797fd3113feb6b93fd0dec494b13a2547 # v1

`zizmor --offline .github/workflows/` now reports no findings.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Signed-off-by: Myasnikov Daniil <[email protected]>
myasnikovdaniil added a commit that referenced this pull request Jul 9, 2026
## What this PR does

Adds `cut-prerelease.yaml` — a `workflow_dispatch` that becomes the
**sole entry point for creating a pre-release tag** (`vX.Y.Z-alpha.N` /
`-beta.N` / `-rc.N`), replacing the manual `git push origin
HEAD:refs/tags/<tag>`.

## Why

#2677 makes stable tags immutable, but tag *creation* was still only
convention-enforced for **pre-releases**. Repo rulesets **can** already
restrict *stable* `vX.Y.Z` creation to CI — a "Restrict creations"
ruleset targeting `v*` with an exclude pattern for `v*-*` covers stable
tags without any new workflow. What that approach **cannot** cover is
pre-releases: `-rc` / `-alpha` / `-beta` tags were hand-pushed, so they
can't be locked to CI while they still have a human entry point.

This workflow closes that gap by making CI the sole creator of
pre-release tags too. With it in place, an admin can extend "Restrict
creations" (CI app as sole bypass) to **all** of `v*` — pre-release and
stable alike — instead of only the stable subset.

_(Correction: an earlier version of this description claimed a ruleset
couldn't express "stable-only" creation. It can, via include + exclude
patterns — thanks @IvanHunters. The workflow's real value is extending
creation-control to pre-release tags, plus the audit trail, single entry
point, guaranteed `base_ref`, and app-identity push that makes
`tags.yaml` fire.)_

## How it works

- Generates the CozyStack CI app token (same app as `promote-rc.yaml`)
and pushes the tag **as the app** — so `tags.yaml` fires (a tag pushed
with the default `GITHUB_TOKEN` would not) and the push event's
`base_ref` is populated (`tags.yaml`'s *Get base branch* step requires
it).
- Validates `^v\d+\.\d+\.\d+-(alpha|beta|rc)\.\d+$` and **rejects stable
`vX.Y.Z`** — stable tags are cut write-once by the promote flow at a PR
merge commit, never here.
- Gates on the dispatcher having **write access** to the repo
(`workflow_dispatch` already requires it; the explicit check is
auditable defense-in-depth that fails closed).
- Enforces dispatch from `main` (new minor's `vX.Y.0-*` only) or the
matching `release-X.Y` branch; write-once (refuses an existing tag) with
a stale-tip guard so a branch that advanced since dispatch fails cleanly
instead of burning the tag name.
- Pushes `HEAD:refs/tags/<tag>` from the branch tip — the exact form
`docs/release.md` prescribes. Runs on `ubuntu-latest` (no build runner
needed).

Also updates `tags.yaml`'s two human-facing error messages and
`docs/release.md` (the RC and patch flows, the workflow list, and a new
repo-side tag-ruleset enforcement note) to point at the workflow instead
of a manual push. Completes SHA-pinning on the touched release workflows
(`nightly.yaml`, `promote-rc.yaml`, `retention.yaml`) — the zizmor
"Audit workflows" gate was red on `main` for those.

## Rollout ordering

The two `v*` tag rulesets have a hard order (now documented in
`docs/release.md`):

1. **Immutability** (*Restrict updates* + *deletions*, no bypass) — safe
to enable at any time; nothing in the flow ever moves or deletes a `v*`
tag.
2. **Creation control** (*Restrict creations*, CI app sole bypass) —
enable **only after this PR ships**, since this workflow is what makes
CI the sole creator of pre-release tags. Enabling it earlier would lock
maintainers out of cutting rc/alpha/beta.

## Validation

- `actionlint` — clean.
- `act -l` — job graph resolves (`cut` / `workflow_dispatch`).
- `zizmor --offline` — no findings across the whole workflows dir
(SHA-pinned actions, least-privilege `contents: read`).
- Run blocks `bash -n` syntax-checked.
- **Untested locally:** the workflow runtime (app-token auth, the real
tag push, `base_ref` population, the `getCollaboratorPermissionLevel`
gate) needs a live run with the CI secrets — the same "orchestration is
unproven until it runs" caveat #3017 carried.

## Review follow-up

Addressed the non-blocking findings from @IvanHunters' review (commit
`fix(ci): address review findings`):

1. **[MINOR]** `main` dispatch now requires patch `Z == 0` (a new
minor's pre-release), so a patch-line rc can't be cut from `main`
content.
2. **[MINOR]** Added a stale-tip guard: if the dispatch branch advanced
between dispatch and push, fail with a re-run hint instead of tagging a
stale commit / burning the write-once name on an empty-`base_ref`
failure.
3. **[MINOR]** Corrected the PR-body rationale above (rulesets can
express stable-only creation via include + exclude).
4. **[NIT]** `ls-remote` now distinguishes "tag absent" (exit 2) from a
transport/auth failure.
5. **[NIT]** Shallow checkout (`fetch-depth: 1`, no `fetch-tags`);
dropped the redundant local tag pre-check.
6. **[NIT]** Pure-input validation now runs before the token mint +
checkout (fail-fast).

Plus a **write-access gate** on the dispatcher, requested separately.

Part of #2677.

## Release note

```release-note
NONE
```

🤖 Generated with [Claude Code](https://claude.com/claude-code)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added a controlled workflow to create pre-release tags (alpha/beta/rc)
with write-once behavior.
* Added stronger enforcement so pre-releases are cut from the correct
release line.
* **Bug Fixes**
* Improved guidance when stable tags are rejected or when base branch
info is missing.
* Added safeguards to prevent cutting tags from an out-of-date branch
tip.
* **Documentation**
* Updated release process docs, including new tag protection rulesets
for `v*` tags.
* **Chores**
* Pinned multiple CI workflow actions to fixed versions for reliability.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
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) kind/feature Categorizes issue or PR as related to a new feature 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