From d97f6a90e695d180b8aeef7f5910614f977b5acc Mon Sep 17 00:00:00 2001 From: hatayama Date: Thu, 16 Jul 2026 09:38:47 +0900 Subject: [PATCH] Add recovery runbook and 403 guidance Document owner-assisted recovery for historical release targets and explain the tag and draft release prerequisites. Emit actionable guidance when recovery-target creation is rejected by GitHub, while preserving the existing release creation and reuse flow. --- .github/workflows/native-cli-publish.yml | 33 +++++- docs/release-recovery-runbook.md | 134 +++++++++++++++++++++++ 2 files changed, 165 insertions(+), 2 deletions(-) create mode 100644 docs/release-recovery-runbook.md diff --git a/.github/workflows/native-cli-publish.yml b/.github/workflows/native-cli-publish.yml index 08ff8ada5..6d85c2abd 100644 --- a/.github/workflows/native-cli-publish.yml +++ b/.github/workflows/native-cli-publish.yml @@ -282,6 +282,7 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_REPOSITORY: ${{ github.repository }} + RECOVERY_TARGET: ${{ github.event_name == 'workflow_dispatch' && inputs.recovery-target }} run: | set -eu tag_error_path="${RUNNER_TEMP}/native-cli-release-tag-error" @@ -305,7 +306,22 @@ jobs: # Why the tag is created before the release: with an active tag ruleset the # workflow token cannot create a release for a not-yet-existing tag, while # explicit ref creation is permitted and pins the tag to the verified commit. - gh api "repos/${GITHUB_REPOSITORY}/git/refs" -f ref="refs/tags/${RELEASE_TAG}" -f sha="${RELEASE_SHA}" + tag_create_error_path="${RUNNER_TEMP}/native-cli-release-tag-create-error" + set +e + gh api "repos/${GITHUB_REPOSITORY}/git/refs" \ + -f ref="refs/tags/${RELEASE_TAG}" \ + -f sha="${RELEASE_SHA}" > "${tag_create_error_path}" 2>&1 + tag_create_status=$? + set -e + if [ "${tag_create_status}" -ne 0 ]; then + cat "${tag_create_error_path}" >&2 + if [ "${RECOVERY_TARGET}" = "true" ] && grep -qE 'HTTP 403|Resource not accessible by integration' "${tag_create_error_path}"; then + echo "Recovery-target release creation was rejected for a historical commit by GitHub." >&2 + echo "Create the tag and draft release as the repository owner, then rerun this workflow." >&2 + echo "See docs/release-recovery-runbook.md for the recovery procedure." >&2 + fi + exit "${tag_create_status}" + fi fi if gh release view "${RELEASE_TAG}" --json isDraft,targetCommitish > release-input/release-state.json 2>/dev/null; then is_draft=$(jq -r '.isDraft' release-input/release-state.json) @@ -318,7 +334,20 @@ jobs: fi prerelease_flag="" case "${RELEASE_TAG}" in *-beta.*) prerelease_flag="--prerelease" ;; esac - gh release create "${RELEASE_TAG}" --draft --title "${RELEASE_TAG}" --notes-file release-input/release-notes.md --target "${RELEASE_SHA}" ${prerelease_flag} + release_create_error_path="${RUNNER_TEMP}/native-cli-release-create-error" + set +e + gh release create "${RELEASE_TAG}" --draft --title "${RELEASE_TAG}" --notes-file release-input/release-notes.md --target "${RELEASE_SHA}" ${prerelease_flag} > "${release_create_error_path}" 2>&1 + release_create_status=$? + set -e + if [ "${release_create_status}" -ne 0 ]; then + cat "${release_create_error_path}" >&2 + if [ "${RECOVERY_TARGET}" = "true" ] && grep -qE 'HTTP 403|Resource not accessible by integration' "${release_create_error_path}"; then + echo "Recovery-target draft release creation was rejected for a historical commit by GitHub." >&2 + echo "Create the tag and draft release as the repository owner, then rerun this workflow." >&2 + echo "See docs/release-recovery-runbook.md for the recovery procedure." >&2 + fi + exit "${release_create_status}" + fi echo "release_published=false" >> "$GITHUB_OUTPUT" - name: Attest native CLI release assets diff --git a/docs/release-recovery-runbook.md b/docs/release-recovery-runbook.md new file mode 100644 index 000000000..5fa0d0191 --- /dev/null +++ b/docs/release-recovery-runbook.md @@ -0,0 +1,134 @@ +# Release recovery runbook + +## Root cause + +In `recovery-target` mode, the release target is an earlier commit than the +workflow run's `GITHUB_SHA`. GitHub rejects the Actions `GITHUB_TOKEN` when it +tries to create a tag or release for that different commit, even when the token +has `contents: write`. This is neither a missing permission nor a tag ruleset +failure. + +The behavior was confirmed with the same token and `contents: write` +permission in reproduction runs [29459199798](https://github.com/hatayama/unity-cli-loop/actions/runs/29459199798) +and [29459266044](https://github.com/hatayama/unity-cli-loop/actions/runs/29459266044): + +- A tag pointing at the run head returned HTTP 201. +- A tag pointing at `2d8d1b9443843f45b48479134f846b6f540e928b`, an ancestor of + the run head, returned HTTP 403 `Resource not accessible by integration`. +- Creating a draft release directly at that past commit also returned HTTP 403. + +The precise condition is a ref target different from the run's `GITHUB_SHA`, +not merely an unreachable commit. Normal head-based releases are unaffected. + +## Recovery procedure + +Run these commands with an owner-authenticated `gh` session. Replace every +`` before running a command. + +1. Confirm the authenticated account and repository. + + ```sh + gh auth status + gh api user --jq .login + gh api repos// --jq .full_name + ``` + +2. Determine the release commit and release tag from the release PR and the + failed workflow run. + + ```sh + gh pr view --repo / --json mergeCommit --jq .mergeCommit.oid + gh run view --repo / --json headSha + ``` + + The release commit must be the commit validated by the workflow. Do not use + the current branch head when recovering an older release. + +3. Create the release tag at the validated release commit. + + ```sh + gh api repos///git/refs \ + -f ref=refs/tags/ \ + -f sha= + ``` + +4. Download the native release input artifact and create the draft release. + Creating only the tag is not sufficient: the Actions token also receives + HTTP 403 when it tries to create the draft release. + + ```sh + gh run download --repo / \ + -n native-cli-release-input -D + gh release create --repo / \ + --draft --title \ + --notes-file /release-input/release-notes.md \ + --target + ``` + + Add `--prerelease` when the release tag is a prerelease tag. + +5. Rerun the failed publish job and approve the release environment if + prompted. + + ```sh + gh run rerun --repo / --failed + ``` + +6. If an older `dispatcher-publish` run is waiting for the `cli-release` + environment, cancel it before retrying the recovery. The workflow uses a + concurrency group without automatic cancellation, so an old approval-waiting + run can block the newer run indefinitely. + + ```sh + gh run list --repo / --workflow dispatcher-publish.yml \ + --branch --limit 20 + gh run cancel --repo / + ``` + +7. Recover the Unity package release only after the runner release has + completed and its assets are available. The package sync script creates a + release at the historical release commit and can hit the same 403; create + that release as the owner before rerunning the sync. + + ```sh + gh release create v --repo / \ + --title v \ + --notes-file \ + --target + ``` + + Add `--prerelease` when the package release is a prerelease. + + Run the package sync or the relevant post-publish job after the runner + release assets are complete. The sync reuses an existing correctly targeted + release. + +8. Complete the workflow-dispatch recovery manually. Push-only post-publish + steps are intentionally skipped for a `workflow_dispatch` run. + + ```sh + gh pr edit --repo / \ + --remove-label 'autorelease: pending' \ + --add-label 'autorelease: tagged' + gh workflow run release-please.yml --repo / --ref \ + -f branch= + ``` + +## Scope of the workflows + +`native-cli-publish.yml` is the workflow with `recovery-target` and now emits +this runbook path when tag or draft-release creation returns the observed 403. +The creation and reuse logic is unchanged. + +`dispatcher-publish.yml` has no recovery-target input and requires its release +target to equal `GITHUB_SHA`, so it has no historical-commit creation path. +Its concurrency group still matters during recovery because an older run can +block a retry. + +`scripts/sync-release-please-package-releases.sh` can create package releases +at release-please commits from the repository history. This runbook's owner +pre-creation step covers that path; the runner release must be recovered first. + +Do not add GitHub Actions to the tag-ruleset bypass actors. That would grant +the bot tag deletion and force-update capabilities without addressing the +historical-commit restriction.