From b930cda6832ad564f79c9e7da9b222f6889e2594 Mon Sep 17 00:00:00 2001 From: William Aaron Cheung Date: Wed, 9 Sep 2026 18:57:16 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(release-settle):=20`settlers:=20any`?= =?UTF-8?q?=20=E2=80=94=20rely=20on=20the=20environment;=20now=20the=20def?= =?UTF-8?q?ault?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With a `release` environment on the settle job, the required reviewers' approval is the release decision; the extra check that the dispatcher is an admin duplicated it. `settlers` now defaults to `any` (no actor check) and stays available as an optional restriction on who may start a settle (logins and/or `admin`). Template, guide, action README and the action's description say so; the template no longer sets it. Co-Authored-By: Claude Fable 5.1 --- .github/actions/README.md | 2 +- .github/actions/release-settle/README.md | 5 ++--- .github/actions/release-settle/action.yml | 25 ++++++++++++++--------- workflow-templates/release-settle.yml | 17 +++++++-------- 4 files changed, 27 insertions(+), 22 deletions(-) diff --git a/.github/actions/README.md b/.github/actions/README.md index d62af2a..57ec545 100644 --- a/.github/actions/README.md +++ b/.github/actions/README.md @@ -16,7 +16,7 @@ input, output, step and error tables are generated from its `action.yml`. | [`release-candidate`](release-candidate/README.md) | [Release pipeline](RELEASE.md) | Start a release (trunk-first). `stage: propose` bumps the version file on the default branch, drafts this release's changelog entry (dated at settle) from the commits since the previous tag, syncs the previous release's entry from its tag, and opens a `chore/release-candidate-vX.Y.Z` PR; `stage: cut`, run when that PR merges, creates `release-vX.Y.Z` at the merge commit. No tag is created at either stage — tags come from release-publish, once, at settlement. Run as a step in a job the consumer owns; the consumer checks the repository out first (`fetch-depth: 0`, `persist-credentials: false`). Guide: .github/actions/RELEASE.md in megaeth-labs/.github. | | [`release-publish`](release-publish/README.md) | [Release pipeline](RELEASE.md) | Publish a settled release. Normally runs when a `chore/release-settle-vX.Y.Z` PR merges into its release branch (or, with `commit` + `version` given, on an explicit commit that `release-settle` in `direct` mode just made): creates the annotated tag `vX.Y.Z` at the merge commit (exactly once — refuses if it exists, or if the branch moved after the settle PR was opened) and publishes the GitHub Release with the changelog section as notes. This is the only place in the release flow that creates a tag; merging the settle PR is the approval. The default branch's changelog catches up in the next release candidate PR — nothing is back-merged. The consumer checks the repository out first (`fetch-depth: 0`, `persist-credentials: false`). Guide: .github/actions/RELEASE.md in megaeth-labs/.github. | | [`release-publish-rust-crates`](release-publish-rust-crates/README.md) | [Release pipeline](RELEASE.md) | Publish an explicit list of workspace crates to crates.io at the release version. Verifies every listed crate's manifest version first, skips crates already published at that version (so a re-run after a partial failure finishes the rest), publishes the remainder in one `cargo publish -p … -p …` invocation — Cargo orders by dependency and waits for the index between crates (requires Cargo ≥ 1.90) — then polls crates.io until every crate reports the version. `dry_run` runs `cargo publish --dry-run`: full packaging and build verification, nothing uploaded. Run inside a job that has checked out the release tag and installed the toolchain the crates need. Never use `--workspace`: a crate without `publish = false` that was never meant to be published would go out with it. Guide: .github/actions/RELEASE.md in megaeth-labs/.github. | -| [`release-settle`](release-settle/README.md) | [Release pipeline](RELEASE.md) | Propose settling a release candidate: verify `commit` is the tip of the release branch and carries the expected version, generate release notes from the commits since the previous tag, write them into the changelog (stamping the date onto the candidate's `## vX.Y.Z` entry), and — in the default `pr` mode — open a `chore/release-settle-vX.Y.Z` PR onto the release branch; merging that PR is the settlement decision and release-publish then tags the merge commit once. In `direct` mode the dispatch itself is the decision: the dispatching actor must be authorised by `settlers`, the changelog commit is pushed straight to the release branch (the app must be a bypass actor on that branch's ruleset), and release-publish runs immediately on that commit. Run as a step in a job the consumer owns; the consumer checks the repository out first (`fetch-depth: 0`, `persist-credentials: false`). Needs `gh` and `python3` on the runner. Guide: .github/actions/RELEASE.md in megaeth-labs/.github. | +| [`release-settle`](release-settle/README.md) | [Release pipeline](RELEASE.md) | Propose settling a release candidate: verify `commit` is the tip of the release branch and carries the expected version, generate release notes from the commits since the previous tag, write them into the changelog (stamping the date onto the candidate's `## vX.Y.Z` entry), and — in the default `pr` mode — open a `chore/release-settle-vX.Y.Z` PR onto the release branch; merging that PR is the settlement decision and release-publish then tags the merge commit once. In `direct` mode the dispatch itself is the decision, gated by the `release` environment the consumer puts on the settle job (optionally also by `settlers`); the changelog commit is pushed straight to the release branch (the app must be a bypass actor on that branch's ruleset), and release-publish runs immediately on that commit. Run as a step in a job the consumer owns; the consumer checks the repository out first (`fetch-depth: 0`, `persist-credentials: false`). Needs `gh` and `python3` on the runner. Guide: .github/actions/RELEASE.md in megaeth-labs/.github. | | [`release-upload-artifact`](release-upload-artifact/README.md) | [Release pipeline](RELEASE.md) | Upload one file to Google Artifact Registry (generic repository) or a Cloud Storage bucket, idempotently: if the destination already holds a file, its hash is compared — identical means "already there" (success, nothing uploaded), different means failure, never a silent overwrite. Every destination is an input; nothing is defaulted. Authentication is the caller's job: run `google-github-actions/auth` (service-account key or Workload Identity Federation) before this step so `gcloud` is authenticated. `dry_run` computes the checksum and checks the destination but uploads nothing. Guide: .github/actions/RELEASE.md in megaeth-labs/.github. | | [`release-verify-version`](release-verify-version/README.md) | [Release pipeline](RELEASE.md) | Run a command that prints a built artifact's version (`my-binary --version`) and require the output to match the release version, so a binary built from the wrong tree — or a tag that does not match the manifest — never reaches a publish step. By default the expected output is ` ` (the clap shape: the command's basename, then the version without its leading `v`); `expected` overrides it. A command that cannot run at all — missing library, crash, no such flag — fails the step even on a dry run: an artifact that does not execute is never publishable, and its exit code is never swallowed. A mismatch is a warning on a dry run, so a rehearsal reports what a real run would refuse and carries on, and a hard stop otherwise. Guide: .github/actions/RELEASE.md in megaeth-labs/.github. | diff --git a/.github/actions/release-settle/README.md b/.github/actions/release-settle/README.md index a579ffd..c033915 100644 --- a/.github/actions/release-settle/README.md +++ b/.github/actions/release-settle/README.md @@ -3,7 +3,7 @@ `uses: megaeth-labs/.github/.github/actions/release-settle@main` -Propose settling a release candidate: verify `commit` is the tip of the release branch and carries the expected version, generate release notes from the commits since the previous tag, write them into the changelog (stamping the date onto the candidate's `## vX.Y.Z` entry), and — in the default `pr` mode — open a `chore/release-settle-vX.Y.Z` PR onto the release branch; merging that PR is the settlement decision and release-publish then tags the merge commit once. In `direct` mode the dispatch itself is the decision: the dispatching actor must be authorised by `settlers`, the changelog commit is pushed straight to the release branch (the app must be a bypass actor on that branch's ruleset), and release-publish runs immediately on that commit. Run as a step in a job the consumer owns; the consumer checks the repository out first (`fetch-depth: 0`, `persist-credentials: false`). Needs `gh` and `python3` on the runner. Guide: .github/actions/RELEASE.md in megaeth-labs/.github. +Propose settling a release candidate: verify `commit` is the tip of the release branch and carries the expected version, generate release notes from the commits since the previous tag, write them into the changelog (stamping the date onto the candidate's `## vX.Y.Z` entry), and — in the default `pr` mode — open a `chore/release-settle-vX.Y.Z` PR onto the release branch; merging that PR is the settlement decision and release-publish then tags the merge commit once. In `direct` mode the dispatch itself is the decision, gated by the `release` environment the consumer puts on the settle job (optionally also by `settlers`); the changelog commit is pushed straight to the release branch (the app must be a bypass actor on that branch's ruleset), and release-publish runs immediately on that commit. Run as a step in a job the consumer owns; the consumer checks the repository out first (`fetch-depth: 0`, `persist-credentials: false`). Needs `gh` and `python3` on the runner. Guide: .github/actions/RELEASE.md in megaeth-labs/.github. Family: [Release pipeline](../RELEASE.md). @@ -22,7 +22,7 @@ Family: [Release pipeline](../RELEASE.md). | `release_branch_prefix` | no | `release-v` | Release branch name prefix; the branch is `X.Y.Z`. | | `changelog_file` | no | `CHANGELOG.md` | Changelog the entry is written to (Keep-a-Changelog style, newest first). | | `settle_mode` | no | `pr` | `pr` (default): open a settle PR. `direct`: settle and publish now, from this dispatch. | -| `settlers` | no | | Who may settle in `direct` mode: comma-separated GitHub logins, and/or the keyword `admin` (the dispatching actor must have admin permission on the repository, checked with `github_token`). Required for `direct`. | +| `settlers` | no | `any` | Who may settle in `direct` mode. `any` (default): no actor check — the settle job's `release` environment and its required reviewers are the gate, so use it only with `environment:` on that job. Otherwise a comma-separated list of GitHub logins and/or the keyword `admin` (the dispatching actor must have admin permission on the repository, checked with `github_token`), as an extra restriction on who may start a settle. | | `github_token` | no | `${{ github.token }}` | Job token used for the `admin` permission check in `direct` mode. | | `pr_labels` | no | | Comma-separated labels for the settle PR (for repos whose label gates apply to release branches too). | | `git_user_name` | no | `mega-maxwell[bot]` | Committer identity for the commits this action makes. | @@ -65,7 +65,6 @@ Family: [Release pipeline](../RELEASE.md). - `$COMMIT is not the tip of $branch (tip is $tip). Settle the tip, or move the branch first.` - `$FILE at $COMMIT says $actual, expected $version` - `$version is not newer than the latest tag ${latest:-}` -- `settle_mode: direct requires settlers (logins and/or` - `$ACTOR is not authorised to settle directly (settlers: $SETTLERS)` - `$CHANGELOG already carries exactly this v$VERSION entry; nothing to settle` - `could not push the settle commit to $BRANCH — the branch moved since $COMMIT, or the app is not a bypass actor on its ruleset` diff --git a/.github/actions/release-settle/action.yml b/.github/actions/release-settle/action.yml index d888600..6b8be35 100644 --- a/.github/actions/release-settle/action.yml +++ b/.github/actions/release-settle/action.yml @@ -7,10 +7,10 @@ description: >- `pr` mode — open a `chore/release-settle-vX.Y.Z` PR onto the release branch; merging that PR is the settlement decision and release-publish then tags the merge commit once. In `direct` mode the dispatch itself is - the decision: the dispatching actor must be authorised by `settlers`, the - changelog commit is pushed straight to the release branch (the app must be - a bypass actor on that branch's ruleset), and release-publish runs - immediately on that commit. Run as a step in a job the consumer owns; the + the decision, gated by the `release` environment the consumer puts on the + settle job (optionally also by `settlers`); the changelog commit is pushed + straight to the release branch (the app must be a bypass actor on that + branch's ruleset), and release-publish runs immediately on that commit. Run as a step in a job the consumer owns; the consumer checks the repository out first (`fetch-depth: 0`, `persist-credentials: false`). Needs `gh` and `python3` on the runner. Guide: .github/actions/RELEASE.md in megaeth-labs/.github. @@ -55,11 +55,15 @@ inputs: default: pr settlers: description: >- - Who may settle in `direct` mode: comma-separated GitHub logins, and/or - the keyword `admin` (the dispatching actor must have admin permission - on the repository, checked with `github_token`). Required for `direct`. + Who may settle in `direct` mode. `any` (default): no actor check — the + settle job's `release` environment and its required reviewers are the + gate, so use it only with `environment:` on that job. Otherwise a + comma-separated list of GitHub logins and/or the keyword `admin` (the + dispatching actor must have admin permission on the repository, checked + with `github_token`), as an extra restriction on who may start a + settle. required: false - default: "" + default: any github_token: description: "Job token used for the `admin` permission check in `direct` mode." required: false @@ -198,8 +202,9 @@ runs: MODE: ${{ inputs.settle_mode }} run: | set -euo pipefail - if [[ -z "$SETTLERS" ]]; then - echo "::error::settle_mode: direct requires settlers (logins and/or 'admin')"; exit 1 + if [[ -z "$SETTLERS" || "$SETTLERS" == "any" ]]; then + echo "settlers: any — no actor check; the settle job's environment reviewers are the gate" + exit 0 fi ok=false IFS=',' read -ra entries <<<"$SETTLERS" diff --git a/workflow-templates/release-settle.yml b/workflow-templates/release-settle.yml index 9e4c61f..8b9bb4e 100644 --- a/workflow-templates/release-settle.yml +++ b/workflow-templates/release-settle.yml @@ -6,16 +6,17 @@ name: Release Settle # tag, commits them to the changelog on the release branch, and publishes: # annotated tag at that commit, GitHub Release with the notes. # -# The dispatch is the settlement decision, gated twice: the `release` -# environment on the job (its required reviewers approve the run before it -# starts; give it a deployment branch policy of the default branch only) and -# `settlers` in the action (the dispatching actor must be a repository -# admin, or a listed login). The app must be a bypass actor on the -# release-branch ruleset and on the `v*` tag ruleset. +# The dispatch is the settlement decision, gated by the `release` +# environment on the job: its required reviewers approve the run before it +# starts (give the environment a deployment branch policy of the default +# branch only). The app must be a bypass actor on the release-branch ruleset +# and on the `v*` tag ruleset. To also restrict who may *start* a settle, set +# `settlers` on the action (logins and/or `admin`); the default `any` relies +# on the environment alone. # # To settle by PR instead — a reviewed `chore/release-settle-vX.Y.Z` PR onto # the release branch, tagged by release-publish.yml when it merges — remove -# `environment:`, `settle_mode` and `settlers` below. +# `environment:` and `settle_mode` below. # # Guide: https://github.com/megaeth-labs/.github/blob/main/.github/actions/RELEASE.md @@ -65,4 +66,4 @@ jobs: version_pattern: plain changelog_file: CHANGELOG.md settle_mode: direct - settlers: admin + # settlers: admin # optional: also require the dispatcher to be an admin (or a listed login) From ec30eb86d787be6c91478749f6607d190e9e1e9f Mon Sep 17 00:00:00 2001 From: William Aaron Cheung Date: Wed, 9 Sep 2026 18:58:16 +0800 Subject: [PATCH 2/2] docs(release): settlers is optional; the environment is the gate Co-Authored-By: Claude Fable 5.1 --- .github/actions/RELEASE.md | 17 ++++++++++------- .github/actions/release-settle/README.md | 15 ++++++++------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/.github/actions/RELEASE.md b/.github/actions/RELEASE.md index ee19297..69aabe1 100644 --- a/.github/actions/RELEASE.md +++ b/.github/actions/RELEASE.md @@ -46,8 +46,9 @@ The choices that shape it: create them. - **One human gate.** The settle dispatch is the release decision. A `release` GitHub environment on the settle job makes the run wait for its - required reviewers, and the action checks that the dispatching actor is - authorised (`settlers`). Everything after the click is mechanical. + required reviewers; that click is the approval. An optional `settlers` + list can additionally restrict who may start a settle. Everything after + the click is mechanical. - **The changelog has one owner per phase.** The candidate PR drafts the entry (`## vX.Y.Z`, no date) from merged PR titles and syncs the previous release's final entry from its tag; settlement finalises it (date, any @@ -113,8 +114,8 @@ click, the action verifies the commit is a full SHA, the release branch exists, `v1.2.3` does not, the commit is the branch tip, the version file at that commit says `1.2.3`, and `1.2.3` is newer than the latest tag. It warns if the tip lacks a workflow the default branch has (see -[Operations](#operations-and-recovery)). It checks the dispatcher against -`settlers`. Then it regenerates the notes up to the tip, writes +[Operations](#operations-and-recovery)). If `settlers` is set, it checks +the dispatcher against it. Then it regenerates the notes up to the tip, writes `## v1.2.3 (YYYY-MM-DD)` into the changelog, commits that straight onto `release-v1.2.3` (the app bypasses the branch ruleset), creates the annotated tag `v1.2.3` at that commit, and publishes the GitHub Release, marked latest, @@ -213,8 +214,10 @@ same values in all three: `version_file` and `version_pattern`, and `changelog_file` if not `CHANGELOG.md`. In `release-candidate.yml`, set `bump_command` if anything else must move with the version, and install the toolchain it needs in the `propose` job before the action step. Keep -`settle_mode: direct` and `settlers: admin` (or a list of logins) and the -`environment: release` line in the settle job. Keep `release-publish.yml` +`settle_mode: direct` and the `environment: release` line in the settle job; +add `settlers: admin` (or a list of logins) only if starting a settle should +be restricted beyond what the environment's reviewers approve. Keep +`release-publish.yml` even though direct settlement never triggers it: it is the PR-mode fallback, and its job gate rejects anything the app did not open. @@ -260,7 +263,7 @@ toolchain it needs. Examples in use: | `settle_mode` | Decision | Requirements | |---|---|---| -| `direct` (the templates) | the dispatch, approved through the `release` environment and checked against `settlers` | `settlers`: comma-separated logins and/or `admin` (the dispatcher must have admin permission, checked with the job token); the app bypasses the release-branch ruleset | +| `direct` (the templates) | the dispatch, approved through the `release` environment | the environment's required reviewers; the app bypasses the release-branch ruleset. `settlers` (default `any`) may additionally name who can start a settle: comma-separated logins and/or `admin` (the dispatcher must have admin permission, checked with the job token) | | `pr` (the action default) | merging the settle PR | the release-branch ruleset requires a reviewed PR; `release-publish.yml` present on the release branch | **Labels** (`pr_labels`, candidate and settle): for repositories whose diff --git a/.github/actions/release-settle/README.md b/.github/actions/release-settle/README.md index c033915..0bdcbb3 100644 --- a/.github/actions/release-settle/README.md +++ b/.github/actions/release-settle/README.md @@ -86,11 +86,11 @@ reviewers before this step runs: version_pattern: toml changelog_file: CHANGELOG.md settle_mode: direct - settlers: admin + # settlers: admin # optional extra restriction on who may start a settle ``` -Leave out `settle_mode` and `settlers` (and the job's `environment:`) to -settle by PR instead. +Leave out `settle_mode` (and the job's `environment:`) to settle by PR +instead. ## Notes @@ -99,10 +99,11 @@ settle by PR instead. says the version; the version is newer than the latest `v*` tag. Then a warning if `commit` lacks a workflow the default branch has (a `release` event runs `on-release.yml` from the tag's tree). -- `direct`: the dispatcher must match `settlers`; the dated entry is - committed straight onto the release branch (the app must bypass its - ruleset) and `release-publish` runs in the same job with `commit` and - `version` set. +- `direct`: the settle job's `release` environment reviewers are the gate + (`settlers: any`, the default); a login list or `admin` in `settlers` + additionally checks the dispatcher. The dated entry is committed straight + onto the release branch (the app must bypass its ruleset) and + `release-publish` runs in the same job with `commit` and `version` set. - `pr`: pushes `chore/release-settle-vX.Y.Z` and opens the settle PR. A re-run closes the previous settle PR, deletes its branch and pushes a fresh one; nothing is force-pushed.