Skip to content

Commit da5a4d5

Browse files
committed
ci(release): split release pipeline into prepare/validate/dispatch stages
Replace the single tag-push workflow with a manual, re-runnable pipeline: `release-prepare` runs CI and builds the fsck binaries per tag, `release-validate` checks the tag against Cargo.toml, and `release` is dispatched by hand with a prepare run ID to publish to crates.io and cut the GitHub release. Each stage is idempotent, so a failed publish or release step can be retried without rebuilding tests or binaries.
1 parent db5fa27 commit da5a4d5

3 files changed

Lines changed: 298 additions & 128 deletions

File tree

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# Tag gate for a releasable build. Pushing a v* tag validates the tag, runs the
2+
# full test suite, and builds the `pagedb-fsck` binaries once. Nothing is
3+
# published and nothing leaves the repo.
4+
#
5+
# The manual `Release` workflow then consumes THIS run's binary artifacts by run
6+
# ID for the GitHub Release, so a failure in the publish or release stage can be
7+
# fixed and retried without repeating the tests or the compile.
8+
#
9+
# Artifacts are retained 14 days: that is the window in which a `Release` run can
10+
# still resume from this prepare run.
11+
12+
name: Release Prepare
13+
run-name: Release Prepare ${{ github.ref_name }}
14+
15+
on:
16+
push:
17+
tags:
18+
- "v*"
19+
20+
concurrency:
21+
group: release-prepare-${{ github.ref_name }}
22+
cancel-in-progress: false
23+
24+
permissions:
25+
contents: read
26+
27+
env:
28+
CARGO_TERM_COLOR: always
29+
30+
jobs:
31+
validate-version:
32+
uses: ./.github/workflows/release-validate.yml
33+
with:
34+
ref: ${{ github.ref_name }}
35+
36+
# Full lint + cross-platform test + wasm/features/bench gates. Calls test.yml
37+
# directly rather than ci.yml, whose PR-draft gate skips under workflow_call.
38+
ci:
39+
name: CI Gate
40+
needs: validate-version
41+
uses: ./.github/workflows/test.yml
42+
43+
# ── Build pagedb-fsck binary per target ─────────────────────────────────
44+
build-binaries:
45+
name: Build fsck (${{ matrix.label }})
46+
needs: [validate-version, ci]
47+
runs-on: ${{ matrix.runs-on }}
48+
strategy:
49+
fail-fast: false
50+
matrix:
51+
include:
52+
- runs-on: ubuntu-latest
53+
target: x86_64-unknown-linux-gnu
54+
label: linux-x64
55+
ext: ""
56+
- runs-on: ubuntu-24.04-arm
57+
target: aarch64-unknown-linux-gnu
58+
label: linux-arm64
59+
ext: ""
60+
- runs-on: macos-latest
61+
target: aarch64-apple-darwin
62+
label: macos-arm64
63+
ext: ""
64+
- runs-on: macos-13
65+
target: x86_64-apple-darwin
66+
label: macos-x64
67+
ext: ""
68+
- runs-on: windows-latest
69+
target: x86_64-pc-windows-msvc
70+
label: windows-x64
71+
ext: ".exe"
72+
steps:
73+
- uses: actions/checkout@v6
74+
75+
- name: Install Rust + target
76+
uses: dtolnay/rust-toolchain@stable
77+
with:
78+
targets: ${{ matrix.target }}
79+
80+
- uses: Swatinem/rust-cache@v2
81+
with:
82+
prefix-key: fsck-${{ matrix.target }}
83+
84+
- name: Build pagedb-fsck
85+
run: cargo build --release --bin pagedb-fsck --target ${{ matrix.target }}
86+
87+
- name: Package (Unix)
88+
if: runner.os != 'Windows'
89+
run: |
90+
set -euo pipefail
91+
cd target/${{ matrix.target }}/release
92+
chmod +x pagedb-fsck
93+
tar czf pagedb-fsck-${{ needs.validate-version.outputs.version }}-${{ matrix.label }}.tar.gz pagedb-fsck
94+
95+
- name: Package (Windows)
96+
if: runner.os == 'Windows'
97+
shell: pwsh
98+
run: |
99+
cd target/${{ matrix.target }}/release
100+
$name = "pagedb-fsck-${{ needs.validate-version.outputs.version }}-${{ matrix.label }}.zip"
101+
Compress-Archive -Path "pagedb-fsck${{ matrix.ext }}" -DestinationPath $name
102+
103+
- uses: actions/upload-artifact@v4
104+
with:
105+
name: fsck-${{ matrix.label }}
106+
path: |
107+
target/${{ matrix.target }}/release/pagedb-fsck-*.tar.gz
108+
target/${{ matrix.target }}/release/pagedb-fsck-*.zip
109+
if-no-files-found: error
110+
retention-days: 14
111+
112+
summary:
113+
name: Release instructions
114+
needs: [validate-version, build-binaries]
115+
runs-on: ubuntu-latest
116+
steps:
117+
- name: Write summary
118+
env:
119+
VERSION: ${{ needs.validate-version.outputs.version }}
120+
REPO: ${{ github.repository }}
121+
RUN_ID: ${{ github.run_id }}
122+
run: |
123+
{
124+
echo "## Prepared v${VERSION}"
125+
echo
126+
echo "Tests passed and the fsck binaries are built. Publish with:"
127+
echo
128+
echo '```sh'
129+
echo "gh workflow run release.yml -R ${REPO} \\"
130+
echo " -f tag=v${VERSION} \\"
131+
echo " -f prepare_run_id=${RUN_ID}"
132+
echo '```'
133+
echo
134+
echo "If one stage fails, re-run the same command with only the"
135+
echo "remaining stage enabled (e.g. \`-f publish_crate=false\`)."
136+
echo "Nothing is recompiled."
137+
} >> "$GITHUB_STEP_SUMMARY"
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# Reusable tag/version validation. Called by release-prepare.yml (from the
2+
# pushed tag) and release.yml (from the dispatched tag), so the two entry
3+
# points can never disagree about what a tag means.
4+
5+
name: Release Validate
6+
7+
on:
8+
workflow_call:
9+
inputs:
10+
ref:
11+
description: "Git ref to validate (a v* tag)"
12+
required: true
13+
type: string
14+
outputs:
15+
version:
16+
description: "Tag version without the leading v"
17+
value: ${{ jobs.validate.outputs.version }}
18+
is_full_release:
19+
description: "true when the version carries no prerelease suffix"
20+
value: ${{ jobs.validate.outputs.is_full_release }}
21+
22+
permissions:
23+
contents: read
24+
25+
jobs:
26+
validate:
27+
name: Validate Version Tag
28+
runs-on: ubuntu-latest
29+
outputs:
30+
version: ${{ steps.version.outputs.version }}
31+
is_full_release: ${{ steps.version.outputs.is_full_release }}
32+
steps:
33+
- uses: actions/checkout@v6
34+
with:
35+
ref: ${{ inputs.ref }}
36+
37+
- name: Install Rust
38+
uses: dtolnay/rust-toolchain@stable
39+
40+
- name: Validate tag against Cargo.toml
41+
id: version
42+
env:
43+
TAG: ${{ inputs.ref }}
44+
run: |
45+
set -euo pipefail
46+
TAG_VERSION="${TAG#v}"
47+
48+
CARGO_VERSION=$(cargo metadata --no-deps --format-version=1 \
49+
| jq -r '.packages[] | select(.name == "pagedb") | .version')
50+
CARGO_BASE=$(echo "$CARGO_VERSION" | grep -oP '^\d+\.\d+\.\d+')
51+
52+
echo "Tag version: $TAG_VERSION"
53+
echo "Cargo.toml version: $CARGO_VERSION"
54+
echo "Cargo.toml base: $CARGO_BASE"
55+
56+
if [[ ! "$TAG_VERSION" =~ ^([0-9]+\.[0-9]+\.[0-9]+)(-[a-zA-Z]+\.[0-9]+)?$ ]]; then
57+
echo "::error::Invalid tag format '$TAG'. Expected: vX.Y.Z or vX.Y.Z-label.N"
58+
exit 1
59+
fi
60+
61+
TAG_BASE="${BASH_REMATCH[1]}"
62+
63+
if [[ "$TAG_BASE" != "$CARGO_BASE" ]]; then
64+
echo "::error::Base version mismatch! Tag '$TAG_BASE' != Cargo.toml '$CARGO_BASE'"
65+
exit 1
66+
fi
67+
68+
# Full release = no hyphen suffix (v0.1.0, not v0.1.0-beta.1)
69+
if [[ "$TAG_VERSION" == *-* ]]; then
70+
echo "is_full_release=false" >> "$GITHUB_OUTPUT"
71+
else
72+
echo "is_full_release=true" >> "$GITHUB_OUTPUT"
73+
fi
74+
75+
echo "version=$TAG_VERSION" >> "$GITHUB_OUTPUT"

0 commit comments

Comments
 (0)