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
46 changes: 46 additions & 0 deletions .github/workflows/create-rc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: Create RC

on:
push:
branches:
- 'releases/v*'

permissions:
contents: write

jobs:
create-tag:
runs-on: ubuntu-latest
# The default pending slot cancels older queued RCs on rapid pushes.
concurrency:
group: release-${{ github.ref_name }}
cancel-in-progress: false
queue: max

steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
with:
egress-policy: audit

- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# Tag the triggering commit even if the branch advances while queued.
ref: ${{ github.sha }}
fetch-depth: 0
fetch-tags: true

- name: Create or reuse RC tag
env:
RELEASE_BRANCH: ${{ github.ref_name }}
run: ./scripts/release/create-tag.sh "$RELEASE_BRANCH" rc

- name: Summary
run: |
TAG=$(cat "$RUNNER_TEMP/release_tag")
{
echo "## RC tag ready"
echo ""
echo "**Tag**: \`${TAG}\`"
} >> "$GITHUB_STEP_SUMMARY"
79 changes: 79 additions & 0 deletions .github/workflows/publish-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
name: Publish Release

on:
workflow_dispatch:
inputs:
version:
description: Version to release (X.Y.Z, without a v prefix)
required: true
type: string

permissions:
contents: write

concurrency:
group: publish-release-${{ inputs.version }}
cancel-in-progress: false

jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
with:
egress-policy: audit

- name: Checkout release tools
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: main
fetch-depth: 0
fetch-tags: true

- name: Preserve release tools
# Existing release branches may not contain the release scripts.
run: cp -R scripts/release "$RUNNER_TEMP/release-scripts"

- name: Validate and checkout release branch
env:
VERSION: ${{ inputs.version }}
run: |
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::Invalid version format: $VERSION (expected X.Y.Z)"
exit 1
fi
RELEASE_BRANCH="releases/v${VERSION}"
git fetch origin "refs/heads/$RELEASE_BRANCH" || {
echo "::error::Branch $RELEASE_BRANCH not found. Run Start Release first."
exit 1
}
git checkout --detach FETCH_HEAD

- name: Create or reuse final tag
env:
VERSION: ${{ inputs.version }}
run: bash "$RUNNER_TEMP/release-scripts/create-tag.sh" "releases/v${VERSION}" final

- name: Create or reuse draft release
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
run: |
TAG=$(cat "$RUNNER_TEMP/release_tag")
if gh release view "$TAG" > /dev/null 2>&1; then
echo "Release $TAG already exists; reusing it."
else
gh release create "$TAG" --draft --generate-notes --title "$TAG" --verify-tag
fi

- name: Summary
run: |
TAG=$(cat "$RUNNER_TEMP/release_tag")
{
echo "## Release ready"
echo ""
echo "**Tag**: \`${TAG}\`"
echo ""
echo "Review the generated notes and publish the draft on GitHub. An existing release is left unchanged."
} >> "$GITHUB_STEP_SUMMARY"
38 changes: 38 additions & 0 deletions .github/workflows/release-branch-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: Release Branch CI

on:
pull_request:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Please add types: [opened, synchronize, reopened, edited]: retargeting a PR from an unfinalised release branch to a finalised one otherwise leaves the previous passing check on the unchanged head SHA without rerunning this guard.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in fbaa6a5. Release Branch CI now runs on types: [opened, synchronize, reopened, edited], so retargeting a PR to a finalized release branch reruns the Finalized Release check. docs/RELEASE.md lists the events.

🤖 Reply generated with Toshi

# `edited` covers retargeting the PR to another base branch.
types: [opened, synchronize, reopened, edited]
branches:
- 'releases/v*'

