Skip to content
Closed
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
10 changes: 8 additions & 2 deletions .github/ci-credentials.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,17 @@ The Git dependencies in the current lockfile are public; Cargo does not need a P

| Configuration | Scope | Consumer |
| --- | --- | --- |
| `MEGA_MAXWELL_CLIENT_ID` variable | Organization | Release candidate, settle, publish, and PR review App tokens |
| `MEGA_MAXWELL_PK` secret | Organization | Same App-token jobs |
| `MEGA_GUTENBERG_CLIENT_ID` variable | Organization | Release App (mega-gutenberg) tokens in `release-candidate.yml` and `release-settle.yml`, the only release identity |
| `MEGA_GUTENBERG_PK` secret | Organization, access granted to this repository | Same release App-token jobs |
| `MEGA_MAXWELL_CLIENT_ID` variable | Organization | PR review App token (CI App, mega-maxwell); never used by a release workflow |
| `MEGA_MAXWELL_PK` secret | Organization | Same PR review job |
| `CLAUDE_CODE_OAUTH_TOKEN` secret | Organization | Claude workflows |
| `CODECOV_TOKEN` secret | Repository | Coverage report upload |
| `GCP_AUTH_KEY` secret | `publish` environment | Release binary upload to Artifact Registry |
| `release` environment | Repository | Gates release-settle's publish job (annotated tag and GitHub Release); required reviewer, deployment policy for `main`, `release-v*` and `v*` tags |

The release flow's dependency token for `cargo update --workspace` is the read-only job token, which is enough while every Git dependency stays public.
`app-token-lint.yml` enforces the App-token rules (which App may mint what, and where) on every workflow change.

