Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/actions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ a README whose input, output, step and error tables are generated from its
<!-- generated: catalogue -->
| 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-<x>: 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=<path>::<job>/<step>: <rule>: <message>`; 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-<x>: 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=<path>::<job>/<step>: <rule>: <message>`; 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. |
Expand All @@ -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. |
Expand Down
52 changes: 27 additions & 25 deletions .github/actions/RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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
Expand All @@ -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 - <<JSON
gh api -X PUT repos/OWNER/REPO/environments/release --input - <<JSON
{"wait_timer": 0, "prevent_self_review": false,
"reviewers": [{"type": "User", "id": $uid}],
"deployment_branch_policy": {"protected_branches": false, "custom_branch_policies": true}}
JSON
gh api -X POST repos/OWNER/REPO/environments/publish/deployment-branch-policies -f name=main -f type=branch
gh api -X POST repos/OWNER/REPO/environments/publish/deployment-branch-policies -f name='release-v*' -f type=branch
gh api -X POST repos/OWNER/REPO/environments/publish/deployment-branch-policies -f name='v*' -f type=tag
gh api -X POST repos/OWNER/REPO/environments/release/deployment-branch-policies -f name=main -f type=branch
gh api -X POST repos/OWNER/REPO/environments/release/deployment-branch-policies -f name='release-v*' -f type=branch
gh api -X POST repos/OWNER/REPO/environments/release/deployment-branch-policies -f name='v*' -f type=tag
```

**5. The workflows.** Stamp `release-candidate.yml`, `release-settle.yml`
Expand Down Expand Up @@ -385,9 +388,9 @@ The Release's `published` event runs the repository's `on-release.yml`. What
every one of them carries, and why (the template has all of it):

- `TAG: ${{ github.ref_name }}` and a `Require a tag ref` first step in each
job, never a tag input: the `publish` environment authorises the run's
job, never a tag input: the `release` environment authorises the run's
ref, so that ref is the only thing the run may build and publish.
- `environment: publish` on every job that reads a publish credential.
- `environment: release` on every job that reads a release publish credential.
- A workflow-level `concurrency` group keyed on `github.ref`,
`cancel-in-progress: false`: uploads must not overlap and must not be
cancelled mid-flight.
Expand Down Expand Up @@ -442,11 +445,10 @@ bookended by `release-snapshot`:
`snapshot/<package>` 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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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`.
Loading
Loading