concurrency:
group: ${{ github.workflow }}-${{ github.base_ref }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true

permissions:
contents: read

jobs:
finalized-release:
name: Finalized Release
runs-on: ubuntu-latest
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
with:
egress-policy: audit

- name: Checkout release tools
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: main
fetch-depth: 0
fetch-tags: true
persist-credentials: false

- name: Check release is not finalized
env:
RELEASE_BRANCH: ${{ github.base_ref }}
run: ./scripts/release/check-finalized.sh "$RELEASE_BRANCH"
40 changes: 40 additions & 0 deletions .github/workflows/release-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: Release Tests

on:
pull_request:
paths:
- 'scripts/release/**'
- '.github/workflows/*release*.yml'
- '.github/workflows/create-rc.yml'
- 'justfile'
push:
branches:
- main
paths:
- 'scripts/release/**'
- '.github/workflows/*release*.yml'
- '.github/workflows/create-rc.yml'
- 'justfile'

permissions:
contents: read

jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
with:
egress-policy: audit

- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false

- name: Test release scripts
run: ./scripts/release/test-release.sh

- name: Check release shell scripts
run: shellcheck scripts/release/*.sh
62 changes: 62 additions & 0 deletions .github/workflows/start-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
name: Start Release

on:
workflow_dispatch:
inputs:
bump_type:
description: Version bump type
required: true
type: choice
options:
- minor
- patch
- major
default: minor

permissions:
contents: write

concurrency:
group: start-release
cancel-in-progress: false

jobs:
start:
runs-on: ubuntu-latest
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
with:
egress-policy: audit

- name: Checkout release tools
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: main
fetch-depth: 0
fetch-tags: true

- name: Preserve release tools
# Patch bases can predate the release scripts.
run: cp -R scripts/release "$RUNNER_TEMP/release-scripts"

- name: Create release branch and first RC
env:
BUMP_TYPE: ${{ inputs.bump_type }}
run: bash "$RUNNER_TEMP/release-scripts/start-release.sh" "$BUMP_TYPE"

- name: Summary
run: |
CURRENT=$(cat "$RUNNER_TEMP/current_version")
NEW=$(cat "$RUNNER_TEMP/new_version")
BRANCH=$(cat "$RUNNER_TEMP/release_branch")
TAG=$(cat "$RUNNER_TEMP/release_tag")
{
echo "## Release branch created"
echo ""
echo "**Version**: v${CURRENT} to v${NEW}"
echo "**Branch**: \`${BRANCH}\`"
echo "**First RC**: \`${TAG}\`"
echo ""
echo "Next: test the RC, then run **Publish Release**."
} >> "$GITHUB_STEP_SUMMARY"
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

This repo contains the contracts and scripts for Base.
For contract deployment artifacts, see [base-org/contract-deployments](https://github.com/base-org/contract-deployments).
For branches, release candidates, and publishing, see the [release process](docs/RELEASE.md).

<!-- Badge row 1 - status -->

Expand Down
113 changes: 113 additions & 0 deletions docs/RELEASE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Release process

Release versions come from branch names, not a repository version file. Release
management creates branches, tags, and draft GitHub releases. It does not build
artifacts or change individual contracts' versions or semver snapshots.

## Start a release

Run **Start Release** in GitHub Actions from `main` and select a bump type:

- `minor` is the default. It increments the minor version and resets the patch.
- `patch` increments the patch version.
- `major` increments the major version and resets the minor and patch.

The workflow uses the highest final `vX.Y.Z` tag by version order. RC tags of
both formats are ignored. For example, `v8.10.0` comes after `v8.9.0`, even if a
`v9.0.0-rc.1` tag also exists.

Major and minor releases branch from `main`. Patch releases branch from the
highest existing `releases/vX.Y.*` branch for the latest final tag's major and
minor version. Starting a release fails if the new branch already exists or if
a patch release has no matching base branch.

Start Release pushes `releases/vX.Y.Z` and an annotated first RC tag at its head
atomically. Both refs become available together, before subsequent Create RC jobs
can allocate a tag. Branch pushes made with `GITHUB_TOKEN` do not trigger other
workflows, so Start Release cannot rely on Create RC to make that first tag.
There is no version-sync step.

## Create release candidates

Every push to a `releases/v*` branch containing the automation runs **Create RC**.
It tags the triggering commit, even if newer commits arrive while it waits.
New tags use `vX.Y.Z-rc.N`.

Legacy tags use `vX.Y.Z.rcN`. Both formats reserve RC numbers for their exact
version. For example, with `v8.4.0.rc1` and `v8.4.0.rc2` present, the next RC is
`v8.4.0-rc.3`. Numbering is numeric, so `rc.10` follows `rc.9`. Tags for other
versions do not affect the count.

Subsequent RC tag allocation is serialized per release branch with
`cancel-in-progress: false`
and `queue: max`. GitHub allows up to 100 pending jobs in that queue. A full
queue cancels additional jobs. Jobs enter the queue when they start waiting,
which need not match commit order. Each job still tags its own triggering commit.

### Retries

Rerun a failed Create RC workflow. If a new-format RC tag for that version already
points at the triggering commit, the script reuses the highest such tag instead
of allocating another number. Legacy tags at that commit count toward numbering
but are not reused.

Start Release refuses to reuse an existing release branch. Its atomic push
publishes neither the branch nor the first RC if either ref is rejected. If the
workflow fails after the push succeeds, verify the branch and tag rather than
rerunning Start Release. Do not move an existing tag to repair a failed run.

## Publish the final release

Run **Publish Release** from `main` and enter `X.Y.Z`, without a `v` or `releases/`
prefix. The workflow:

- Validates the version and checks that `releases/vX.Y.Z` exists.
- Checks out the release branch head and creates `vX.Y.Z` there.
- Creates a draft GitHub release with GitHub-generated "What's Changed" notes.

Review the notes and publish the draft on GitHub. There are no Docker images,
binaries, or other release assets to wait for.

Publishing is serialized per version. If the final tag already points at the
release branch head, a retry reuses it. A final tag at a different commit causes
a failure; the workflow never moves it. If a GitHub release already exists,
the workflow leaves its notes and draft or published state unchanged.

When a pull request is opened, reopened, updated, or edited (including
retargeting its base branch), **Release Branch CI** checks the repository's tags using
tools from `main`. Its Finalized Release check fails for a `releases/vX.Y.Z`
target once `vX.Y.Z` exists, even if the GitHub release is still a draft. Keep
this check required in branch protection. Creating the final tag does not rerun
checks on open pull requests, so an earlier passing check can stay green. After
finalizing, rerun checks on open pull requests into that branch or lock the
branch via branch protection to prevent merges. The check does not prevent
direct pushes.

## Existing release branches

Push-triggered workflows only run on release branches that contain their
workflow files. Updating `main` does not install automation on existing branches.
Branches such as `releases/v8.4.0` keep the manual release process unless the
release workflows and scripts are backported. Patch branches created from an
older release branch inherit that limitation, although Start Release still
creates their initial RC using the tools from `main`.

To adopt automatic RCs and the finalized-release check on an active branch,
backport the release workflows and `scripts/release/`. Existing queued or running
workflows keep the configuration they started with. Release Branch CI loads its
tools from `main`, so the backport can pass even when the target branch has no
release scripts. Publish Release also uses tools from `main` and can tag an
existing branch without that backport.

## Quick reference

| Action | Workflow | Trigger | Output |
|--------|----------|---------|--------|
| Start a release | Start Release | Manual bump type | Release branch and first RC tag |
| Create an RC | Create RC | Push to `releases/v*` | RC tag at the triggering commit |
| Finalize a release | Publish Release | Manual version | Final tag and draft GitHub release |
| Guard a finalized branch | Release Branch CI | Pull request to `releases/v*` | Fails if the final tag exists |

Run `just test-release` to test version selection, legacy RC numbering, retry
behavior, atomic branch and first-RC publication, and final-tag protection using
temporary local repositories without network access.
Loading
Loading