diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b39c391b5b..185f687f97 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1093,37 +1093,36 @@ jobs: [ "$IOS" = success ] || { echo "::error::full device suite (ios) did not succeed on an upstream bump: $IOS"; rc=1; } exit "$rc" - # Keep refs/heads/upstream-base on this fork pointing at the tag in + # Verify refs/heads/upstream-base on this fork points at the tag pinned in # upstream-base.txt. publish tags a tree materialized from a depth-1 clone of # nodejs/node, so the pack it sends names the base commit's parent without # carrying it; the remote's connectivity check rejects the tag ("did not - # receive expected object") unless it already holds that ancestry. This job - # is what makes it hold — the branch both delivers the objects and keeps them - # reachable, so no later GC can take them back. + # receive expected object") unless it already holds that ancestry. The + # upstream-base branch is what makes it hold -- it both delivers the objects + # and keeps them reachable, so no later GC can take them back. # - # It runs on every push rather than only on a bump: the ls-remote comparison - # costs a second and no-ops, and gating on a "did this push move the file" - # derivation would rest the invariant on that derivation being right. Running - # at bump-merge time also keeps the full clone off the release run's critical - # path, where a failure costs a 3-4 hour rebuild to retry. + # Moving that branch is a manual maintainer step, deliberately not done here. + # The push carries upstream's own .github/workflows/ files, and GitHub + # refuses those from an App token (which GITHUB_TOKEN is) unless it holds the + # `workflows` permission -- a scope no `permissions:` block can grant. Doing + # it in CI therefore means storing a PAT or deploy key with write access to + # this repo, which is not worth the exposure for a push taken once per + # upstream bump. So this job only checks, and prints the exact commands. # - # Skipped on pull_request: it wants contents:write, which this workflow - # deliberately keeps away from PR runs. A bump PR therefore gets no signal - # here, but its merge to recipe pushes the base long before any release run. + # It runs on every event, PRs included: the check is two ls-remotes, and a + # bump PR is exactly where the maintainer wants to learn the base still + # needs pushing -- long before a release run gets as far as tagging. upstream-base: - if: github.event_name != 'pull_request' runs-on: ubuntu-24.04 - timeout-minutes: 30 + timeout-minutes: 10 permissions: - contents: write # pushes refs/heads/upstream-base - env: - GH_TOKEN: ${{ github.token }} + contents: read steps: - uses: actions/checkout@v7 with: fetch-depth: 1 - - name: Mirror the pinned upstream base onto this fork + - name: Check the pinned upstream base is on this fork run: | set -euo pipefail # || true: grep exits 1 on a comments-only file, which under pipefail @@ -1140,26 +1139,33 @@ jobs: refs/heads/upstream-base | cut -f1) if [ "$WANT" = "$HAVE" ]; then - echo "upstream-base already at ${BASE} (${WANT})" + echo "upstream-base is at ${BASE} (${WANT})" exit 0 fi - # --single-branch: the base tag's history alone, not every release - # line. The push that follows is small whenever the fork already - # carries an older base — git sends only the delta between them. - git clone --bare --single-branch --branch "$BASE" \ - "$UPSTREAM" "$RUNNER_TEMP/upstream.git" - # --force: a new base need not descend from the old one (it does not - # across a major). Superseded bases stay reachable from the release - # tags built on them, so nothing is orphaned. - # - # --no-follow-tags: never let upstream's own vX.Y.Z tag ride along. - # It would collide with this fork's vX.Y.Z-R release tag namespace - # and can be refused outright by a tag ruleset, failing the push. - git -C "$RUNNER_TEMP/upstream.git" push --force --no-follow-tags \ - "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" \ - "${WANT}:refs/heads/upstream-base" - echo "Pushed upstream-base -> ${BASE} (${WANT})" + echo "::error::upstream-base is not at ${BASE}; a maintainer must push it before a release can be tagged (see docs/UPGRADING.md)" + cat >&2 < ${WANT}:refs/heads/upstream-base + + then re-run this job. + + --force because a new base need not descend from the old one (it does + not across a major); superseded bases stay reachable from the release + tags built on them, so nothing is orphaned. --no-follow-tags keeps + upstream's own ${BASE} tag out of this fork's vX.Y.Z-R tag namespace, + where it would collide and can be refused by a tag ruleset. + EOF + exit 1 # Publish: on a release commit, once every gate in this run is green, tag # the materialized tree and publish the GitHub prerelease with this run's @@ -1215,8 +1221,9 @@ jobs: # Note: the materialized tree is a SHALLOW clone of nodejs/node (prepare.sh # uses --depth 1), so this pushes a tag whose history is grafted at the # upstream base, and git sends only the commits above that boundary. The - # remote accepts it only because the upstream-base job (needs:, above) has - # already put the base and its ancestry there. + # remote accepts it only because the base and its ancestry are already + # there, on the upstream-base branch that the upstream-base job (needs:, + # above) has verified. - name: Tag the materialized tree run: | git config user.name "nodejs-mobile-bot" diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 2d4a213718..1fd9d2e4e2 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -38,10 +38,11 @@ Releasing is a button, a review, and (optionally) an approval: One of those needs is `upstream-base`, which is plumbing rather than a gate. The tag is pushed from a tree materialized by a depth-1 clone, so its history stops at the upstream base and the remote will only accept it - if it already holds that base and everything under it. The job keeps this - fork's `upstream-base` branch on the pinned tag, which puts those objects - there and keeps them reachable. It no-ops in a second unless the base - moved, so it costs nothing except on the run after an upgrade. + if it already holds that base and everything under it. This fork's + `upstream-base` branch is what puts those objects there and keeps them + reachable, and the job checks it is on the pinned tag. Moving it is a + manual step taken once per upgrade, so on a release run this only ever + confirms what the upgrade already did. → [UPGRADING.md](./UPGRADING.md) 4. **Optional human gate:** the publish job runs in the `release` Environment. Add required reviewers under Settings → Environments → diff --git a/docs/UPGRADING.md b/docs/UPGRADING.md index a92c539688..580b90ab8d 100644 --- a/docs/UPGRADING.md +++ b/docs/UPGRADING.md @@ -189,13 +189,28 @@ gate is an allow-list, so it cannot see tests upstream just added — they would otherwise surface on the nightly or at release). Expect the PR to take substantially longer than a normal one. → [TESTING.md](./TESTING.md) -Merging the bump also moves this fork's `upstream-base` branch to the new -tag, via the `upstream-base` job. That branch is load-bearing rather than -informational: the release tag is pushed from a shallow clone whose history -stops at the base, so the remote can only accept it if it already holds the -base and its ancestry. Don't delete the branch, and if the job fails, fix it -before cutting a release — `publish` `needs:` it, so a release run would -otherwise get as far as tagging and stop there. +A bump also needs this fork's `upstream-base` branch moved to the new tag. +That branch is load-bearing rather than informational: the release tag is +pushed from a shallow clone whose history stops at the base, so the remote +can only accept it if it already holds the base and its ancestry. Don't +delete it, and don't leave it behind — `publish` `needs:` the `upstream-base` +job, so a release run would otherwise get as far as tagging and stop there. + +Moving it is a manual maintainer step. CI only checks it, because the push +carries upstream's own `.github/workflows/` files and GitHub refuses those +from `GITHUB_TOKEN` without the `workflows` permission — a scope a workflow +cannot grant itself. Automating it would mean giving CI a PAT or deploy key +with write access to this repo, which is not worth the exposure for a push +taken once per upgrade. The `upstream-base` job prints the exact commands +when it fails; they amount to: + +```sh +git fetch https://github.com/nodejs/node.git refs/tags/v24.20.0 +git push --force --no-follow-tags digidem :refs/heads/upstream-base +``` + +Push it any time from the bump PR onwards — nothing reads the branch except +a release run — then re-run the job. ## Release