ci(release): stop finalize persisting credentials so the tag push triggers 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>
This commit is contained in:
Myasnikov Daniil committed 2026-07-24 18:16:26 +05:00
1 parent 936190d4b6
commit 01e1e7188e
3 files changed
+29 -1

No files matched your search

@@ -81,6 +81,15 @@ jobs:
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
# Do NOT persist GITHUB_TOKEN as http.extraheader. Every tag/branch push
# below re-injects the app token via `git remote set-url`, but a persisted
# extraheader silently wins over the URL credential — so those pushes
# would authenticate as GITHUB_TOKEN, and a GITHUB_TOKEN push creates no
# workflow run (GitHub anti-recursion). That is exactly why v1.6.0's
# stable tag triggered no tags.yaml run (the generate-changelog and
# update-website-docs backstops never fired). With this false, the set-url
# app token authenticates, matching how promote-rc.yaml already pushes.
persist-credentials: false
# Create the release tag at the merge commit — write-once.
#
+1 -1
View File
@@ -259,7 +259,7 @@ Reviewer checklist:
Fires on merge of a PR that is merged, carries the `release` label, and is authored by `cozystack-ci[bot]` (the last guard closes the "name a branch `release-X.Y.Z`, get it labelled, merge it" hole). Head branch must match `release-X.Y.Z[-suffix]`. This is where **every irreversible side effect of a promotion happens** — after the rc's mandatory E2E was checked at dispatch and the PR merged, making promotion transactional. Steps:
1. **Create the tag at the merge commit** (write-once). The merge commit of `Prepare release vX.Y.Z` did not exist before the PR opened, so there is nothing to move — the tag is created here for the first time. A pre-existing tag at a different commit fails the step loudly rather than being force-moved (see [Tag immutability](#tag-immutability)).
1. **Create the tag at the merge commit** (write-once). The merge commit of `Prepare release vX.Y.Z` did not exist before the PR opened, so there is nothing to move — the tag is created here for the first time. A pre-existing tag at a different commit fails the step loudly rather than being force-moved (see [Tag immutability](#tag-immutability)). The checkout uses `persist-credentials: false` so this push authenticates as the **CI app** (via `git remote set-url`), not the default `GITHUB_TOKEN` — a `GITHUB_TOKEN` push creates no workflow run, so a persisted-credential push would silently skip `tags.yaml` entirely and its stable-tag backstops (`generate-changelog`, `update-website-docs`) would never fire.
2. **Cut the `api/apps/v1alpha1/vX.Y.Z` Go-module tag** (write-once, stable only) at the same commit, so Go consumers of `api/apps/v1alpha1` get the release. Moved here from `tags.yaml`, whose `prepare-release` body is skipped for a promoted stable (the draft already exists).
3. **Ensure the maintenance branch `release-X.Y` exists** at the tag commit. Created if missing; updated fast-forward-only — a non-fast-forward update warns and is left for a maintainer rather than being force-updated.
4. **Publish the draft release**, with `docs/changelogs/vX.Y.Z.md` from the merge commit as its body (an absent or whitespace-only file warns and leaves the draft's body in place, rather than publishing blank notes). `make_latest` is computed against published-non-prerelease tags: prereleases stay `false`; tags older than the current max stay `false` (and the current max is force-restored to `latest` if necessary, so an older patch tag cut after a newer minor won't downgrade `latest`).
+19
View File
@@ -8,6 +8,7 @@ REPO_ROOT="$(cd "$(dirname "${BATS_TEST_FILENAME:-$0}")/.." && pwd)"
PROMOTE="$REPO_ROOT/.github/workflows/promote-rc.yaml"
PULL_REQUESTS="$REPO_ROOT/.github/workflows/pull-requests.yaml"
TAGS="$REPO_ROOT/.github/workflows/tags.yaml"
FINALIZE="$REPO_ROOT/.github/workflows/pull-requests-release.yaml"
job_block() {
awk -v job=" $1:" '
@@ -217,3 +218,21 @@ code_lines() {
grep -qF 'promote-rc.yaml::website-docs' "$TAGS"
grep -qiF 'backstop' "$TAGS"
}
@test "finalize checkout does not persist credentials so the app-token tag push triggers tags.yaml" {
block="$(job_block finalize "$FINALIZE")"
[ -n "$block" ]
checkout="$(printf '%s\n' "$block" | awk '
/^ - name: Checkout repo$/ { inside = 1; next }
/^ - name: / { inside = 0 }
inside')"
[ -n "$checkout" ]
# The one-line root-cause fix. Without persist-credentials:false the checkout
# persists GITHUB_TOKEN as http.extraheader, which silently defeats the app token
# each later `git remote set-url` injects onto the tag pushes — and a
# GITHUB_TOKEN-authenticated push creates no workflow run (anti-recursion), so
# tags.yaml's stable-tag backstops never fire (v1.6.0's tag never triggered it).
count="$(printf '%s\n' "$checkout" | code_lines | rg -cF 'persist-credentials: false' || true)"
[ "${count:-0}" -eq 1 ]
}