Claude jobs rely on the [pinned action's credential replacement](https://github.com/anthropics/claude-code-action/blob/5ef2e550a465a721f4f45e4a7d3c340c873e1dcc/src/github/operations/git-config.ts), which removes checkout's authorization header and configures Git with the action's own token before Claude starts.
In `interactive`, that write token remains in the origin URL in `.git/config` while `Bash(cargo:*)` is allowed, so a build script or proc-macro from a PR branch could read it; `persist-credentials: false` alone would not remove this risk.
Expand Down
27 changes: 27 additions & 0 deletions .github/workflows/app-token-lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: App Token Lint

# Fails a pull request that mints a GitHub App token the wrong way: a CI-app
# token with write permissions it must not have, the CI app in a release
# workflow, or a release-app (mega-gutenberg) token outside the `release`
# environment. Read-only; no secrets. The rules are listed in the guide.
#
# Guide: https://github.com/megaeth-labs/.github/blob/main/.github/actions/app-token-lint/README.md

on:
pull_request:
paths: [".github/workflows/**"]
push:
branches: [main]

permissions:
contents: read

jobs:
app-token-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false

- uses: megaeth-labs/.github/.github/actions/app-token-lint@main
3 changes: 3 additions & 0 deletions .github/workflows/claude.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ jobs:
private-key: ${{ secrets.MEGA_MAXWELL_PK }}
owner: megaeth-labs
repositories: stateless-validator
permission-contents: read
permission-pull-requests: write
permission-issues: write

- uses: actions/checkout@v4
with:
Expand Down
11 changes: 6 additions & 5 deletions .github/workflows/on-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,19 @@ name: On Release

# Publish target for a stateless-validator release: the two binaries as
# downloads on the GitHub Release page, with a SHA256SUMS file. Runs when the
# release pipeline publishes the Release (release-settle creates it right
# after the tag), and on demand for a rehearsal on a tag ref:
# release pipeline publishes the Release (release-settle's publish job creates
# it right after the tag, as the release app mega-gutenberg), and on demand for
# a rehearsal on a tag ref:
# gh workflow run on-release.yml --ref vX.Y.Z -f dry_run=true
# The tag being released is always the run's own ref; there is no tag input.
#
# This file must exist on the release branch: a `release` event resolves the
# workflow from the tagged commit's tree, not from the default branch, and the
# tag sits on the release branch. A branch cut from a default branch that has
# this file carries it; a branch cut before it landed needs it cherry-picked
# (via its own PR — the `release-*` ruleset requires one) before release-settle
# is dispatched, or the Release publishes with nothing attached and no failed
# run; release-settle warns when the tip lacks a workflow main has.
# (via its own PR — the `release-*` ruleset requires one) before the settle PR
# is merged, or the Release publishes with nothing attached and no failed run;
# release-settle warns when the settled commit lacks a workflow main has.
# A rehearsal likewise only works on a tag whose tree has this file.
#
# Targets, fed by one build so the Release page and the registry carry the
Expand Down
172 changes: 149 additions & 23 deletions .github/workflows/release-candidate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,24 @@ name: Release Candidate

# Trunk-first release start. Dispatch with a version: the shared action bumps
# the version file on the default branch, drafts the changelog entry, and
# opens a candidate PR. When that
# PR merges, the `cut` job creates `release-vX.Y.Z` at the merge commit. No
# tag is created here — see release-settle / release-publish.
# opens a candidate PR. When that PR merges, the push to the default branch
# starts this workflow again: `route` recognises the merged candidate PR and
# `cut` creates `release-vX.Y.Z` at the merge commit. No tag is created here;
# see release-settle.
#
# To release a commit that is not the tip of the default branch, dispatch with
# `base_commit` as well: `propose` creates `release-vX.Y.Z` at that commit and
# aims the candidate PR at it, so that PR never touches the default branch and
# the `cut` job below does not run.
# `route` never selects `cut` for it.
#
# Every write is made by the release GitHub App (mega-gutenberg). Store
# MEGA_GUTENBERG_CLIENT_ID as an organization variable and MEGA_GUTENBERG_PK
# as an organization secret with access granted to this repository.
# Branch and PR jobs need no environment; tagging and
# publishing are review-gated in release-settle. `route` is read-only and uses
# the job token.
#
# Guide: https://github.com/megaeth-labs/.github/blob/main/.github/actions/RELEASE.md

on:
workflow_dispatch:
Expand All @@ -23,8 +33,7 @@ on:
required: false
type: string
default: ""
pull_request:
types: [closed]
push:
branches: [main]

permissions:
Expand All @@ -34,60 +43,175 @@ jobs:
propose:
if: github.event_name == 'workflow_dispatch'
# One proposal at a time: two dispatches would race on the candidate
# branch. Job-level, not workflow-level: every PR closing on the default
# branch also starts this workflow, and a workflow-wide group would let
# that noise evict a queued release run (one pending run per group).
# branch. Job-level, not workflow-level: every push to the default branch
# also starts this workflow, and a workflow-wide group would let that
# noise evict a queued release run (one pending run per group).
# A skipped job holds no slot in a job-level group.
concurrency:
group: ${{ github.workflow }}-propose
cancel-in-progress: false
runs-on: ubuntu-latest
steps:
- uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3
id: app-token
id: release-token
with:
client-id: ${{ vars.MEGA_MAXWELL_CLIENT_ID }}
private-key: ${{ secrets.MEGA_MAXWELL_PK }}
client-id: ${{ vars.MEGA_GUTENBERG_CLIENT_ID }}
private-key: ${{ secrets.MEGA_GUTENBERG_PK }}
owner: megaeth-labs
repositories: stateless-validator
permission-contents: write
permission-pull-requests: write
permission-workflows: write

- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
persist-credentials: false

# The bump command runs cargo.
- uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1
with:
cache: false

- uses: megaeth-labs/.github/.github/actions/release-candidate@main
with:
stage: propose
token: ${{ steps.app-token.outputs.token }}
token: ${{ steps.release-token.outputs.token }}
version: ${{ inputs.version }}
base_commit: ${{ inputs.base_commit }}
version_file: Cargo.toml
version_pattern: toml
changelog_file: CHANGELOG.md
# Cargo.lock carries the workspace crate versions too.
bump_command: cargo update --workspace
# Every Git dependency in Cargo.lock is public (see
# .github/ci-credentials.md), so the read-only job token is enough;
# it never sees the release token.
dependency_token: ${{ github.token }}

# Only trunk candidates follow trunk pushes; base_commit candidates belong
# to their release branch and must never pick up changes from trunk.
refresh-candidates:
if: github.event_name == 'push' && github.ref_name == github.event.repository.default_branch
runs-on: ubuntu-latest
permissions:
pull-requests: read
outputs:
prs: ${{ steps.candidates.outputs.prs }}
steps:
- id: candidates
env:
GH_TOKEN: ${{ github.token }}
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
run: |
prs="$(gh api --method GET --paginate --slurp \
"repos/$GITHUB_REPOSITORY/pulls" \
-f state=open -f base="$DEFAULT_BRANCH" -f per_page=100 \
--jq '[.[][] | select(.head.ref | startswith("chore/release-candidate-")) | select(.head.repo.full_name == .base.repo.full_name) | .number]')"
echo "prs=$(jq -c . <<< "$prs")" >> "$GITHUB_OUTPUT"

refresh:
needs: refresh-candidates
if: needs.refresh-candidates.outputs.prs != '[]'
strategy:
fail-fast: false
matrix:
pr: ${{ fromJSON(needs.refresh-candidates.outputs.prs) }}
concurrency:
group: ${{ github.workflow }}-refresh-${{ matrix.pr }}
cancel-in-progress: false
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
steps:
# Recheck after queueing: a candidate may have closed or changed base
# while another refresh held its concurrency slot.
- id: candidate
env:
GH_TOKEN: ${{ github.token }}
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
PR_NUMBER: ${{ matrix.pr }}
run: |
version="$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER" \
--jq 'select(.state == "open") | select(.base.ref == env.DEFAULT_BRANCH) | select(.head.repo.full_name == .base.repo.full_name) | .head.ref | select(startswith("chore/release-candidate-")) | ltrimstr("chore/release-candidate-")')"
echo "version=$version" >> "$GITHUB_OUTPUT"

- uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3
if: steps.candidate.outputs.version != ''
id: release-token
with:
client-id: ${{ vars.MEGA_GUTENBERG_CLIENT_ID }}
private-key: ${{ secrets.MEGA_GUTENBERG_PK }}
owner: megaeth-labs
repositories: stateless-validator
permission-contents: write
permission-pull-requests: write
permission-workflows: write

- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
if: steps.candidate.outputs.version != ''
with:
fetch-depth: 0
persist-credentials: false

- uses: megaeth-labs/.github/.github/actions/release-candidate@main
if: steps.candidate.outputs.version != ''
with:
stage: refresh
token: ${{ steps.release-token.outputs.token }}
version: ${{ steps.candidate.outputs.version }}
pr_number: ${{ matrix.pr }}
version_file: Cargo.toml
version_pattern: toml
changelog_file: CHANGELOG.md

# Read-only: decides from the pushed commit whether it is a merged candidate
# PR that targeted the default branch. One proposed with `base_commit` merges
# into the release branch propose already cut, and routes to nothing here.
route:
if: github.event_name == 'push'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
outputs:
action: ${{ steps.route.outputs.action }}
commit: ${{ steps.route.outputs.commit }}
pr_number: ${{ steps.route.outputs.pr_number }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
persist-credentials: false

- id: route
uses: megaeth-labs/.github/.github/actions/release-route@main
with:
kind: candidate
version_file: Cargo.toml
version_pattern: toml

cut:
if: >-
github.event_name == 'pull_request' &&
github.event.pull_request.merged == true &&
startsWith(github.event.pull_request.head.ref, 'chore/release-candidate-') &&
github.event.pull_request.user.login == 'mega-maxwell[bot]'
# One cut at a time, never cancelled: pull_request.closed does not refire.
needs: route
if: needs.route.outputs.action == 'cut'
# One cut at a time, never cancelled: a push does not refire.
# Job-level for the reason given on `propose`.
concurrency:
group: ${{ github.workflow }}-cut
cancel-in-progress: false
runs-on: ubuntu-latest
steps:
- uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3
id: app-token
id: release-token
with:
client-id: ${{ vars.MEGA_MAXWELL_CLIENT_ID }}
private-key: ${{ secrets.MEGA_MAXWELL_PK }}
client-id: ${{ vars.MEGA_GUTENBERG_CLIENT_ID }}
private-key: ${{ secrets.MEGA_GUTENBERG_PK }}
owner: megaeth-labs
repositories: stateless-validator
permission-contents: write
permission-pull-requests: read
permission-workflows: write

- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
Expand All @@ -97,6 +221,8 @@ jobs:
- uses: megaeth-labs/.github/.github/actions/release-candidate@main
with:
stage: cut
token: ${{ steps.app-token.outputs.token }}
token: ${{ steps.release-token.outputs.token }}
commit: ${{ needs.route.outputs.commit }}
pr_number: ${{ needs.route.outputs.pr_number }}
version_file: Cargo.toml
version_pattern: toml
56 changes: 0 additions & 56 deletions .github/workflows/release-publish.yml

This file was deleted.

Loading
Loading