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
41 changes: 41 additions & 0 deletions .github/workflows/app-token-lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
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/release-candidate.yml
- .github/workflows/release-settle.yml
- .github/workflows/on-release.yml
- .github/workflows/app-token-lint.yml
push:
branches: [main]
paths:
- .github/workflows/release-candidate.yml
- .github/workflows/release-settle.yml
- .github/workflows/on-release.yml
- .github/workflows/app-token-lint.yml

permissions:
contents: read

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

# claude.yml mints a CI-app (Maxwell) token without `permission-*`, so
# lint only the release workflows.
- uses: megaeth-labs/.github/.github/actions/app-token-lint@main
with:
paths: .github/workflows/release-candidate.yml .github/workflows/release-settle.yml .github/workflows/on-release.yml .github/workflows/app-token-lint.yml
8 changes: 4 additions & 4 deletions .github/workflows/on-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ name: On Release
# version is the commit SHA and the file sits under
# release/vX.Y.Z/ — <binary>/<sha>/release/vX.Y.Z/<binary>.
#
# GCP_AUTH_KEY lives in the `publish` environment, whose deployment policy
# allows only v* tag refs: a job can read it only when the run's ref is a
# release tag, never from a branch build.
# GCP_AUTH_KEY lives in the `release` environment, which admits main,
# release-v* branches and v* tags with required reviewer approval.
# The registry job reads it when publishing artifacts for a release tag.

on:
release:
Expand Down Expand Up @@ -131,7 +131,7 @@ jobs:
needs: build
runs-on: ubuntu-24.04
timeout-minutes: 15
environment: publish
environment: release
steps:
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
Expand Down
174 changes: 151 additions & 23 deletions .github/workflows/release-candidate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,27 @@ 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), which reads
# MEGA_GUTENBERG_CLIENT_ID and MEGA_GUTENBERG_PK from organization scope, so no
# job here declares an environment. The decision to release is made by merging
# the candidate PR, not by a click on the run. `route` is read-only and uses
# the job token.
#
# The candidate PR carries no `compat:` label: the compatibility level is a
# per-release judgement, so the reviewer adds it, and the required
# Compatibility Label Check re-runs on `labeled`.
#
# Guide: https://github.com/megaeth-labs/.github/blob/main/.github/actions/RELEASE.md

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

permissions:
Expand All @@ -34,60 +46,174 @@ 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

# For `bump_command`; picks up rust-toolchain.toml.
- 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 megaeth-labs git dependency (algebra, salt, mega-evm) is public,
# so the read-only job token resolves them.
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 \
"repos/$GITHUB_REPOSITORY/pulls" \
-f state=open -f base="$DEFAULT_BRANCH" -f per_page=100 \
| jq -cs '[.[][] | 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 +223,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