diff --git a/.github/workflows/release-from-notes.yml b/.github/workflows/release-from-notes.yml index a555fe1..8884805 100644 --- a/.github/workflows/release-from-notes.yml +++ b/.github/workflows/release-from-notes.yml @@ -322,7 +322,7 @@ jobs: "https://x-access-token@github.com/${GITHUB_REPOSITORY}.git" \ "HEAD:refs/heads/release/${VERSION}" - - name: Open and auto-merge release PR + - name: Open and merge release PR id: pr env: GH_TOKEN: ${{ github.token }} @@ -336,11 +336,17 @@ jobs: --body "Automated release commit and next-development version bump for ${TAG}. This PR is required because main is protected; once merged, the release continues automatically.")" PR_NUMBER="${PR_URL##*/}" echo "pr_number=${PR_NUMBER}" >> "${GITHUB_OUTPUT}" - gh pr merge "${PR_NUMBER}" --merge --auto --delete-branch - - # Waits for GitHub's native auto-merge to land the PR (e.g. once required - # status checks pass). Fails fast on a real conflict or on the PR being - # closed unmerged, instead of spinning for the full timeout. + # Merge immediately rather than requesting GitHub's native auto-merge + # (which requires the repo-level "Allow auto-merge" setting and + # would otherwise just fail with a GraphQL error). This works because + # required approvals is 0 and main has no required status checks that + # only run on pull_request events, so the PR is already mergeable. + gh pr merge "${PR_NUMBER}" --merge --delete-branch + + # Safety net: the merge above is normally synchronous, but this confirms + # it actually landed (e.g. if a merge queue or delayed check deferred + # it) and fails fast on a real conflict or on the PR being closed + # unmerged, instead of spinning for the full timeout. - name: Wait for release PR to merge env: GH_TOKEN: ${{ github.token }} diff --git a/docs/release-security.md b/docs/release-security.md index db6e571..d4600de 100644 --- a/docs/release-security.md +++ b/docs/release-security.md @@ -78,8 +78,10 @@ derived release commit) through every job. `prepare-release` does **not** push directly to `main`. It pushes the release commit and the next-SNAPSHOT commit to a throwaway `release/` -branch, opens a PR into `main`, enables GitHub's native auto-merge, and waits -for the PR to actually merge before pushing the tag. This is deliberate: +branch, opens a PR into `main`, and merges it immediately (required approvals +is 0 and `main` has no required status checks that only run on `pull_request` +events, so the PR is already mergeable — no dependency on the repo-level +"Allow auto-merge" setting) before pushing the tag. This is deliberate: `main`'s ruleset requires a pull request, and the only way to push directly despite that would be adding a bypass actor to the ruleset. Ruleset bypass is granted to an **identity** (e.g. "GitHub Actions"), not to this one job — @@ -227,8 +229,8 @@ required reviewers. privileged identity to grant. If you also require status checks on `main`, make sure at least one check actually runs on `pull_request` events (this repo's CI currently only triggers on `push`/`workflow_dispatch`), otherwise - the release PR's auto-merge will wait until `prepare-release`'s own - 15-minute timeout and fail. + the immediate merge in `prepare-release` will fail outright with an + unmergeable-PR error. - **Tag ruleset `v*`**: block updates and deletions for everyone (immutable tags). Tag **creation** does not need to be restricted to a bypass actor: by the time `prepare-release` pushes a tag, the commit it points to has @@ -296,9 +298,12 @@ Same procedures as zenwave-sdk (`docs/release-security.md` there): 2. **Snapshot credentials trust `develop`/`next`** — see snapshot section. 3. **npm publication trusts the environment approval + OIDC claims** — no token exists, but an approved run of this workflow from `main` can publish. -4. **GitHub Actions app bypass on `main`** — narrowed by read-only default - workflow permissions and CODEOWNERS on `.github/`; use a dedicated GitHub - App to narrow further. +4. **No ruleset bypass actor exists on `main` or `v*` anymore.** The release + commit lands via an auto-merged PR instead, which was a deliberate change + from an earlier design that would have added a "GitHub Actions" bypass — + that approach was rejected because ruleset bypass is actor-based, not + job-based: it would have let *any* workflow in this repository with + `contents:write` push straight to `main`, not just `prepare-release`. 5. **Trust in `main` itself** — everything on `main` is by definition trusted; branch protection and reviews are what make that hold.