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
134 changes: 6 additions & 128 deletions .github/actions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -347,131 +347,9 @@ pr-review:
cancel-in-progress: true
```

## Release actions

Three composite actions implement the org release flow — trunk-first
candidate, environment-gated settle (or settle-by-PR), publish-once. They are language-agnostic: the only
repo-specific inputs are where the version lives (`version_file` +
`version_pattern`: `plain`, `toml`, `json`), an optional `bump_command` for
whatever else must move with the version (`cargo update --workspace` for a
lockfile; path-dependency versions in a Cargo workspace — it runs with
`OLD_VERSION`/`NEW_VERSION` set, and the calling job installs the toolchain it
needs first), and the changelog path. Builds and
artifact uploads are not part of them: a repo that ships something composes
its publish targets in an `on-release.yml` from the extensions below, fired
by the GitHub Release that `release-publish` creates.

| Action | Trigger in the consumer | Does |
|---|---|---|
| `release-candidate` `stage: propose` | `workflow_dispatch` on the default branch | bumps `version_file`, runs `bump_command` (lockfile, path-dep versions), drafts this release's changelog entry under `## vX.Y.Z` (dated at settle), syncs the previous release's entry from its tag, opens `chore/release-candidate-vX.Y.Z` PR |
| `release-candidate` `stage: cut` | that PR merging | creates `release-vX.Y.Z` at the merge commit |
| `release-settle` | `workflow_dispatch` with version + tip SHA | guards, warns if the tip lacks a workflow the default branch has, regenerates the entry up to the tip and stamps the date, opens `chore/release-settle-vX.Y.Z` PR onto the release branch (a re-run on a new tip closes the previous settle PR and opens a fresh one; nothing is ever force-pushed) — or, with `settle_mode: direct` and an authorised actor (`settlers`), commits it straight to the branch and publishes at once |
| `release-publish` | the settle PR merging (PR mode only; in direct mode `release-settle` runs it in the same job) | annotated tag at the merge commit (refuses if it exists or the branch drifted), GitHub Release with the entry as notes |

The changelog has one owner per phase and is never back-merged: the candidate
PR drafts the entry on the default branch and syncs the *previous* release's
final entry from its tag; the settle PR finalises the entry (date, drift) on
the release branch; the tag and the GitHub Release carry the final text. Between
a release and the next candidate the default branch's copy of the latest entry
carries no date — the GitHub Release is authoritative in that window.

`workflow-templates/release-*.yml` are the reference callers; they show up
under "New workflow → By megaeth-labs" in every org repo. Consumers track the
actions at `@main`, like every other action here: a merge to this repo reaches
all product release flows at once, which is what `actions-test.yml` and the
`main` ruleset are for.

Requirements in the consumer repo:

