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
2 changes: 1 addition & 1 deletion .github/actions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,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 changelog entry (dated at settle) from the commits since the previous tag, syncs the previous release's entry from its tag, and opens a `chore/release-candidate-vX.Y.Z` PR; `stage: cut`, run when that PR merges, creates `release-vX.Y.Z` at the merge commit. No tag is created at either stage — tags come from release-publish, once, at settlement. Run as a step in a job the consumer owns; the consumer checks the repository out first (`fetch-depth: 0`, `persist-credentials: false`). Guide: .github/actions/RELEASE.md in megaeth-labs/.github. |
| [`release-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-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. |
Expand Down
58 changes: 56 additions & 2 deletions .github/actions/RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@ The choices that shape it:
default branch through an ordinary reviewed PR; the release branch is cut
from that merge. Fixes for a release go to the release branch by PR and
must also land on the default branch, because the next candidate is cut
from there.
from there. When the release is an earlier commit rather than the tip
(`base_commit`), the same reviewed PR lands on the release branch instead,
and the default branch is left alone — see [Releasing a commit that is not
the tip](#releasing-a-commit-that-is-not-the-tip).
- **No release-candidate tags.** A tag is created exactly once, at
settlement, on the commit that ships. Tags are immutable: the `v*` tag
ruleset forbids updating or deleting them, and nothing but the app can
Expand Down Expand Up @@ -134,6 +137,52 @@ decision; `release-publish.yml` then tags the merge commit and publishes the
Release, refusing if the branch moved since the PR was made (the merge's first
parent must be the settled SHA) or the PR was not opened by the app.

### Releasing a commit that is not the tip

`main` moves while a release is being prepared. To ship an earlier commit,
name it:

```sh
gh workflow run release-candidate.yml --ref main \
-f version=1.2.3 -f base_commit="$(git rev-parse origin/main~4)"
```

The commit may be given as anything the checkout resolves — a full SHA, a
tag, `origin/main~4` — but it must be reachable from the default branch: a
release ships code that went through trunk. Note that `v*` release tags are
not reachable from it, because settlement commits the dated changelog on the
release branch; to release from an old release line, name a commit on the
default branch, not its tag.

Step 1 then works differently, and steps 3 and 4 are unchanged:

- `propose` creates `release-v1.2.3` at that commit itself, rather than
leaving it to `cut`, and opens the candidate PR **against that branch**.
The PR cannot target the default branch: merging it there would take the
commits this release is deliberately leaving out.
- The changelog entry is drafted over what actually ships, the commits from
the previous tag up to the named commit, not up to the tip.
- Merging the candidate PR puts the version bump and the entry on
`release-v1.2.3`. There is no step 2: the `cut` job does not fire, because
the PR's base is the release branch and the workflow's `pull_request`
trigger filters on the default branch. The `release branch` ruleset governs
this PR, so it needs its approving review and a squash merge, like any
other change to a release branch.
- **The default branch is left untouched.** Its version file keeps the old
version, and it gains no changelog entry until the next candidate, which
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, so this releases an earlier commit on the current line. It does not
patch an older line that a higher tag has already passed; there is no
backport flow.

If an attempt fails after the branch was created — a broken `bump_command`,
say — re-dispatching the same version and commit reuses the branch, because
it is still sitting on exactly that commit with nothing merged into it. Any
other mismatch is refused, and the branch has to be deleted by hand.

## Installing it in a repository

Everything below was done for mega-agents, mega-evm, stateless-validator and
Expand Down Expand Up @@ -267,6 +316,11 @@ toolchain it needs. Examples in use:
| `direct` (the templates) | the dispatch, approved through the `release` environment | the environment's required reviewers; the app bypasses the release-branch ruleset. `settlers` (default `any`) may additionally name who can start a settle: comma-separated logins and/or `admin` (the dispatcher must have admin permission, checked with the job token) |
| `pr` (the action default) | merging the settle PR | the release-branch ruleset requires a reviewed PR; `release-publish.yml` present on the release branch |

**Which commit is released** (`base_commit`, candidate only): empty, the
default, releases the tip of the default branch through the trunk-first flow.
A commit-ish releases that commit instead — see
[Releasing a commit that is not the tip](#releasing-a-commit-that-is-not-the-tip).

**Labels** (`pr_labels`, candidate and settle): for repositories whose
label gates apply to the app's PRs.

Expand Down Expand Up @@ -400,7 +454,7 @@ What each action does, in one line:

| Action | Trigger in the consumer | Does |
|---|---|---|
| `release-candidate` `stage: propose` | `workflow_dispatch` on the default branch | bumps `version_file`, runs `bump_command`, drafts this release's changelog entry under `## vX.Y.Z`, syncs the previous release's entry from its tag, opens `chore/release-candidate-X.Y.Z` |
| `release-candidate` `stage: propose` | `workflow_dispatch` on the default branch | bumps `version_file`, runs `bump_command`, drafts this release's changelog entry under `## vX.Y.Z`, syncs the previous release's entry from its tag, opens `chore/release-candidate-X.Y.Z`; with `base_commit`, cuts `release-vX.Y.Z` at that commit first and aims the PR there |
| `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; `direct`: commits it to the release branch and publishes at once; `pr`: opens `chore/release-settle-vX.Y.Z` (a re-run closes the previous settle PR and opens a fresh one) |
| `release-publish` | the settle PR merging, or `release-settle` in direct mode | annotated tag at the commit (refuses if it exists or the branch drifted), GitHub Release with the entry as notes, marked latest |
Expand Down
12 changes: 8 additions & 4 deletions .github/actions/release-candidate/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
`uses: megaeth-labs/.github/.github/actions/release-candidate@main`

<!-- generated: description -->
Start a release (trunk-first). `stage: propose` bumps the version file on the default branch, drafts this release's changelog entry (dated at settle) from the commits since the previous tag, syncs the previous release's entry from its tag, and opens a `chore/release-candidate-vX.Y.Z` PR; `stage: cut`, run when that PR merges, creates `release-vX.Y.Z` at the merge commit. No tag is created at either stage — tags come from release-publish, once, at settlement. Run as a step in a job the consumer owns; the consumer checks the repository out first (`fetch-depth: 0`, `persist-credentials: false`). Guide: .github/actions/RELEASE.md in megaeth-labs/.github.
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.
<!-- /generated -->

Family: [Release pipeline](../RELEASE.md).
Expand All @@ -16,6 +16,7 @@ Family: [Release pipeline](../RELEASE.md).
| `stage` | yes | | `propose` (on workflow_dispatch) or `cut` (on the candidate PR merging). |
| `token` | yes | | Token that authors the PR and creates the branch. Must be a GitHub App installation token (e.g. the Maxwell app) so the PR triggers CI; the job's GITHUB_TOKEN does not. |
| `version` | no | | Version to release, X.Y.Z or vX.Y.Z. Required for `propose`. |
| `base_commit` | no | | The commit to release, for `propose`, when it must not be the tip of the default branch. Any commit-ish the checkout can resolve (a full SHA, a tag, `origin/main~3`), and it must be reachable from the default branch — a release still ships code that went through trunk. Empty (the default) is the trunk-first flow: the candidate PR merges into the default branch and `cut` creates the release branch at that merge commit. Set, `propose` creates the release branch at this commit and opens the candidate PR against that branch, because merging it into the default branch would take the commits this release is leaving out; the default branch is then untouched and `cut` never runs. The version must still be newer than the newest `v*` tag in the repository. |
| `version_file` | yes | | File holding the version (VERSION, Cargo.toml, pyproject.toml, package.json). |
| `version_pattern` | no | `plain` | How the version is stored in `version_file`: plain, toml or json. |
| `bump_command` | no | | Optional shell command run after `version_file` is rewritten, for anything else that must move with the version: lockfiles (`cargo update --workspace`), path-dependency versions, generated files. Runs with `OLD_VERSION` and `NEW_VERSION` in the environment, in the repository root, under `bash -euo pipefail`. Whatever it changes is committed with the bump. Install any toolchain it needs in the calling job before this action. |
Expand All @@ -34,7 +35,8 @@ Family: [Release pipeline](../RELEASE.md).
|---|---|
| `version` | Normalised X.Y.Z. |
| `pr_url` | `propose`: URL of the candidate PR. |
| `release_branch` | `cut`: the release branch created. |
| `release_branch` | The release branch: created by `cut`, or by `propose` when `base_commit` is set. |
| `base_commit` | `propose` with `base_commit`: that commit-ish resolved to a full SHA. |
<!-- /generated -->

## What it runs
Expand All @@ -43,6 +45,7 @@ Family: [Release pipeline](../RELEASE.md).
1. Resolve version
1. Configure git auth
1. Guard (propose) *(only if `inputs.stage == 'propose'`)*
1. Cut the release branch at the base commit (propose) *(only if `inputs.stage == 'propose' && inputs.base_commit != ''`)*
1. Bump version file *(only if `inputs.stage == 'propose'`)*
1. Run bump command *(only if `inputs.stage == 'propose' && inputs.bump_command != ''`)*
1. Draft changelog *(only if `inputs.stage == 'propose' && inputs.changelog_file != ''`)*
Expand All @@ -60,12 +63,13 @@ Family: [Release pipeline](../RELEASE.md).
- `release candidates start from the default branch ($default); this run is on $GITHUB_REF_NAME`
- `$VERSION is not newer than the latest tag ${latest:-<none>}`
- `tag v$VERSION already exists`
- `branch ${PREFIX}${VERSION} already exists`
- `base_commit $BASE_COMMIT is not a commit in this checkout; check the repository out with fetch-depth: 0`
- `base_commit $base is not reachable from the default branch ($default); a release must ship code that landed on trunk`
- `branch $branch already exists`
- `after bump_command, $FILE reads $actual, expected $NEW_VERSION`
- `cut runs on a merged candidate PR (pull_request closed, merged == true)`
- `candidate PR #$PR_NUMBER was opened by`
- `$FILE at $MERGE_SHA says $actual, expected $VERSION`
- `branch $branch already exists`
<!-- /generated -->

## Example
Expand Down
Loading
Loading