Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
14 changes: 10 additions & 4 deletions .github/actions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +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) may be minted only in a job of the `release` environment, or in a `publish` job when the token asks for exactly `permission-contents: read`; 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 `environment: release` appears only in release-candidate.yml, release-settle.yml and on-release.yml. 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 @@ -20,10 +21,11 @@ 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 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. `base_commit` releases a commit that is not the tip of the default branch: `propose` cuts the release branch there itself and points the candidate PR at it, leaving `cut` nothing to do. 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-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 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 either 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, in `environment: release`; 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. 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 — 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. 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-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-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. |
| [`release-settle`](release-settle/README.md) | [Release pipeline](RELEASE.md) | Maintain a settle PR for 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 open or refresh a `chore/release-settle-vX.Y.Z` PR onto the release branch without rewriting its history. New commits on the release branch refresh this PR automatically; merging it is the settlement decision and release-publish then tags the merge commit once. Run as a step in a job the consumer owns; 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-snapshot`](release-snapshot/README.md) | [Release pipeline](RELEASE.md) | The two bookends of a snapshot publish — a build of one commit pushed to the artifact registry with no tag and no GitHub Release, for components that deployments pin by commit. `stage: resolve` turns `ref` (a branch, tag or commit; empty means the run's own commit) into the coordinates the upload steps need: the full commit, and the registry `version` (the commit) and `path` (`<profile>/<label>`, e.g. `release/latest`) that match how deployment tooling addresses a snapshot. With `allowed_branches` it refuses a commit that is not reachable from one of those branches, so a dispatch cannot publish an unreviewed commit even though the environment only authorises the branch the workflow ran from. `stage: summary` writes the run summary after the uploads: where each file went, its checksum, and the manifest entry (`commit`, `version`, `profile`) a deployment repository pins; optionally it also records a commit status per package so the commit page shows what was published. Needs a checkout with history for `resolve` (the branch guard uses `merge-base`); nothing here is organisation-specific. 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 `<name> <version>` (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. |
Expand Down Expand Up @@ -65,4 +67,8 @@ actions share ([README](release-tools/README.md)).
- A new action needs a directory with `action.yml`; the script creates its
README skeleton, whose `Example` and `Notes` sections you then fill in.
- Templates in `workflow-templates/` are the reference callers: keep them in
step with the actions they call.
step with the actions they call. `.github/scripts/copy_templates.py`
renders the release templates for one repository (version file and
pattern, bump command, private dependencies, default branch) into a fresh
directory and fails if a placeholder is left or a `with:` key is not an
input of the action it calls; its unit test runs in `actions-test.yml`.
42 changes: 26 additions & 16 deletions .github/actions/RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,14 @@ The choices that shape it:
`publish` environment whose deployment policy allows only `v*` tag refs, so
a branch build can never read them. The environment has no reviewers: the
human gate is settlement.
- **No force pushes to release branches, settle branches or tags.**
Re-running settle supersedes what it made before (closes the old PR, drops
the branch, pushes fresh), so it works in repositories whose rulesets ban
force pushes. The one exception is a candidate re-dispatched while its PR
is still open: that PR's branch is updated in place, which is a force push
(see [Operations](#operations-and-recovery)).
- **Nothing force-pushes any branch, or a tag.** Re-dispatching a candidate
supersedes the open one: it closes that PR with a comment, deletes the
branch, pushes a fresh one as a new ref and opens a fresh PR. Re-running
settle fast-forwards its own PR's branch, or closes a PR it does not
maintain and starts fresh. So the pipeline works in repositories whose rulesets
ban force pushes on every branch, with no exemption. A test
(`.github/scripts/test_no_force_push.py`) fails if any shipped action or
template gains a forced push.
- **Generic.** The actions know nothing about MegaETH: the version file and
its format, the changelog path, the branch prefix, the app identity and
every publish destination are inputs. The templates carry this
Expand All @@ -97,8 +99,9 @@ branch `chore/release-candidate-1.2.3`: the version file bumped, whatever
changelog entry `## v1.2.3` drafted from the merged PR titles since the last
`v*` tag (grouped by Conventional Commit type, with PR links), and the
previous release's entry synced from its tag. Review it like any PR; fix
wording in it if the generated entry needs work. A stale candidate branch
from an earlier attempt is dropped first, unless its PR is still open.
wording in it if the generated entry needs work. Re-dispatching the
candidate supersedes this PR (closed with a comment, a fresh branch and PR
opened, nothing force-pushed), so edits made on it are not carried over.

**2. Cut.** Merging the candidate PR runs the `cut` job, which creates
`release-v1.2.3` at the merge commit. No tag yet. From here, fixes for the
Expand Down Expand Up @@ -178,8 +181,10 @@ Step 1 then works differently, and steps 3 and 4 are unchanged:
syncs the entry back from the tag the way it does after any release. The
next version must still be newer than `v1.2.3`.

The version must be newer than the newest `v*` tag in the repository, as
usual. That covers two cases: an earlier commit on the current default
The version must be newer than the newest `vX.Y.Z` tag in the repository, as
usual (tags such as `v1.2.0-rc1` are not releases and are ignored). Publishing
re-checks this when the settle PR merges, so a line that a newer release
overtook while its settle PR was open is refused, never tagged. That covers two cases: an earlier commit on the current default
branch, and a hotfix of the latest tag (new `release-vX.Y.Z` cut there,
candidate PR aimed at that branch). It still does not patch an older line
that a higher tag has already passed.
Expand Down Expand Up @@ -422,12 +427,17 @@ default branch has. Fix: cherry-pick the file onto the release branch through
its own PR, then settle. (A tag's tree is final: a target that was missing
or broken at a tag cannot be re-run for that tag; the next release gets it.)

**A stale candidate branch exists** from an earlier attempt: the candidate
action deletes it before pushing, unless a PR on it is still open, in which
case that PR is updated in place with a force push of its branch. In a
repository whose rulesets ban force pushes, close the open candidate PR
before re-dispatching; the action then drops the branch and opens a fresh
PR.
**A candidate is re-dispatched** (a fix landed, or the first attempt failed
part-way): the fresh candidate supersedes the old one. An open candidate PR
for that version is closed with a comment saying so, its branch is deleted,
the new candidate commit is pushed as a new branch and a fresh PR is opened.
Nothing is force-pushed, so there is nothing to close by hand first. A retry
converges from wherever the last attempt stopped (a PR already closed, a
branch already gone, a branch pushed with no PR yet), and a concurrent run
that pushed the branch or opened the PR first is kept rather than replaced:
the result is always one branch and one open PR. The branch is deleted on
merge only by the repository's "Automatically delete head branches" setting;
without it, a merged candidate's branch stays, unused.

**Two runs of the same stage overlap.** They queue, never cancel: the
candidate's `propose` and `cut`, and `release-publish`'s `publish`, carry
Expand Down
Loading
Loading