- Org variable `MEGA_MAXWELL_CLIENT_ID` (the Maxwell app's client id) / secret `MEGA_MAXWELL_PK`. PRs
and tag pushes must come from an App token: `GITHUB_TOKEN` does not trigger
downstream workflows.
- The human gate for creating a tag is the settle dispatch: the templates
put `environment: release` on the settle job, so its required reviewers
approve the run before it starts (create the environment with reviewers,
`prevent self-review` off if the dispatcher approves, and a deployment
branch policy of the default branch only), and the action's
`settlers: admin` checks the dispatching actor again. A repo that settles
by PR instead (no `settle_mode`) has the reviewed settle PR as its gate:
the `release-*` branch ruleset below requires one.
- A tag ruleset for `v*` (no creation/deletion/force-push) with the app as a
bypass actor, so `release-publish` is the only tag creator.
- A branch ruleset for `release-*` requiring PRs, with the app as a bypass
actor: nothing but the app's settle commit reaches a release branch
without a reviewed PR. In PR mode, drift after a settle PR is opened is
caught by `release-publish` itself (it refuses if the branch tip at merge
is not the settled SHA), so no "up to date" status check is needed.
- `gh` and `python3` on the runner (any GitHub-hosted image).
- The cut and publish stages only accept PRs opened by the app identity
(`pr_author`, default `mega-maxwell[bot]`); the templates also gate the
jobs' `if:` on it, so a hand-made `chore/release-*` branch merged by a
collaborator never reaches the app-token steps.
- A `concurrency` group on every release workflow, always with
`cancel-in-progress: false` (the templates carry them): one proposal at a
time, one cut at a time, one settle per version, one publish per release
branch, one `on-release` run per tag. Each of them ends in a push, a tag or
an upload that must never be cancelled half-way, and none of them is
atomic with its own guard, so a second run queues rather than overlaps.
The groups on the `pull_request`-triggered jobs (`cut`, `publish`) are
job-level, not workflow-level: every PR closing on that branch starts the
workflow, and GitHub keeps one pending run per group, so a workflow-wide
group would let an unrelated closure evict a queued release run. A
skipped job holds no slot in a job-level group.
- Every workflow the release depends on must be on the tagged commit, not
just on the default branch: `release-publish.yml` runs from the settle
PR's merge into the release branch, and `on-release.yml` from the tag's
tree (that is how `release` events resolve a workflow). A release branch
cut before such a file landed needs it cherry-picked through its own PR
before settlement is dispatched (or, in PR mode, before the settle PR
merges); `release-settle` warns when the settled
commit lacks a workflow the default branch has, because the alternative is
a Release with nothing attached and no failed run.

Release notes are generated from commit subjects between the previous `v*`
tag and the settled commit, grouped by Conventional Commit type with PR links
from `(#N)` suffixes. The pure text logic lives in
`release-tools/release_tools.py` and is unit-tested by `actions-test.yml`.

## Release extensions (publish targets)

The core above ends with a tag and a GitHub Release. Products that ship
something run their targets from `on: release: published`, composed in a
repo-owned workflow stamped from `workflow-templates/on-release.yml`. Every
target is a step with a `dry_run` input, and the template threads one
`workflow_dispatch` flag into all of them, so a whole release can be
rehearsed on an existing tag with nothing published, uploaded or attached.

| Action | Publishes | Idempotency on re-run |
|---|---|---|
| `release-publish-rust-crates` | an explicit crate list to crates.io at the release version — one `cargo publish -p … -p …` (Cargo ≥ 1.90 orders and waits); polls the index afterwards | crates already at the version are skipped |
| `release-upload-artifact` | one file to Artifact Registry (generic) or a GCS bucket; all destinations are inputs | identical file already there → `exists`; different → fails, never overwrites |
| `release-assets` | files + `SHA256SUMS` on the GitHub Release | `--clobber` |
| `release-verify-version` | nothing — runs a version probe (`my-binary --version`) between build and publish and requires `<name> <version>` (or a custom `expected`); a mismatch warns on a dry run and stops a release, a probe that cannot run stops both | n/a |

Credentials are the caller's: `CARGO_REGISTRY_TOKEN` for crates.io;
`google-github-actions/auth` (service-account key or WIF) before an upload
step. Hold them in a **`publish` environment** whose deployment policy
allows only `v*` tags, and declare `environment: publish` on the publish
jobs (the template does): the secrets are then readable only by a run whose
ref is a release tag — the `release: published` run, or a rehearsal
dispatched on a tag ref — never by a branch build. Give that environment
no required reviewers: the human gate is the settle approval (the `release`
environment), and a second click before the uploads only holds them up.
`gcloud` and `gh` are on GitHub-hosted runners; the extensions are not
meant for the TKE image.

What every `on-release.yml` 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
ref, so that ref is the only thing the run may build and publish.
- `environment: publish` on every job that reads a publish credential.
- A workflow-level `concurrency` group keyed on `github.ref` with
`cancel-in-progress: false`: uploads must not overlap and must not be
cancelled mid-flight.
- `release-verify-version` between the build and the first publish step for
each binary. Never wrap a probe of the built artifact in
`2>/dev/null || echo …`: that turns a binary that cannot load into a green
step and a published download.
- The file itself on the tagged commit (see the release-branch requirement
above).
## Release pipeline

The `release-*` actions and the `workflow-templates/release-*.yml` /
`on-release.yml` callers are documented in [RELEASE.md](RELEASE.md): what the
pipeline does and why, one release step by step, installation in a new
repository, options, publish targets, and recovery.
Loading
Loading