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
1 change: 1 addition & 0 deletions .github/actions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ input, output, step and error tables are generated from its `action.yml`.
| [`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. |
| [`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. |
<!-- /generated -->
Expand Down
34 changes: 33 additions & 1 deletion .github/actions/RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ options exist, and what to do when something goes wrong.

The code is next to this file: `release-candidate/`, `release-settle/`,
`release-publish/` (the core), `release-verify-version/`,
`release-publish-rust-crates/`, `release-upload-artifact/`, `release-assets/`
`release-publish-rust-crates/`, `release-upload-artifact/`, `release-assets/`,
`release-snapshot/` (the snapshot bookends)
(the publish targets), and `release-tools/` (the text helpers they share, unit
tested by `actions-test.yml`). The reference callers a repository stamps from
are in `workflow-templates/` at the repository root. `README.md` beside this
Expand Down Expand Up @@ -313,6 +314,35 @@ and uploads `mega-evme` to the registry and the Release page;
stateless-validator uploads both of its binaries to the registry and the
Release page; mega-agents and salt ship nothing beyond the Release.

## Snapshots (`snapshot-publish.yml`)

Not every component is versioned. Deployments pin some binaries by commit —
`version: latest`, `profile: release`, and the commit — and want "build this
commit and push it", with no tag, no GitHub Release and no changelog. That
is the snapshot flow: one dispatch-only workflow, `snapshot-publish.yml`,
bookended by `release-snapshot`:

1. `release-snapshot` `stage: resolve` turns the dispatch input `ref` (or
the run's own commit) into the full commit, refuses it unless it is
reachable from `allowed_branches`, and hands out the registry
coordinates: `version` = the commit, `path` = `<profile>/<label>`
(`release/latest`).
2. The repository's own build steps, then `release-upload-artifact` per
file with those coordinates — the layout the tagged pipeline writes,
`latest` in place of the tag, so deployment tooling reads both the same
way. Re-runs on the same commit are no-ops for files already there.
3. `release-snapshot` `stage: summary` writes the run summary: every
upload with its checksum, and the manifest entry to pin; optionally a
`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 branch 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 keeps `publish` for `v*` tags
and gives snapshots their own environment. Nothing else is needed: no
version file, no changelog, no rulesets, no app.

## Operations and recovery

**The release branch moved after settling** (a fix landed): run settle
Expand Down Expand Up @@ -374,6 +404,8 @@ What each action does, in one line:
| `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 |
| `release-snapshot` `stage: resolve` | `workflow_dispatch` of `snapshot-publish.yml` | resolves `ref` (or the run's commit) to a full SHA, refuses it unless reachable from `allowed_branches`, outputs the registry `version` (the commit) and `path` (`<profile>/<label>`) |
| `release-snapshot` `stage: summary` | after the uploads in the same job | run summary with every upload, its checksum and the manifest entry to pin; optional `snapshot/<package>` commit statuses |

Release notes are generated from commit subjects between the previous `v*`
tag and the settled commit, grouped by Conventional Commit type, with PR
Expand Down
135 changes: 135 additions & 0 deletions .github/actions/release-snapshot/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# Release · Snapshot

`uses: megaeth-labs/.github/.github/actions/release-snapshot@main`

<!-- generated: description -->
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.
<!-- /generated -->

Family: [Release pipeline](../RELEASE.md).

## Inputs

<!-- generated: inputs -->
| Input | Required | Default | Description |
|---|---|---|---|
| `stage` | yes | | `resolve` (before the build) or `summary` (after the uploads). |
| `ref` | no | | Branch, tag or commit to publish. Empty: the run's own commit (`github.sha`). |
| `allowed_branches` | no | | Comma-separated branches the commit must be reachable from (`origin/<branch>` after a full checkout). Empty: no guard. |
| `profile` | no | `release` | Build profile that names the first path segment (`release`, `profiling`, …). |
| `label` | no | `latest` | Second path segment. Snapshots use `latest`; the deployment manifest pins the commit. |
| `commit` | no | | (summary) The commit that was published — the `commit` output of the resolve stage. |
| `entries` | no | | (summary) One line per uploaded file: `<package> <uri> <sha256> <status>`, the outputs of `release-upload-artifact` (`status` is `uploaded`, `exists` or `dry-run`). |
| `record_status` | no | `false` | (summary) `true`: create a commit status `snapshot/<package>` on the commit for every entry, linking to this run; needs `token` with `statuses: write`. Dry runs record nothing. |
| `token` | no | `${{ github.token }}` | (summary) Token for the commit statuses. |
| `dry_run` | no | `false` | `true`: the summary says so and no status is recorded. |
<!-- /generated -->

## Outputs

<!-- generated: outputs -->
| Output | Description |
|---|---|
| `commit` | (resolve) The full commit SHA that will be built. |
| `short` | (resolve) Its 12-character prefix. |
| `version` | (resolve) The registry version to upload under — the commit. |
| `path` | (resolve) The registry path to upload under — `<profile>/<label>`. |
| `describe` | (resolve) `git describe --tags --always` of the commit, for logs and binaries. |
| `branch` | (resolve) The first of `allowed_branches` the commit was found on (empty when no guard). |
| `packages` | (summary) Comma-separated package names from `entries`. |
| `manifest` | (summary) The manifest entries as YAML text, `<package>` → `commit`, `version`, `profile` per package, for a follow-up that pins them somewhere. |
<!-- /generated -->

## What it runs

<!-- generated: steps -->
1. Resolve the commit *(only if `inputs.stage == 'resolve'`)*
1. Write the summary *(only if `inputs.stage == 'summary'`)*
<!-- /generated -->

## Errors it reports

<!-- generated: errors -->
- `not a git checkout; check the repository out first (fetch-depth: 0)`
- `profile must be a plain word, got`
- `label must be a plain word, got`
- `cannot resolve`
- `$ref (${commit:0:12}) is not on any allowed branch ($ALLOWED); refusing to publish a commit that has not landed there`
- `commit must be the full SHA from the resolve stage, got`
- `entries is empty; pass one`
- `malformed entry:`
<!-- /generated -->

## Example

The two bookends of `workflow-templates/snapshot-publish.yml`; the build and
the uploads sit between them:

```yaml
- uses: actions/checkout@v4
with:
fetch-depth: 0 # the branch guard needs history

- uses: megaeth-labs/.github/.github/actions/release-snapshot@main
id: snapshot
with:
stage: resolve
ref: ${{ inputs.ref }} # empty: this run's commit
allowed_branches: main # refuse anything not landed on main
profile: release

- run: git checkout --quiet ${{ steps.snapshot.outputs.commit }}
- run: cargo build --release -p my-binary

- uses: google-github-actions/auth@v3
with:
credentials_json: ${{ secrets.GCP_AUTH_KEY }}

- uses: megaeth-labs/.github/.github/actions/release-upload-artifact@main
id: upload
with:
file: target/release/my-binary
kind: artifact-registry
project: my-gcp-project
location: my-region
repository: my-generic-repository
package: my-binary
version: ${{ steps.snapshot.outputs.version }} # the commit
path: ${{ steps.snapshot.outputs.path }} # release/latest

- uses: megaeth-labs/.github/.github/actions/release-snapshot@main
with:
stage: summary
commit: ${{ steps.snapshot.outputs.commit }}
entries: |
my-binary ${{ steps.upload.outputs.uri }} ${{ steps.upload.outputs.sha256 }} ${{ steps.upload.outputs.status }}
record_status: "true" # needs statuses: write
```

The run summary then carries the table of uploads and the manifest entry a
deployment repository pins:

```yaml
my-binary:
commit: "<full sha>"
version: "latest"
profile: "release"
```

## Notes

- A snapshot is addressed by commit: the registry version *is* the commit,
and `<profile>/<label>` (default `release/latest`) is the path inside it —
the same layout the tagged pipeline writes with the tag in place of
`latest`. Re-running on the same commit is a no-op for files already
there (`release-upload-artifact` compares hashes).
- The environment on the job authorises the ref the workflow was
*dispatched* on, not the commit `ref` selects; `allowed_branches` closes
that gap by refusing any commit not reachable from those branches.
- `resolve` prefers `origin/<ref>` when `ref` names a branch, then anything
git can resolve, then a fetch of `ref` from origin.
- No tag, no GitHub Release, no changelog. Traceability comes from the
environment's deployment record on the commit, the run summary, and —
with `record_status` — a `snapshot/<package>` commit status linking to
the run. A commit that must become a versioned release goes through the
tagged pipeline.
Loading
Loading