diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c8e74ea146..3e07b36372 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -307,7 +307,25 @@ jobs: echo "Error: no nightly tag points at HEAD after release." >&2 exit 1 fi - git push origin "refs/tags/${tag}" + if ! git push origin "refs/tags/${tag}"; then + sha="$(git rev-parse HEAD)" + { + echo "## Tag push rejected" + echo "" + echo "The npm publish succeeded, but pushing \`${tag}\` was rejected." + echo "This usually means the tagged commit modifies workflow files," + echo "which GITHUB_TOKEN may not reference when creating refs from" + echo "dispatch or scheduled runs. Recover with maintainer credentials:" + echo "" + echo '```' + echo "git tag ${tag} ${sha}" + echo "git push origin refs/tags/${tag}" + echo "gh workflow run docker.yml --ref refs/tags/${tag}" + echo '```' + } >> "$GITHUB_STEP_SUMMARY" + echo "::error::Tag push rejected; see the job summary for recovery commands." >&2 + exit 1 + fi # Tag pushes made with GITHUB_TOKEN do not fire docker.yml's tag # trigger (GitHub suppresses workflow runs caused by GITHUB_TOKEN @@ -485,7 +503,25 @@ jobs: echo "Error: no beta tag points at HEAD after release." >&2 exit 1 fi - git push origin "refs/tags/${tag}" + if ! git push origin "refs/tags/${tag}"; then + sha="$(git rev-parse HEAD)" + { + echo "## Tag push rejected" + echo "" + echo "The npm publish succeeded, but pushing \`${tag}\` was rejected." + echo "This usually means the tagged commit modifies workflow files," + echo "which GITHUB_TOKEN may not reference when creating refs from" + echo "dispatch or scheduled runs. Recover with maintainer credentials:" + echo "" + echo '```' + echo "git tag ${tag} ${sha}" + echo "git push origin refs/tags/${tag}" + echo "gh workflow run docker.yml --ref refs/tags/${tag}" + echo '```' + } >> "$GITHUB_STEP_SUMMARY" + echo "::error::Tag push rejected; see the job summary for recovery commands." >&2 + exit 1 + fi # Tag pushes made with GITHUB_TOKEN do not fire docker.yml's tag # trigger (GitHub suppresses workflow runs caused by GITHUB_TOKEN @@ -743,7 +779,26 @@ jobs: echo "Error: no stable tag points at HEAD after release." >&2 exit 1 fi - git push origin "refs/tags/${tag}" + if ! git push origin "refs/tags/${tag}"; then + sha="$(git rev-parse HEAD)" + { + echo "## Tag push rejected" + echo "" + echo "The npm publish succeeded, but pushing \`${tag}\` was rejected." + echo "This usually means the tagged commit modifies workflow files," + echo "which GITHUB_TOKEN may not reference when creating refs from" + echo "dispatch or scheduled runs. Recover with maintainer credentials:" + echo "" + echo '```' + echo "git tag ${tag} ${sha}" + echo "git push origin refs/tags/${tag}" + echo "gh workflow run docker.yml --ref refs/tags/${tag}" + echo "./scripts/create-github-release.sh ${tag#v}" + echo '```' + } >> "$GITHUB_STEP_SUMMARY" + echo "::error::Tag push rejected; see the job summary for recovery commands." >&2 + exit 1 + fi # Tag pushes made with GITHUB_TOKEN do not fire docker.yml's tag # trigger (GitHub suppresses workflow runs caused by GITHUB_TOKEN diff --git a/doc/RELEASING.md b/doc/RELEASING.md index a77ca18729..f6f3e6d41c 100644 --- a/doc/RELEASING.md +++ b/doc/RELEASING.md @@ -380,6 +380,17 @@ force one: dispatch `release.yml` with `channel: nightly` (optionally pinning If the nightly published to npm but the tag push or Docker dispatch failed, push the `nightly/v*` tag manually and run `docker.yml` at that tag. +### If a tag push is rejected with a workflows-permission error + +GITHUB_TOKEN may not create refs that point at commits which modify workflow +files when the run was started by dispatch or schedule (push-triggered runs +are exempt, which is why canary tags on the same commit succeed). The npm +publish is already complete and correct when this happens. The failed job's +summary contains the exact recovery commands: create and push the tag with +maintainer credentials, then dispatch `docker.yml` at the tag (and for +stable, run `create-github-release.sh`). This only occurs when a +release-infrastructure commit itself becomes a promotion source. + ### If a beta looks bad during soak Do not promote it to stable. Fix forward: land the fix on `master`, let it diff --git a/scripts/__tests__/release-verify-workflow.test.mjs b/scripts/__tests__/release-verify-workflow.test.mjs index 02de233cb6..df30f9e953 100644 --- a/scripts/__tests__/release-verify-workflow.test.mjs +++ b/scripts/__tests__/release-verify-workflow.test.mjs @@ -47,6 +47,16 @@ test("promotion selection guards against sources that predate their channel tool assert.match(releaseWorkflow, /git show "\$\{sha\}:scripts\/release\.sh" \| grep -qF 'canary\|nightly\|beta\|stable\)'/); }); +test("every lane's tag push degrades to recovery instructions when rejected", () => { + const releaseWorkflow = readWorkflow("release.yml"); + + // GITHUB_TOKEN may not create refs pointing at workflow-modifying commits + // from dispatch or scheduled runs; a rejected tag push after a successful + // npm publish must surface runbook recovery commands, not a bare error. + const occurrences = releaseWorkflow.match(/## Tag push rejected/g) ?? []; + assert.equal(occurrences.length, 3, "nightly, beta, and stable each carry the recovery summary"); +}); + test("release smoke workflow extends the container readiness budget for CI", () => { const smokeWorkflow = readWorkflow("release-smoke.yml"); const harness = readFileSync(path.join(repoRoot, "scripts/docker-onboard-smoke.sh"), "utf8");