From 73941e6ae8f0c28cc845992866079ac6d97a5b17 Mon Sep 17 00:00:00 2001 From: Troublor <14846504+Troublor@users.noreply.github.com> Date: Wed, 28 Oct 2026 20:48:45 +0800 Subject: [PATCH] fix(release): standardize gated environment and app credentials --- .github/actions/README.md | 4 +- .github/actions/RELEASE.md | 52 ++++++++++--------- .github/actions/app-token-lint/README.md | 9 ++-- .github/actions/app-token-lint/action.yml | 4 +- .github/actions/release-candidate/README.md | 4 +- .github/actions/release-candidate/action.yml | 12 ++--- .../release-publish-rust-crates/README.md | 2 +- .github/actions/release-publish/README.md | 2 +- .github/actions/release-publish/action.yml | 6 +-- .github/actions/release-settle/README.md | 4 +- .github/scripts/app_token_lint.py | 8 +-- .github/scripts/copy_templates.py | 4 +- .github/scripts/test_app_token_lint.py | 35 +++++++------ .github/scripts/test_copy_templates.py | 6 ++- .github/workflows/actions-test.yml | 10 ++-- README.md | 10 ++-- docs/RELEASING.md | 22 ++++---- workflow-templates/on-release.yml | 17 +++--- workflow-templates/release-candidate.yml | 17 +++--- workflow-templates/release-settle.yml | 21 ++++---- workflow-templates/snapshot-publish.yml | 9 ++-- 21 files changed, 137 insertions(+), 121 deletions(-) diff --git a/.github/actions/README.md b/.github/actions/README.md index d89d083..d97c5b2 100644 --- a/.github/actions/README.md +++ b/.github/actions/README.md @@ -9,7 +9,7 @@ a README whose input, output, step and error tables are generated from its | Action | Family | Does | |---|---|---| -| [`app-token-lint`](app-token-lint/README.md) | Standalone | Fail a workflow change that mints GitHub App tokens the wrong way. Maxwell (the CI app) may not request a `permission-: write` other than pull-requests and issues; Gutenberg (the release app) reads `RELEASE_APP_CLIENT_ID` and `RELEASE_APP_PRIVATE_KEY` from repository scope and may be minted only in `release-candidate.yml`, `release-settle.yml` or `on-release.yml`, with or without an environment and whatever contents permission the job needs, because L6 still forces an explicit `permission-*` list either way; release workflows (those using the release-candidate, release-settle, release-publish or release-route actions, and on-release.yml) mint no Maxwell token, never name their own repository in a read-only dependency token, and never use `mega-maxwell[bot]` as a git identity or PR author filter; and the one-reviewer `publish` environment gates only tagging, Release creation and artifact upload. Violations are reported as `::error file=::/: : `; unreadable or malformed YAML is an error too. No network. | +| [`app-token-lint`](app-token-lint/README.md) | Standalone | Fail a workflow change that mints GitHub App tokens the wrong way. Maxwell (the CI app) may not request a `permission-: write` other than pull-requests and issues; Gutenberg (the release app) reads `MEGA_GUTENBERG_CLIENT_ID` and `MEGA_GUTENBERG_PK` from organization scope and may be minted only in `release-candidate.yml`, `release-settle.yml` or `on-release.yml`, with or without an environment and whatever contents permission the job needs, because L6 still forces an explicit `permission-*` list either way; release workflows (those using the release-candidate, release-settle, release-publish or release-route actions, and on-release.yml) mint no Maxwell token, never name their own repository in a read-only dependency token, and never use `mega-maxwell[bot]` as a git identity or PR author filter; and the one-reviewer `release` environment gates only tagging, Release creation and artifact upload. Violations are reported as `::error file=::/: : `; unreadable or malformed YAML is an error too. No network. | | [`claude-interactive`](claude-interactive/README.md) | [Claude CI](CLAUDE-CI.md) | Run the interactive @claude handler with centralized MegaETH permissions. | | [`claude-issue-triage`](claude-issue-triage/README.md) | [Claude CI](CLAUDE-CI.md) | Run the centralized MegaETH Claude issue triage. | | [`claude-label-check`](claude-label-check/README.md) | [Claude CI](CLAUDE-CI.md) | Run the centralized MegaETH Claude pull request label check. | @@ -21,7 +21,7 @@ a README whose input, output, step and error tables are generated from its | [`notify-lark`](notify-lark/README.md) | Standalone | Post a message to a Lark (Feishu) group through a custom-bot webhook. The default is a plain text message; `msg_type: post` sends rich text with a title, and `payload` sends any JSON you built yourself (interactive cards, mentions), untouched. If the bot has signature verification enabled, pass its signing `secret` and the request is signed with the documented timestamp + HMAC-SHA256 scheme. The webhook URL and secret go through the environment and are masked. A non-2xx response or a Lark error code fails the step unless `fail_on_error` is `false`; the response body is an output either way. Nothing is organisation-specific: the webhook is an input. | | [`pr-lint`](pr-lint/README.md) | Standalone | Lint a pull request. Currently validates that the PR title follows Conventional Commits, posting a sticky comment on failure and removing it once fixed; further PR-level lint steps can be added here over time. Run as a step inside a job the consumer names, so the resulting status-check context is that job name. | | [`release-assets`](release-assets/README.md) | [Release pipeline](RELEASE.md) | Attach files, plus a generated `SHA256SUMS`, to the GitHub Release for a tag. Re-runs replace assets of the same name (`--clobber`), so the step is idempotent. `dry_run` writes and prints `SHA256SUMS` but attaches nothing. Needs a token with `contents: write` on the repository (the job token is enough). Guide: .github/actions/RELEASE.md in megaeth-labs/.github. | -| [`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 undated changelog entry from the commits since the previous tag, syncs the previous release's entry from its tag, and opens a `chore/release-candidate-X.Y.Z` PR. `stage: refresh` merges an open candidate PR's own base into its head and re-drafts the changelog without rewriting history; any push requires renewed approval. `stage: cut`, run on the push that merged that PR (the consumer passes the `commit` and `pr_number` outputs of `release-route`), re-reads the PR through the API, refuses anything but a merged, gutenberg-authored candidate into the default branch, and creates `release-vX.Y.Z` at the merge commit (a retry that finds it already there is a no-op; at any other commit it refuses). No tag is created at any stage — tags come from release-publish, once, at settlement. `base_commit` releases a commit that is not the tip of the default branch: `propose` cuts the release branch there itself, carries the default branch's release workflows onto it, and points the candidate PR at it; that PR merges into the release branch, so `cut` never runs for it. Run as a step in a job the consumer owns; the repository-scope `RELEASE_APP_CLIENT_ID` and `RELEASE_APP_PRIVATE_KEY` provide the app credentials. The one-reviewer `publish` environment gates only tagging, Release creation and artifact upload. The consumer checks the repository out first (`fetch-depth: 0`, `persist-credentials: false`). Guide: .github/actions/RELEASE.md in megaeth-labs/.github. | +| [`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 undated changelog entry from the commits since the previous tag, syncs the previous release's entry from its tag, and opens a `chore/release-candidate-X.Y.Z` PR. `stage: refresh` merges an open candidate PR's own base into its head and re-drafts the changelog without rewriting history; any push requires renewed approval. `stage: cut`, run on the push that merged that PR (the consumer passes the `commit` and `pr_number` outputs of `release-route`), re-reads the PR through the API, refuses anything but a merged, gutenberg-authored candidate into the default branch, and creates `release-vX.Y.Z` at the merge commit (a retry that finds it already there is a no-op; at any other commit it refuses). No tag is created at any stage — tags come from release-publish, once, at settlement. `base_commit` releases a commit that is not the tip of the default branch: `propose` cuts the release branch there itself, carries the default branch's release workflows onto it, and points the candidate PR at it; that PR merges into the release branch, so `cut` never runs for it. Run as a step in a job the consumer owns; the organization-level `MEGA_GUTENBERG_CLIENT_ID` and `MEGA_GUTENBERG_PK` provide the app credentials. The one-reviewer `release` environment gates only tagging, Release creation and artifact upload. 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. By default, runs on a push to `release-vX.Y.Z` (the consumer passes the `commit` and `pr_number` that `release-route` resolved for the push). Re-verifies the merged `chore/release-settle-vX.Y.Z` PR through the API — merged, merge commit equals the pushed commit, head/base names, author, and the settled-SHA marker against the merge commit's parent. With `settle_source: dispatch`, verifies the merged candidate PR's merge SHA, head, base and author, and that `release_branch` still points at `commit` for a fresh publish, or contains `commit` when resuming an existing tag; repeats this state check immediately before tagging. Then creates the annotated tag `vX.Y.Z` at that commit and publishes the GitHub Release with the changelog section as notes. A tag already at the commit without a Release (a run that died between the two steps) is resumed: the tag step is skipped and the Release is still created. Re-run the push-triggered workflow for the settle-PR shape, or dispatch for the direct shape. A tag at the commit with its Release already there is a retry of a finished run and exits 0 with a notice; a tag at any other commit refuses, because a release is published once. A version that is not newer than every other `vX.Y.Z` tag (the rule `release-route` applies) is refused before the tag, and the Release is marked latest only when it is newer than the current latest Release. A tag push or Release create that loses a race to a concurrent run of the same publish keeps the winner's, if it is at this commit. 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-route`](release-route/README.md) | [Release pipeline](RELEASE.md) | Route a push or manual dispatch to cut, refresh, publish or skip using merged release PR metadata. Read-only; uses the job token, not a release App secret. Requires a checkout with full history and persist-credentials disabled. | diff --git a/.github/actions/RELEASE.md b/.github/actions/RELEASE.md index 3a719cc..b9e7700 100644 --- a/.github/actions/RELEASE.md +++ b/.github/actions/RELEASE.md @@ -61,7 +61,7 @@ The choices that shape it: that PR publishes. When the branch is exactly the candidate merge, with no fixes on top and no settle PR open, a maintainer can instead dispatch `release-settle.yml` with the version: the clean state routes straight to - `publish`, and the run waits for a required reviewer of the `publish` + `publish`, and the run waits for a required reviewer of the `release` environment. That approval stands in for the PR. - **Settlement is a state, not a mode.** For a versioned dispatch, `release-route` reads the repository (the candidate PR's merge commit, @@ -84,11 +84,11 @@ The choices that shape it: the job. Its PRs trigger CI (a `GITHUB_TOKEN` push would not), the tag ruleset makes it the only tag creator, and publish refuses PRs the app did not open. -- **Credentials are repository-scope; one environment gates the writes.** - The app credentials are a repository variable (`RELEASE_APP_CLIENT_ID`) - and a repository secret (`RELEASE_APP_PRIVATE_KEY`), so the branch and PR +- **Credentials are organization-level; one environment gates the writes.** + The app credentials are an organization variable (`MEGA_GUTENBERG_CLIENT_ID`) + and an organization secret (`MEGA_GUTENBERG_PK`) granted to each consumer, so the branch and PR jobs mint tokens with no environment at all. Only the jobs that tag, - create the Release or upload artifacts run in the `publish` environment, + create the Release or upload release artifacts run in the `release` environment, which has required reviewers and a deployment policy of the default branch, `release-v*` branches and `v*` tags. The publish-target credentials (`CARGO_REGISTRY_TOKEN`, `GCP_AUTH_KEY`, whatever the targets @@ -179,7 +179,7 @@ to the branch refreshes the same PR. A blank `version` refreshes every eligible release branch and never publishes. -Both ways run the same publish job in the `publish` environment, so both +Both ways run the same publish job in the `release` environment, so both wait for a required reviewer under "Review deployments" on the run page. After the approval, the job re-verifies the PR (for a dispatch, the merged candidate PR, and that the release branch still points at the commit, @@ -253,9 +253,10 @@ installed on it (organisation settings, GitHub Apps, mega-gutenberg, repository access). Nothing here can check that for you; a missing installation shows up as the token step failing in the first run. -**2. Repository variable and secret.** The workflows mint the app token from -the repository variable `RELEASE_APP_CLIENT_ID` and the repository secret -`RELEASE_APP_PRIVATE_KEY`. Repository scope, not an environment: the +**2. Organization variable and secret.** The workflows mint the app token from +the organization variable `MEGA_GUTENBERG_CLIENT_ID` and the organization secret +`MEGA_GUTENBERG_PK`. Grant the organization secret access to each consumer +repository, and make the variable available to it. Organization scope, not an environment: the candidate and settle branch jobs mint tokens with no environment, so the credentials must resolve outside one. @@ -289,7 +290,9 @@ done `RepositoryRole` actor `5` is the admin role; the `Integration` actor is the app's id. -**4. The `publish` environment.** Required reviewers: the maintainers who +**4. The `release` environment.** The single gated environment for tagging, +Release creation and artifact upload; snapshots use `snapshot` instead, +outside the human review gate. Required reviewers: the maintainers who may approve a release. A versioned dispatch publishes without a PR, so this review is its approval. Deployment policy: the default branch, `release-v*` branches and `v*` tags. The settle publish job runs on the first two, the @@ -302,14 +305,14 @@ a branch build could read it. ```sh uid=$(gh api users/LOGIN -q .id) -gh api -X PUT repos/OWNER/REPO/environments/publish --input - <` commit status linking back to the run. Install: copy the template, replace the build steps and destinations, and -put the GCP secret in an environment whose deployment policy allows the +put the GCP secret in the `snapshot` environment whose deployment policy allows the branch the workflow is dispatched from. The environment authorises the -dispatch ref; `allowed_branches` guards the commit actually built. A -repository that also runs the tagged pipeline can share `publish` when its -policy lists that branch, or give snapshots their own environment. Nothing +dispatch ref; `allowed_branches` guards the commit actually built. Snapshots +stay outside the `release` environment's human review gate. Nothing else is needed: no version file, no changelog, no rulesets, no app. ## Operations and recovery @@ -507,8 +509,8 @@ workflow file differs from the default branch's. It is not a required check; merge, and the reviewer works again from the merged file. **The app cannot push, tag, or read the credentials**: check, in this order, -that the app is installed on the repository, that the repository variable -and secret exist (the branch jobs have no environment to fall back on), and +that the app is installed on the repository, that the organization variable +and secret are available to it (the branch jobs have no environment to fall back on), and that the app is a bypass actor on the `v*` tag ruleset. ## Reference @@ -542,9 +544,9 @@ actions at `@main`: a merge here reaches every repository at once, and that workflow is the gate. Requirements on the consumer's side, in one list: the app installed; the -repository variable `RELEASE_APP_CLIENT_ID` and secret -`RELEASE_APP_PRIVATE_KEY`; the two rulesets, with the app as bypass actor on -the tag ruleset only; the `publish` environment with its reviewers and +organization variable `MEGA_GUTENBERG_CLIENT_ID` and secret +`MEGA_GUTENBERG_PK` with access granted to the repository; the two rulesets, with the app as bypass actor on +the tag ruleset only; the `release` environment with its reviewers and deployment policies; `gh` and `python3` on the runner (any GitHub-hosted image); `release-candidate.yml` and `release-settle.yml` with the same `version_file`/`version_pattern`/`changelog_file` in each; a `CHANGELOG.md`. diff --git a/.github/actions/app-token-lint/README.md b/.github/actions/app-token-lint/README.md index d0ceebe..d0d7825 100644 --- a/.github/actions/app-token-lint/README.md +++ b/.github/actions/app-token-lint/README.md @@ -3,7 +3,7 @@ `uses: megaeth-labs/.github/.github/actions/app-token-lint@main` -Fail a workflow change that mints GitHub App tokens the wrong way. Maxwell (the CI app) may not request a `permission-: write` other than pull-requests and issues; Gutenberg (the release app) reads `RELEASE_APP_CLIENT_ID` and `RELEASE_APP_PRIVATE_KEY` from repository scope and may be minted only in `release-candidate.yml`, `release-settle.yml` or `on-release.yml`, with or without an environment and whatever contents permission the job needs, because L6 still forces an explicit `permission-*` list either way; release workflows (those using the release-candidate, release-settle, release-publish or release-route actions, and on-release.yml) mint no Maxwell token, never name their own repository in a read-only dependency token, and never use `mega-maxwell[bot]` as a git identity or PR author filter; and the one-reviewer `publish` environment gates only tagging, Release creation and artifact upload. Violations are reported as `::error file=::/: : `; unreadable or malformed YAML is an error too. No network. +Fail a workflow change that mints GitHub App tokens the wrong way. Maxwell (the CI app) may not request a `permission-: write` other than pull-requests and issues; Gutenberg (the release app) reads `MEGA_GUTENBERG_CLIENT_ID` and `MEGA_GUTENBERG_PK` from organization scope and may be minted only in `release-candidate.yml`, `release-settle.yml` or `on-release.yml`, with or without an environment and whatever contents permission the job needs, because L6 still forces an explicit `permission-*` list either way; release workflows (those using the release-candidate, release-settle, release-publish or release-route actions, and on-release.yml) mint no Maxwell token, never name their own repository in a read-only dependency token, and never use `mega-maxwell[bot]` as a git identity or PR author filter; and the one-reviewer `release` environment gates only tagging, Release creation and artifact upload. Violations are reported as `::error file=::/: : `; unreadable or malformed YAML is an error too. No network. Standalone action; no family guide. @@ -66,9 +66,8 @@ another directory or several, e.g. `paths: .github/workflows workflow-templates` environment; **L3** release workflows mint no Maxwell token and no read-only dependency token naming their own repository; **L4** release workflows never use `mega-maxwell[bot]`; - **L5** `environment: release`, and `environment: publish` in release workflows, - only in the same exact three files as L2' - (non-release workflows may still use `publish`); **L6** a Maxwell or Gutenberg + **L5** `environment: release` only in the same exact three files as L2' + (including in non-release workflows); **L6** a Maxwell or Gutenberg token, and any app token in a release job, requests at least one `permission-` (without one it gets every permission of the installation). - The shared L2'/L5 allowlist matches the shipped release workflow templates @@ -77,7 +76,7 @@ another directory or several, e.g. `paths: .github/workflows workflow-templates` is a shipped workflow template. - A token step is a step using `actions/create-github-app-token`; Maxwell is recognised by `MEGA_MAXWELL_`, `CI_APP_ID` or `MEGA_CI_APP_PK` in - `client-id`, `app-id` or `private-key`, Gutenberg by `RELEASE_APP_`. + `client-id`, `app-id` or `private-key`, Gutenberg by `MEGA_GUTENBERG_`. - A permission that is not a literal value (an expression) cannot be checked and counts as a violation for a Maxwell token. - A path that does not exist, a directory without `*.yml`/`*.yaml` files and diff --git a/.github/actions/app-token-lint/action.yml b/.github/actions/app-token-lint/action.yml index 827fbc3..1d201a2 100644 --- a/.github/actions/app-token-lint/action.yml +++ b/.github/actions/app-token-lint/action.yml @@ -3,7 +3,7 @@ description: >- Fail a workflow change that mints GitHub App tokens the wrong way. Maxwell (the CI app) may not request a `permission-: write` other than pull-requests and issues; Gutenberg (the release app) reads - `RELEASE_APP_CLIENT_ID` and `RELEASE_APP_PRIVATE_KEY` from repository scope and + `MEGA_GUTENBERG_CLIENT_ID` and `MEGA_GUTENBERG_PK` from organization scope and may be minted only in `release-candidate.yml`, `release-settle.yml` or `on-release.yml`, with or without an environment and whatever contents permission the job needs, because L6 still forces an explicit @@ -11,7 +11,7 @@ description: >- release-candidate, release-settle, release-publish or release-route actions, and on-release.yml) mint no Maxwell token, never name their own repository in a read-only dependency token, and never use `mega-maxwell[bot]` as a git - identity or PR author filter; and the one-reviewer `publish` environment + identity or PR author filter; and the one-reviewer `release` environment gates only tagging, Release creation and artifact upload. Violations are reported as `::error file=::/: : `; unreadable or malformed YAML is an error too. No network. diff --git a/.github/actions/release-candidate/README.md b/.github/actions/release-candidate/README.md index 4937aa2..1cc964c 100644 --- a/.github/actions/release-candidate/README.md +++ b/.github/actions/release-candidate/README.md @@ -3,7 +3,7 @@ `uses: megaeth-labs/.github/.github/actions/release-candidate@main` -Start a release (trunk-first). `stage: propose` bumps the version file on the default branch, drafts this release's undated changelog entry from the commits since the previous tag, syncs the previous release's entry from its tag, and opens a `chore/release-candidate-X.Y.Z` PR. `stage: refresh` merges an open candidate PR's own base into its head and re-drafts the changelog without rewriting history; any push requires renewed approval. `stage: cut`, run on the push that merged that PR (the consumer passes the `commit` and `pr_number` outputs of `release-route`), re-reads the PR through the API, refuses anything but a merged, gutenberg-authored candidate into the default branch, and creates `release-vX.Y.Z` at the merge commit (a retry that finds it already there is a no-op; at any other commit it refuses). No tag is created at any stage — tags come from release-publish, once, at settlement. `base_commit` releases a commit that is not the tip of the default branch: `propose` cuts the release branch there itself, carries the default branch's release workflows onto it, and points the candidate PR at it; that PR merges into the release branch, so `cut` never runs for it. Run as a step in a job the consumer owns; the repository-scope `RELEASE_APP_CLIENT_ID` and `RELEASE_APP_PRIVATE_KEY` provide the app credentials. The one-reviewer `publish` environment gates only tagging, Release creation and artifact upload. The consumer checks the repository out first (`fetch-depth: 0`, `persist-credentials: false`). Guide: .github/actions/RELEASE.md in megaeth-labs/.github. +Start a release (trunk-first). `stage: propose` bumps the version file on the default branch, drafts this release's undated changelog entry from the commits since the previous tag, syncs the previous release's entry from its tag, and opens a `chore/release-candidate-X.Y.Z` PR. `stage: refresh` merges an open candidate PR's own base into its head and re-drafts the changelog without rewriting history; any push requires renewed approval. `stage: cut`, run on the push that merged that PR (the consumer passes the `commit` and `pr_number` outputs of `release-route`), re-reads the PR through the API, refuses anything but a merged, gutenberg-authored candidate into the default branch, and creates `release-vX.Y.Z` at the merge commit (a retry that finds it already there is a no-op; at any other commit it refuses). No tag is created at any stage — tags come from release-publish, once, at settlement. `base_commit` releases a commit that is not the tip of the default branch: `propose` cuts the release branch there itself, carries the default branch's release workflows onto it, and points the candidate PR at it; that PR merges into the release branch, so `cut` never runs for it. Run as a step in a job the consumer owns; the organization-level `MEGA_GUTENBERG_CLIENT_ID` and `MEGA_GUTENBERG_PK` provide the app credentials. The one-reviewer `release` environment gates only tagging, Release creation and artifact upload. The consumer checks the repository out first (`fetch-depth: 0`, `persist-credentials: false`). Guide: .github/actions/RELEASE.md in megaeth-labs/.github. Family: [Release pipeline](../RELEASE.md). @@ -14,7 +14,7 @@ Family: [Release pipeline](../RELEASE.md). | Input | Required | Default | Description | |---|---|---|---| | `stage` | yes | | `propose` (on workflow_dispatch), `refresh` (an open candidate), or `cut` (on the candidate PR merging). | -| `token` | yes | | mega-gutenberg installation token (contents, pull-requests, workflows write) from repository-scope `RELEASE_APP_CLIENT_ID` and `RELEASE_APP_PRIVATE_KEY`; it pushes the candidate branch, opens the PR (so CI runs) and creates the release branch. The one-reviewer `publish` environment gates only tagging, Release creation and artifact upload. | +| `token` | yes | | mega-gutenberg installation token (contents, pull-requests, workflows write) from organization-level `MEGA_GUTENBERG_CLIENT_ID` and `MEGA_GUTENBERG_PK`; it pushes the candidate branch, opens the PR (so CI runs) and creates the release branch. The one-reviewer `release` environment gates only tagging, Release creation and artifact upload. | | `dependency_token` | no | | Read-only token for bump_command's private dependencies. Required when bump_command is set; it never falls back to token, so the command does not run with the release token. | | `commit` | no | | The merge commit of the candidate PR (the `commit` output of `release-route`). Required for `cut`; the release branch is created at it. | | `pr_number` | no | | The candidate PR number (the `pr_number` output of `release-route`). Required for `cut`; the PR is re-read through the API, not trusted from the event. | diff --git a/.github/actions/release-candidate/action.yml b/.github/actions/release-candidate/action.yml index 63803fc..3949ecb 100644 --- a/.github/actions/release-candidate/action.yml +++ b/.github/actions/release-candidate/action.yml @@ -17,9 +17,9 @@ description: >- default branch: `propose` cuts the release branch there itself, carries the default branch's release workflows onto it, and points the candidate PR at it; that PR merges into the release branch, so `cut` never runs for it. Run - as a step in a job the consumer owns; the repository-scope - `RELEASE_APP_CLIENT_ID` and `RELEASE_APP_PRIVATE_KEY` provide the app - credentials. The one-reviewer `publish` environment gates only tagging, + as a step in a job the consumer owns; the organization-level + `MEGA_GUTENBERG_CLIENT_ID` and `MEGA_GUTENBERG_PK` provide the app + credentials. The one-reviewer `release` environment gates only tagging, Release creation and artifact upload. The consumer checks the repository out first (`fetch-depth: 0`, `persist-credentials: false`). @@ -32,9 +32,9 @@ inputs: token: description: >- mega-gutenberg installation token (contents, pull-requests, workflows - write) from repository-scope `RELEASE_APP_CLIENT_ID` and - `RELEASE_APP_PRIVATE_KEY`; it pushes the candidate branch, opens the PR - (so CI runs) and creates the release branch. The one-reviewer `publish` + write) from organization-level `MEGA_GUTENBERG_CLIENT_ID` and + `MEGA_GUTENBERG_PK`; it pushes the candidate branch, opens the PR + (so CI runs) and creates the release branch. The one-reviewer `release` environment gates only tagging, Release creation and artifact upload. required: true dependency_token: diff --git a/.github/actions/release-publish-rust-crates/README.md b/.github/actions/release-publish-rust-crates/README.md index 8f01003..82ea835 100644 --- a/.github/actions/release-publish-rust-crates/README.md +++ b/.github/actions/release-publish-rust-crates/README.md @@ -51,7 +51,7 @@ Family: [Release pipeline](../RELEASE.md). ## Example From `workflow-templates/on-release.yml`, in a job under -`environment: publish`: +`environment: release`: ```yaml - uses: megaeth-labs/.github/.github/actions/release-publish-rust-crates@main diff --git a/.github/actions/release-publish/README.md b/.github/actions/release-publish/README.md index 9d7c2dd..51306f5 100644 --- a/.github/actions/release-publish/README.md +++ b/.github/actions/release-publish/README.md @@ -13,7 +13,7 @@ Family: [Release pipeline](../RELEASE.md). | Input | Required | Default | Description | |---|---|---|---| -| `token` | yes | | mega-gutenberg installation token from repository-scope `RELEASE_APP_CLIENT_ID` and `RELEASE_APP_PRIVATE_KEY`; gutenberg is the bypass actor on the `v*` tag ruleset, and a Release it creates fires `release: published` workflows. The one-reviewer `publish` environment gates only tagging, Release creation and artifact upload. | +| `token` | yes | | mega-gutenberg installation token from organization-level `MEGA_GUTENBERG_CLIENT_ID` and `MEGA_GUTENBERG_PK`; gutenberg is the bypass actor on the `v*` tag ruleset, and a Release it creates fires `release: published` workflows. The one-reviewer `release` environment gates only tagging, Release creation and artifact upload. | | `version_file` | yes | | File holding the version (VERSION, Cargo.toml, pyproject.toml, package.json); the same value as in release-candidate. | | `version_pattern` | no | `plain` | How the version is stored in `version_file`: plain, toml or json. | | `changelog_file` | no | `CHANGELOG.md` | Changelog the entry is written to (Keep-a-Changelog style, newest first). | diff --git a/.github/actions/release-publish/action.yml b/.github/actions/release-publish/action.yml index 1beb96b..7c23e97 100644 --- a/.github/actions/release-publish/action.yml +++ b/.github/actions/release-publish/action.yml @@ -30,10 +30,10 @@ description: >- inputs: token: description: >- - mega-gutenberg installation token from repository-scope - `RELEASE_APP_CLIENT_ID` and `RELEASE_APP_PRIVATE_KEY`; gutenberg is the + mega-gutenberg installation token from organization-level + `MEGA_GUTENBERG_CLIENT_ID` and `MEGA_GUTENBERG_PK`; gutenberg is the bypass actor on the `v*` tag ruleset, and a Release it creates fires - `release: published` workflows. The one-reviewer `publish` environment + `release: published` workflows. The one-reviewer `release` environment gates only tagging, Release creation and artifact upload. required: true version_file: diff --git a/.github/actions/release-settle/README.md b/.github/actions/release-settle/README.md index 6c19126..e9234b4 100644 --- a/.github/actions/release-settle/README.md +++ b/.github/actions/release-settle/README.md @@ -69,7 +69,7 @@ Family: [Release pipeline](../RELEASE.md). As used by `workflow-templates/release-settle.yml`. The refresh job needs no environment; the publish job that tags and creates the Release declares -`environment: publish`, whose required reviewers authorise the tag: +`environment: release`, whose required reviewers authorise the tag: ```yaml - uses: megaeth-labs/.github/.github/actions/release-settle@main @@ -105,7 +105,7 @@ template's publish job on a clean dispatch or on the settle PR's merge. event runs `on-release.yml` from the tag's tree). - This action never tags and never creates a Release: it maintains `chore/release-settle-vX.Y.Z` only. `release-publish` does the tagging, run - by the template's publish job on a clean dispatch (where the `publish` + by the template's publish job on a clean dispatch (where the `release` environment's required reviewers are the gate) or on the settle PR's merge. - `pr`: pushes `chore/release-settle-vX.Y.Z` and opens the settle PR, titled `chore(release): settle vX.Y.Z — merging publishes the release`. diff --git a/.github/scripts/app_token_lint.py b/.github/scripts/app_token_lint.py index d4d559b..41604dd 100644 --- a/.github/scripts/app_token_lint.py +++ b/.github/scripts/app_token_lint.py @@ -6,7 +6,7 @@ Maxwell CI app (vars.MEGA_MAXWELL_CLIENT_ID / secrets.MEGA_MAXWELL_PK, older names CI_APP_ID / MEGA_CI_APP_PK). Its commits and PRs are what automation shows as `mega-maxwell[bot]`; it never touches a release. - Gutenberg Release app (vars.RELEASE_APP_CLIENT_ID / secrets.RELEASE_APP_PRIVATE_KEY), + Gutenberg Release app (vars.MEGA_GUTENBERG_CLIENT_ID / secrets.MEGA_GUTENBERG_PK), only usable from an allowlisted release workflow file. Rules, each reported as `::error file=::/: : `: @@ -21,7 +21,7 @@ dependency token's `repositories:` must not name the repository itself. L4 A release workflow may not use `mega-maxwell[bot]` as a git identity or a PR author filter. - L5 `environment: release`, and `publish` in a release workflow, may appear + L5 `environment: release` may appear only in release-candidate.yml, release-settle.yml and on-release.yml. L6 A Maxwell or Gutenberg token, and any app token in a release workflow or a `release`-environment job, must request at least one `permission-`: @@ -49,7 +49,7 @@ TOKEN_ACTION = "actions/create-github-app-token@" MAXWELL_MARKERS = ("MEGA_MAXWELL_", "CI_APP_ID", "MEGA_CI_APP_PK") -GUTENBERG_MARKER = "RELEASE_APP_" +GUTENBERG_MARKER = "MEGA_GUTENBERG_" MAXWELL_WRITABLE = {"permission-pull-requests", "permission-issues"} RELEASE_USES = re.compile(r"megaeth-labs/\.github/\.github/actions/release-(candidate|settle|publish|route)") ON_RELEASE = "on-release" @@ -132,7 +132,7 @@ def is_release_workflow(path: str, doc: dict) -> bool: def lint_job(job_id: str, job: dict, filename: str, release: bool, repo: str | None) -> list[Violation]: found: list[Violation] = [] environment = environment_name(job) - if (environment == "release" or (release and environment == "publish")) and filename not in RELEASE_WORKFLOW_FILES: + if environment == "release" and filename not in RELEASE_WORKFLOW_FILES: found.append((job_id, "-", "L5", f"environment `{environment}` is only for {', '.join(sorted(RELEASE_WORKFLOW_FILES))}")) if release and mentions_maxwell_identity({k: v for k, v in job.items() if k != "steps"}): found.append((job_id, "-", "L4", f"{MAXWELL_IDENTITY} is not a release identity")) diff --git a/.github/scripts/copy_templates.py b/.github/scripts/copy_templates.py index ea8d4fc..295b506 100644 --- a/.github/scripts/copy_templates.py +++ b/.github/scripts/copy_templates.py @@ -99,8 +99,8 @@ def dependency_step(repositories: str) -> str: f" - uses: {TOKEN_ACTION}\n" " id: dependency-token\n" " with:\n" - " client-id: ${{ vars.RELEASE_APP_CLIENT_ID }}\n" - " private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}\n" + " client-id: ${{ vars.MEGA_GUTENBERG_CLIENT_ID }}\n" + " private-key: ${{ secrets.MEGA_GUTENBERG_PK }}\n" " owner: megaeth-labs\n" f" repositories: {scalar(repositories)}\n" " permission-contents: read\n\n" diff --git a/.github/scripts/test_app_token_lint.py b/.github/scripts/test_app_token_lint.py index ec44b09..a6cba8c 100644 --- a/.github/scripts/test_app_token_lint.py +++ b/.github/scripts/test_app_token_lint.py @@ -17,7 +17,7 @@ APPS = { "maxwell": ("${{ vars.MEGA_MAXWELL_CLIENT_ID }}", "${{ secrets.MEGA_MAXWELL_PK }}"), "legacy": ("${{ secrets.CI_APP_ID }}", "${{ secrets.MEGA_CI_APP_PK }}"), - "gutenberg": ("${{ vars.RELEASE_APP_CLIENT_ID }}", "${{ secrets.RELEASE_APP_PRIVATE_KEY }}"), + "gutenberg": ("${{ vars.MEGA_GUTENBERG_CLIENT_ID }}", "${{ secrets.MEGA_GUTENBERG_PK }}"), "other": ("${{ vars.SOMETHING_ELSE_ID }}", "${{ secrets.SOMETHING_ELSE_KEY }}"), } LINE = re.compile(r"^::error file=(?P[^:]*)::(?P[^/]+)/(?P.+?): (?PL\d+'?|parse|input): ") @@ -121,8 +121,8 @@ def test_release_environment_as_mapping_passes(self): text = workflow(job(app="gutenberg", environment="{ name: release }", permissions=self.WRITE)) self.assertEqual(lint("release-settle.yml", text), []) - def test_publish_environment_with_only_contents_read_passes(self): - text = workflow(job(app="gutenberg", environment="publish", permissions={"contents": "read"}, repositories=["rust-rocksdb"])) + def test_release_environment_with_only_contents_read_passes(self): + text = workflow(job(app="gutenberg", environment="release", permissions={"contents": "read"}, repositories=["rust-rocksdb"])) self.assertEqual(lint("on-release.yml", text), []) def test_contents_write_without_environment_in_allowed_files_passes(self): @@ -145,6 +145,10 @@ def test_other_environment_in_allowed_file_passes(self): text = workflow(job(app="gutenberg", environment="staging", permissions=self.WRITE)) self.assertEqual(lint("release-candidate.yml", text), []) + def test_other_mega_app_is_not_gutenberg(self): + text = workflow(job(app="other", permissions=self.WRITE)).replace("SOMETHING_ELSE", "MEGA_OTHER") + self.assertEqual(lint("ci.yml", text), []) + def test_similar_filenames_are_not_allowlisted(self): text = workflow(job(app="gutenberg", permissions=self.WRITE)) for name in ("release-candidate.yaml", "release-publish.yml", "release-publish-x.yaml", @@ -158,12 +162,12 @@ def test_dispatch_trigger_does_not_admit_arbitrary_publish_file(self): "on: push", "on: workflow_dispatch") self.assertEqual(rules(lint("release-publish-anything.yml", text)), [("L2'", "build")]) - def test_publish_environment_with_write_passes(self): - text = workflow(job(app="gutenberg", environment="publish", permissions={"contents": "write"})) + def test_release_environment_with_write_passes(self): + text = workflow(job(app="gutenberg", environment="release", permissions={"contents": "write"})) self.assertEqual(lint("on-release.yml", text), []) - def test_publish_environment_with_more_than_contents_read_passes(self): - text = workflow(job(app="gutenberg", environment="publish", permissions={"contents": "read", "pull-requests": "read"})) + def test_release_environment_with_more_than_contents_read_passes(self): + text = workflow(job(app="gutenberg", environment="release", permissions={"contents": "read", "pull-requests": "read"})) self.assertEqual(lint("on-release.yml", text), []) @@ -235,7 +239,7 @@ def test_release_environment_as_mapping_in_another_file_fails(self): self.assertEqual(rules(lint("release-publish.yml", text)), [("L5", "build")]) def test_gated_environments_and_gutenberg_share_exact_file_allowlist(self): - for environment in ("release", "{ name: release }", "publish", "{ name: publish }"): + for environment in ("release", "{ name: release }"): text = workflow(job(app="gutenberg", environment=environment, permissions={"contents": "write"}, release_step=True)).replace( "/release-settle@main", "/release-publish-rust-crates@main") @@ -247,8 +251,7 @@ def test_gated_environments_and_gutenberg_share_exact_file_allowlist(self): with self.subTest(environment=environment, name=name): lines = lint(name, text) self.assertEqual(rules(lines), [("L5", "build"), ("L2'", "build")]) - self.assertIn(f"environment `{ 'publish' if 'publish' in environment else 'release' }`", - lines[0]) + self.assertIn("environment `release`", lines[0]) def test_release_environment_in_the_three_allowed_files_passes(self): text = workflow(job(app=None, environment="release")) @@ -256,15 +259,15 @@ def test_release_environment_in_the_three_allowed_files_passes(self): with self.subTest(name): self.assertEqual(lint(name, text), []) - def test_publish_environment_in_the_three_allowed_files_passes(self): - for environment in ("publish", "{ name: publish }"): + def test_release_environment_in_release_workflows_passes(self): + for environment in ("release", "{ name: release }"): text = workflow(job(app=None, environment=environment, release_step=True)) for name in ("release-candidate.yml", "release-settle.yml", "on-release.yml"): with self.subTest(environment=environment, name=name): self.assertEqual(lint(name, text), []) - def test_publish_environment_in_other_release_workflows_fails(self): - for environment in ("publish", "{ name: publish }"): + def test_release_environment_in_other_release_workflows_fails(self): + for environment in ("release", "{ name: release }"): text = workflow(job(app=None, environment=environment, release_step=True)) for name in ("ci.yml", "release-publish-binaries.yml"): with self.subTest(environment=environment, name=name): @@ -273,8 +276,8 @@ def test_publish_environment_in_other_release_workflows_fails(self): def test_other_environments_are_free(self): self.assertEqual(lint("ci.yml", workflow(job(app=None, environment="staging"))), []) - def test_publish_environment_outside_release_workflows_is_free(self): - self.assertEqual(lint("snapshot-publish.yml", workflow(job(app=None, environment="publish"))), []) + def test_snapshot_environment_outside_release_workflows_is_free(self): + self.assertEqual(lint("snapshot-publish.yml", workflow(job(app=None, environment="snapshot"))), []) class MalformedInput(unittest.TestCase): diff --git a/.github/scripts/test_copy_templates.py b/.github/scripts/test_copy_templates.py index 3e9190c..d09a862 100644 --- a/.github/scripts/test_copy_templates.py +++ b/.github/scripts/test_copy_templates.py @@ -89,10 +89,12 @@ def test_release_tokens_are_scoped_to_the_repository(self): ("release-settle.yml", ("refresh", "publish"))): for job in jobs: if job == "publish": - self.assertEqual(self.doc[name]["jobs"][job]["environment"], "publish", job) + self.assertEqual(self.doc[name]["jobs"][job]["environment"], "release", job) else: self.assertNotIn("environment", self.doc[name]["jobs"][job], job) token = next(s for s in steps(self.doc[name], job) if s.get("id") == "release-token") + self.assertEqual(token["with"]["client-id"], "${{ vars.MEGA_GUTENBERG_CLIENT_ID }}", job) + self.assertEqual(token["with"]["private-key"], "${{ secrets.MEGA_GUTENBERG_PK }}", job) self.assertEqual(token["with"]["repositories"], "mega-reth", job) def test_bump_command_reads_dependencies_with_a_read_only_token(self): @@ -100,6 +102,8 @@ def test_bump_command_reads_dependencies_with_a_read_only_token(self): ids = [s.get("id") for s in steps(doc, "propose")] self.assertLess(ids.index("dependency-token"), len(ids) - 1) dependency = steps(doc, "propose")[ids.index("dependency-token")]["with"] + self.assertEqual(dependency["client-id"], "${{ vars.MEGA_GUTENBERG_CLIENT_ID }}") + self.assertEqual(dependency["private-key"], "${{ secrets.MEGA_GUTENBERG_PK }}") self.assertEqual(dependency["repositories"], "rust-rocksdb,pstd,rocksdb") self.assertEqual({k for k in dependency if k.startswith("permission-")}, {"permission-contents"}) self.assertEqual(dependency["permission-contents"], "read") diff --git a/.github/workflows/actions-test.yml b/.github/workflows/actions-test.yml index ea6e187..411796a 100644 --- a/.github/workflows/actions-test.yml +++ b/.github/workflows/actions-test.yml @@ -1404,8 +1404,8 @@ jobs: - uses: $TOKEN_ACTION # v3 id: release-token with: - client-id: RELEASE_APP_CLIENT_ID - private-key: RELEASE_APP_PRIVATE_KEY + client-id: MEGA_GUTENBERG_CLIENT_ID + private-key: MEGA_GUTENBERG_PK permission-contents: write - uses: megaeth-labs/.github/.github/actions/release-settle@main with: @@ -1419,13 +1419,13 @@ jobs: jobs: dist-docs: runs-on: ubuntu-24.04 - environment: publish + environment: release steps: - uses: $TOKEN_ACTION # v3 id: release-token with: - client-id: RELEASE_APP_CLIENT_ID - private-key: RELEASE_APP_PRIVATE_KEY + client-id: MEGA_GUTENBERG_CLIENT_ID + private-key: MEGA_GUTENBERG_PK repositories: dist-docs permission-contents: write - run: | diff --git a/README.md b/README.md index d41396d..c0c3f8c 100644 --- a/README.md +++ b/README.md @@ -34,9 +34,13 @@ that can run without a repository, and the documentation check. Two GitHub Apps mint tokens through `actions/create-github-app-token`. Maxwell is the CI app (`vars.MEGA_MAXWELL_CLIENT_ID`, `secrets.MEGA_MAXWELL_PK`): it may write only pull requests and issues and never touches a release. -Gutenberg is the release app (`vars.RELEASE_APP_CLIENT_ID`, -`secrets.RELEASE_APP_PRIVATE_KEY`), minted only in a job of the `release` -environment, where every write of a release is made. The +Gutenberg is the release app (`vars.MEGA_GUTENBERG_CLIENT_ID`, +`secrets.MEGA_GUTENBERG_PK`), from an organization variable and secret +with access granted to each consumer repository. It is minted only in +`release-candidate.yml`, `release-settle.yml` or `on-release.yml`; branch +and PR jobs declare no environment, while tagging, Release creation and +artifact upload use the review-gated `release` environment. Snapshots use +`snapshot`, outside that gate. The [`app-token-lint`](.github/actions/app-token-lint/README.md) action enforces this on every workflow change (rules L1-L6, listed with their messages in the [runbook](docs/RELEASING.md#app-token-lint-errors)); run it locally with diff --git a/docs/RELEASING.md b/docs/RELEASING.md index c0ec995..c39314c 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -11,13 +11,13 @@ per-action READMEs linked below. |---|---|---| | `mega-gutenberg[bot]` (the release app) | Every write of the release: the candidate PR, the `release-vX.Y.Z` branch, the settle PR, the tag, the GitHub Release | Nothing is approved by it; it is not a reviewer | | `mega-maxwell[bot]` (the CI app) | Only ever reads, as far as a release is concerned: at most a read-only token for private dependencies | Create a branch, PR, tag or Release of a release; appear as a git identity or PR-author filter in a release workflow | -| A human reviewer | Reviews and merges the candidate PR and the settle PR; approves the `publish` environment when a dispatch publishes; adds the `compat:` label | - | +| A human reviewer | Reviews and merges the candidate PR and the settle PR; approves the `release` environment when a dispatch publishes; adds the `compat:` label | - | The [app-token lint](#app-token-lint) refuses a gutenberg token anywhere outside the allowlisted release workflow files (`release-candidate.yml`, `release-settle.yml`, `on-release.yml`, `release-publish-*.yml`, rule L2') on every workflow change. The publish job runs in the -`publish` environment; the jobs that propose, cut and settle run without one, +`release` environment; the jobs that propose, cut and settle run without one, and the candidate's `refresh` job mints its token without one too. ## The four flows @@ -83,7 +83,7 @@ one-sentence reason in the job log. between the two steps) routes to `publish` again and resumes. Both paths run [`release-publish`](../.github/actions/release-publish/README.md) - in the review-gated `publish` environment, so the dispatch path also waits + in the review-gated `release` environment, so the dispatch path also waits for a human approval. Publish re-reads the PR through the API (the settle PR for a push; the candidate PR and the current branch tip for a dispatch, checked again right before the tag push), creates the annotated tag `vX.Y.Z` @@ -103,14 +103,16 @@ one-sentence reason in the job log. | Environment | Holds | Deployment policy | Reviewers | |---|---|---|---| -| `publish` | `CARGO_REGISTRY_TOKEN`, `GCP_AUTH_KEY` (the publish targets); the release-app credentials, only when a build job needs a read-only gutenberg token for private dependencies | tags `v*` | one, required: a versioned dispatch publishes without a PR, so the environment review is its approval | +| `release` | `CARGO_REGISTRY_TOKEN`, `GCP_AUTH_KEY` (the publish targets) | default branch, `release-v*` branches and tags `v*` | one, required: a versioned dispatch publishes without a PR, so the environment review is its approval | +| `snapshot` | `GCP_AUTH_KEY` for snapshot uploads | the branch the workflow is dispatched from | none; outside the release review gate | -`release` is not listed because no template uses it; the name survives only in -lint rule L5 below. The release-app credentials (`RELEASE_APP_CLIENT_ID` -variable, `RELEASE_APP_PRIVATE_KEY` secret) resolve at repository scope, so +`release` is the single gated environment for tagging, Release creation and +artifact upload; snapshots use `snapshot`. The release-app credentials (`MEGA_GUTENBERG_CLIENT_ID` +variable, `MEGA_GUTENBERG_PK` secret) resolve at organization scope, with +the organization secret granted access to each consumer repository, so the candidate's `refresh` job mints its token with no environment. -`environment: release`, and `publish` in a release workflow, may appear only in +`environment: release` may appear only in `release-candidate.yml`, `release-settle.yml` and `on-release.yml` (lint rule L5). @@ -144,7 +146,7 @@ exists as `dry_run` on [`release-assets`](../.github/actions/release-assets/READ and [`release-verify-version`](../.github/actions/release-verify-version/README.md). The candidate and settle workflows have no dry-run: the candidate and the settle refresh produce PRs, which are themselves the preview, and the publish -path is gated by the `publish` environment's review instead. +path is gated by the `release` environment's review instead. ## Copying a template @@ -246,7 +248,7 @@ Each is reported as `::error file=::/: : `. | L3 | `no Maxwell token in a release workflow; mint a Gutenberg token instead` | Replace the Maxwell token. | | L3 | `read-only dependency token names the repository itself (…)` | Remove the repository from `repositories:`; the checkout uses the job token. | | L4 | `mega-maxwell[bot] is not a release identity` | Use `mega-gutenberg[bot]` as git identity and PR-author filter. | -| L5 | ``environment `release` is only for release-candidate.yml, release-settle.yml and on-release.yml`` (or `publish` in place of `release`, in a release workflow) | Move the job, or use a different environment. | +| L5 | ``environment `release` is only for release-candidate.yml, release-settle.yml and on-release.yml`` | Move the job, or use a different environment. | | L6 | `Maxwell token requests no permission-* inputs, so it gets every permission of the installation (contents write included); list the permissions it needs` (also for Gutenberg, and for any app token in a release job) | Add the `permission-:` inputs the job needs. | | parse | `malformed YAML …`, `not a workflow: the document is not a mapping`, `no such file or directory`, `no workflow files (*.yml, *.yaml) found` | Fix the input; a file that cannot be read never lints clean. | diff --git a/workflow-templates/on-release.yml b/workflow-templates/on-release.yml index f3ba731..3bc2c30 100644 --- a/workflow-templates/on-release.yml +++ b/workflow-templates/on-release.yml @@ -7,7 +7,7 @@ name: On Release # nothing uploaded, published or attached: # gh workflow run on-release.yml --ref vX.Y.Z -f dry_run=true # The tag being released is always the run's own ref; there is no tag input. -# The `publish` environment authorises that ref, so it must also be what +# The `release` environment authorises that ref, so it must also be what # gets built and published. # # This file must exist on the tagged commit: a `release` event resolves the @@ -19,17 +19,18 @@ name: On Release # settled commit lacks a workflow the default branch has. A rehearsal # likewise needs a tag whose tree has this file. # -# Put CARGO_REGISTRY_TOKEN / GCP_AUTH_KEY in the `publish` environment as +# Put CARGO_REGISTRY_TOKEN / GCP_AUTH_KEY in the `release` environment as # environment secrets. Require reviewers and allow the default branch, # `release-v*` branches (release-settle tagging) and `v*` tags (uploads). # These jobs require a tag ref — a release event or a tag-ref rehearsal — # before building or publishing. # # Release-pipeline jobs mint only release-app (mega-gutenberg) tokens. A build -# job that reads private dependency repositories in the `publish` environment +# job that reads private dependency repositories in the `release` environment # mints one with `permission-contents: read` and `repositories:` naming only -# those repositories (never this one). Store RELEASE_APP_CLIENT_ID as a -# repository variable and RELEASE_APP_PRIVATE_KEY as a repository secret. +# those repositories (never this one). Store MEGA_GUTENBERG_CLIENT_ID as an +# organization variable and MEGA_GUTENBERG_PK as an organization secret +# with access granted to this repository. # # Each target is its own job so a hiccup in one never blocks another. Delete # the jobs this repository does not need; keep the shared actions at @main. @@ -60,7 +61,7 @@ permissions: env: # The run's ref — the released tag on a release event, the dispatched tag - # on a rehearsal. Never an input: the publish environment authorises this + # on a rehearsal. Never an input: the release environment authorises this # ref, so it must also be what gets built and published. TAG: ${{ github.ref_name }} # A release event is never a dry run; a dispatch defaults to one. @@ -70,7 +71,7 @@ jobs: # ---- language package index: crates.io ------------------------------------ crates: runs-on: ubuntu-latest - environment: publish + environment: release steps: - name: Require a tag ref run: | @@ -99,7 +100,7 @@ jobs: # ---- binary: build → verify → MegaETH registry → Release asset ------------- binary: runs-on: ubuntu-latest - environment: publish + environment: release permissions: contents: write # release-assets steps: diff --git a/workflow-templates/release-candidate.yml b/workflow-templates/release-candidate.yml index da96ddf..f6e071e 100644 --- a/workflow-templates/release-candidate.yml +++ b/workflow-templates/release-candidate.yml @@ -13,8 +13,9 @@ name: Release Candidate # `route` never selects `cut` for it. # # Every write is made by the release GitHub App (mega-gutenberg). Store -# RELEASE_APP_CLIENT_ID as a repository variable and RELEASE_APP_PRIVATE_KEY -# as a repository secret. Branch and PR jobs need no environment; tagging and +# MEGA_GUTENBERG_CLIENT_ID as an organization variable and MEGA_GUTENBERG_PK +# as an organization secret with access granted to this repository. +# Branch and PR jobs need no environment; tagging and # publishing are review-gated in release-settle. `route` is read-only and uses # the job token. # @@ -54,8 +55,8 @@ jobs: - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3 id: release-token with: - client-id: ${{ vars.RELEASE_APP_CLIENT_ID }} - private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} + client-id: ${{ vars.MEGA_GUTENBERG_CLIENT_ID }} + private-key: ${{ secrets.MEGA_GUTENBERG_PK }} owner: megaeth-labs repositories: ${{ github.event.repository.name }} permission-contents: write @@ -135,8 +136,8 @@ jobs: if: steps.candidate.outputs.version != '' id: release-token with: - client-id: ${{ vars.RELEASE_APP_CLIENT_ID }} - private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} + client-id: ${{ vars.MEGA_GUTENBERG_CLIENT_ID }} + private-key: ${{ secrets.MEGA_GUTENBERG_PK }} owner: megaeth-labs repositories: ${{ github.event.repository.name }} permission-contents: write @@ -199,8 +200,8 @@ jobs: - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3 id: release-token with: - client-id: ${{ vars.RELEASE_APP_CLIENT_ID }} - private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} + client-id: ${{ vars.MEGA_GUTENBERG_CLIENT_ID }} + private-key: ${{ secrets.MEGA_GUTENBERG_PK }} owner: megaeth-labs repositories: ${{ github.event.repository.name }} permission-contents: write diff --git a/workflow-templates/release-settle.yml b/workflow-templates/release-settle.yml index 57438aa..eb0860b 100644 --- a/workflow-templates/release-settle.yml +++ b/workflow-templates/release-settle.yml @@ -10,14 +10,15 @@ name: Release Settle # merge commit and the GitHub Release with the changelog section # as notes. # Merging the settle PR is the release decision on push. Dispatch with `version` -# to publish a clean release branch through the review-gated `publish` +# to publish a clean release branch through the review-gated `release` # environment, or refresh it when settlement is needed. A blank version # refreshes every eligible release branch. # # Every write is made by the release GitHub App (mega-gutenberg). Store -# RELEASE_APP_CLIENT_ID as a repository variable and RELEASE_APP_PRIVATE_KEY -# as a repository secret. Refresh needs no environment. Only publish uses the -# `publish` environment, with required reviewers and a deployment policy for +# MEGA_GUTENBERG_CLIENT_ID as an organization variable and MEGA_GUTENBERG_PK +# as an organization secret with access granted to this repository. +# Refresh needs no environment. Only publish uses the +# `release` environment, with required reviewers and a deployment policy for # the default branch, `release-v*` branches and `v*` tags (artifact uploads). # The app is a bypass actor on the `v*` tag ruleset only; the `release-*` branch # ruleset still requires the reviewed settle PR. `route` uses the job token. @@ -87,8 +88,8 @@ jobs: - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3 id: release-token with: - client-id: ${{ vars.RELEASE_APP_CLIENT_ID }} - private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} + client-id: ${{ vars.MEGA_GUTENBERG_CLIENT_ID }} + private-key: ${{ secrets.MEGA_GUTENBERG_PK }} owner: megaeth-labs repositories: ${{ github.event.repository.name }} permission-contents: write @@ -113,7 +114,7 @@ jobs: publish: needs: route # Route permits a merged settle PR on push or a clean branch on dispatch; - # both publish paths wait for the publish environment's review. + # both publish paths wait for the release environment's review. if: needs.route.outputs.action == 'publish' # One publish per release branch: the tag guard and the tag push are two # steps, so a re-run must queue behind a run in flight, never overlap it. @@ -121,13 +122,13 @@ jobs: group: release-settle-publish-${{ needs.route.outputs.release_branch }} cancel-in-progress: false runs-on: ubuntu-latest - environment: publish + environment: release steps: - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3 id: release-token with: - client-id: ${{ vars.RELEASE_APP_CLIENT_ID }} - private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} + client-id: ${{ vars.MEGA_GUTENBERG_CLIENT_ID }} + private-key: ${{ secrets.MEGA_GUTENBERG_PK }} owner: megaeth-labs repositories: ${{ github.event.repository.name }} permission-contents: write diff --git a/workflow-templates/snapshot-publish.yml b/workflow-templates/snapshot-publish.yml index 2315612..9770c7a 100644 --- a/workflow-templates/snapshot-publish.yml +++ b/workflow-templates/snapshot-publish.yml @@ -8,13 +8,12 @@ name: Snapshot Publish # gh workflow run snapshot-publish.yml --ref main -f ref= # an older commit on main # … -f dry_run=true # rehearse: build, upload nothing # -# What gets built is `ref` if given, else the run's own commit. The `publish` +# What gets built is `ref` if given, else the run's own commit. The `snapshot` # environment authorises the *dispatch* ref (the branch the workflow ran # from), so its deployment branch policy must list that branch; the # `allowed_branches` guard below is what stops a dispatch from publishing a -# commit that never landed on it. A repository that also runs the tagged -# release pipeline can keep `publish` for tags only and give snapshots their -# own environment (`snapshot`, same GCP secret, branch policy `main`). +# commit that never landed on it. Snapshots use their own environment, +# outside the human review gate (`snapshot`, GCP secret, branch policy `main`). # # The registry layout is what the tagged pipeline produces, with `latest` in # place of the tag: `///latest/`. A rerun on @@ -58,7 +57,7 @@ env: jobs: binary: runs-on: ubuntu-latest - environment: publish + environment: snapshot permissions: contents: read statuses: write # release-snapshot record_status; drop with it