ci(release): carry the finalize fixes onto the 1.6 line (#3530)

## Why

`release-1.6` was created at v1.6.0's merge commit on 2026-07-22, and
the finalize fixes landed on `main` after that. Workflow files run from
the ref they fire on, so a promote PR based on `release-1.6` runs
**this** copy of `pull-requests-release.yaml`, not `main`'s. Shipping
v1.6.1 from this line today would repeat two defects v1.6.0 already hit.

## What this carries

Three upstream commits, after which this file is **byte-identical to
`main`'s copy** — so later backports touching it will not conflict.

| Upstream | Change |
| --- | --- |
| `01e1e7188` | `persist-credentials: false` on the checkout |
| `f49d54a68` | publish the release with the merged changelog as its
body |
| `ba67fea7d` | drop `paths-ignore` from the trigger |

### persist-credentials

Without it the checkout persists `GITHUB_TOKEN` as an
`http.extraheader`, which silently wins over the app token injected by
`git remote set-url`. The stable tag then pushes as `GITHUB_TOKEN`,
which creates no workflow run, so `tags.yaml` never fires and its
`generate-changelog` and `update-website-docs` backstops stay silent.
That is exactly what happened to v1.6.0.

### Changelog as release body

Without it the release publishes with the draft's own body, `"Promoted
from vX.Y.Z-rc.N"`.

**These two compound into a permanent defect on a maintenance line.** A
patch's changelog is committed to `release-1.6.1`, merged into
`release-1.6`, and never reaches `main` — and `update-releasenotes.yaml`
only watches `main`, so nothing would ever sync it. The tag-time
backstop that would have ported it is dead because of the credential
bug. v1.6.1 would ship with placeholder release notes permanently, with
no error anywhere.

### paths-ignore

Latent rather than active. A promote PR carrying only
`docs/changelogs/vX.Y.Z.md` would be dropped by the filter, producing no
finalize run, no tag and no error. v1.6.1 will carry tag-string rewrites
so it would not have fired, but the filter has no remaining purpose now
that the promote PR always carries a changelog.

## On the cherry-pick policy

`docs/release.md`'s skip rule says CI-only changes do not belong in a
patch. That rule governs release *contents*; backporting release
*machinery* so the line is releasable at all is a different thing, and
there is precedent already on this branch — #3473, #3474 and #3514 are
all CI/release backports.

## Verification

`actionlint` clean. `zizmor` clean, no findings. YAML parses. No bats
suite references this workflow, and `hack/promote-gate-contract.bats`
does not exist on this line, so there is no stale contract test to trip.
The diff against `origin/main`'s copy of the file is empty.

## Not included

Three known gaps on this line are deliberately out of scope, none of
which needs a code change to ship v1.6.1:

- `e2e-tag.yaml` is absent and `tags.yaml` has no `rc-e2e` job, so no
E2E runs on the rc. Handled by dispatching **E2E Release Tag from
`main`** against the published rc tag, which also supplies the promote
gate's alternate evidence.
- `pull-requests.yaml` has no `labeled` trigger, so adding `full-e2e` to
the promote PR starts nothing. Backporting that needs the label-event
guards too, and rc-time validation makes it unnecessary here.
- finalize is still the pre-#3456 monolith, so a mid-registry failure is
not re-runnable and needs the documented hand recovery.

```release-note
NONE
```
This commit is contained in:
myasnikovdaniil authored and GitHub committed 2026-08-04 14:50:29 +05:00
commit 6a5ea552e4
1 file changed
+53 -3
+53 -3
View File
@@ -3,8 +3,15 @@ name: "Releasing PR"
on:
pull_request:
types: [closed]
paths-ignore:
- 'docs/**/*'
# No paths filter. A `paths-ignore: docs/**/*` used to live here as an
# optimization, but the promote PR now carries docs/changelogs/vX.Y.Z.md —
# and `Prepare stable branch` explicitly tolerates producing no tag-string
# changes ("digests already stable"). That combination can yield a docs-only
# promote PR, which the filter would drop: no finalize run, so no tag, no
# release, and no error anywhere. Filtering is left entirely to the `if:`
# gate below (merged + `release` label + release-bot author), which is what
# actually decides whether this workflow should do anything. The cost is a
# skipped job on unrelated closed PRs.
# Cancel in‑flight runs for the same PR when a new push arrives.
concurrency:
@@ -74,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.
#
@@ -312,6 +328,39 @@ jobs:
console.log(`🏷️ ${tag} is the highest version, make_latest: true`);
}
// Release body comes from the changelog that merged with THIS PR
// (promote-rc.yaml commits it onto the release-X.Y.Z branch), read
// from the merge commit already checked out here.
//
// Deliberately not left to update-releasenotes.yaml: that workflow
// triggers on pushes to main touching docs/changelogs/v*.md, so it
// would race this job on the very same push — and if it won, it
// would find no published release yet, skip, and never fire again,
// leaving the release body permanently empty. It also only watches
// main, so a patch release whose promote PR targets release-X.Y
// would never be synced at all. Reading the file here is both
// race-free and branch-agnostic; update-releasenotes.yaml remains
// for later maintainer edits and manual re-syncs.
const fs = require('fs');
const changelogPath = `docs/changelogs/${tag}.md`;
let body;
if (fs.existsSync(changelogPath)) {
const raw = fs.readFileSync(changelogPath, 'utf8');
// Last line of defence before the body becomes public and
// immutable-by-convention. promote-rc.yaml validates the changelog
// structurally, but it can also arrive by hand-commit, and an
// empty or whitespace-only file here would blank the release notes
// more thoroughly than publishing the draft's body would.
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)`);
}
} else {
console.log(`::warning::${changelogPath} not found in the merge commit — publishing ${tag} with the draft's existing body. Add the changelog and re-run 'Update Release Notes' to backfill it.`);
}
// Publish the release
await github.rest.repos.updateRelease({
owner: context.repo.owner,
@@ -319,7 +368,8 @@ jobs:
release_id: draft.id,
draft: false,
prerelease: isRc,
make_latest: makeLatest
make_latest: makeLatest,
...(body ? { body } : {})
});
console.log(`🚀 Published release ${tag}`);