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
109 changes: 69 additions & 40 deletions .github/workflows/release-prepare.yml
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
# Tag gate for a releasable build. Pushing a v* tag validates the tag, runs the
# full test suite, and builds the `pagedb-fsck` binaries once. Nothing is
# published and nothing leaves the repo.
# Tag gate for a releasable build. A v* tag validates the version, runs the
# complete reusable suite, builds each pagedb-fsck archive once, and records
# immutable provenance. This workflow never publishes a crate or a release.
#
# The manual `Release` workflow then consumes THIS run's binary artifacts by run
# ID for the GitHub Release, so a failure in the publish or release stage can be
# fixed and retried without repeating the tests or the compile.
#
# Artifacts are retained 14 days: that is the window in which a `Release` run can
# still resume from this prepare run.
# Artifacts are retained for 14 days. A maintainer can retry distribution during
# that window without rebuilding or silently substituting another tag's files.

name: Release Prepare
run-name: Release Prepare ${{ github.ref_name }}
Expand All @@ -33,14 +29,13 @@ jobs:
with:
ref: ${{ github.ref_name }}

# Full lint + cross-platform test + wasm/features/bench gates. Calls test.yml
# directly rather than ci.yml, whose PR-draft gate skips under workflow_call.
# Calls test.yml directly because ci.yml's pull-request draft gate does not
# describe a tag build.
ci:
name: CI Gate
needs: validate-version
uses: ./.github/workflows/test.yml

# ── Build pagedb-fsck binary per target ─────────────────────────────────
build-binaries:
name: Build fsck (${{ matrix.label }})
needs: [validate-version, ci]
Expand All @@ -61,8 +56,8 @@ jobs:
target: aarch64-apple-darwin
label: macos-arm64
ext: ""
# x86_64-apple-darwin cross-compiles on Apple Silicon; the Intel
# macos-13 image was retired, so build it on the arm64 runner.
# The x86_64 target cross-compiles on the current Apple Silicon
# runner; the retired Intel runner is not a release dependency.
- runs-on: macos-latest
target: x86_64-apple-darwin
label: macos-x64
Expand All @@ -72,41 +67,52 @@ jobs:
label: windows-x64
ext: ".exe"
steps:
- uses: actions/checkout@v7

- name: Install Rust + target
uses: dtolnay/rust-toolchain@stable
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
targets: ${{ matrix.target }}
persist-credentials: false

- uses: Swatinem/rust-cache@v2
- name: Install Rust and target
uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 # stable
with:
prefix-key: fsck-${{ matrix.target }}
targets: ${{ matrix.target }}

- name: Set version from tag
shell: bash
run: bash scripts/ci/stamp_version.sh "${{ needs.validate-version.outputs.version }}"
env:
VERSION: ${{ needs.validate-version.outputs.version }}
run: bash scripts/ci/stamp_version.sh "$VERSION"

- name: Build pagedb-fsck
run: cargo build --release --bin pagedb-fsck --target ${{ matrix.target }}
env:
TARGET: ${{ matrix.target }}
run: cargo build --locked --release --bin pagedb-fsck --target "$TARGET"

- name: Package (Unix)
- name: Package archive
if: runner.os != 'Windows'
env:
LABEL: ${{ matrix.label }}
TARGET: ${{ matrix.target }}
VERSION: ${{ needs.validate-version.outputs.version }}
run: |
set -euo pipefail
cd target/${{ matrix.target }}/release
cd "target/${TARGET}/release"
chmod +x pagedb-fsck
tar czf pagedb-fsck-${{ needs.validate-version.outputs.version }}-${{ matrix.label }}.tar.gz pagedb-fsck
tar czf "pagedb-fsck-${VERSION}-${LABEL}.tar.gz" pagedb-fsck

- name: Package (Windows)
- name: Package archive
if: runner.os == 'Windows'
shell: pwsh
env:
EXT: ${{ matrix.ext }}
LABEL: ${{ matrix.label }}
TARGET: ${{ matrix.target }}
VERSION: ${{ needs.validate-version.outputs.version }}
run: |
cd target/${{ matrix.target }}/release
$name = "pagedb-fsck-${{ needs.validate-version.outputs.version }}-${{ matrix.label }}.zip"
Compress-Archive -Path "pagedb-fsck${{ matrix.ext }}" -DestinationPath $name
Set-Location "target/$env:TARGET/release"
$name = "pagedb-fsck-$env:VERSION-$env:LABEL.zip"
Compress-Archive -Path "pagedb-fsck$env:EXT" -DestinationPath $name

- uses: actions/upload-artifact@v7
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: fsck-${{ matrix.label }}
path: |
Expand All @@ -115,29 +121,52 @@ jobs:
if-no-files-found: error
retention-days: 14

summary:
name: Release instructions
provenance:
name: Release provenance and instructions
needs: [validate-version, build-binaries]
runs-on: ubuntu-latest
steps:
- name: Write summary
- name: Record prepared release
env:
COMMIT: ${{ needs.validate-version.outputs.commit }}
REPOSITORY: ${{ github.repository }}
RUN_ID: ${{ github.run_id }}
TAG: ${{ github.ref_name }}
VERSION: ${{ needs.validate-version.outputs.version }}
REPO: ${{ github.repository }}
run: |
jq -n \
--arg commit "$COMMIT" \
--arg repository "$REPOSITORY" \
--arg run_id "$RUN_ID" \
--arg tag "$TAG" \
--arg version "$VERSION" \
'{schema:1, repository:$repository, tag:$tag, version:$version, commit:$commit, prepare_run_id:$run_id}' \
> release-provenance.json

- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: release-provenance
path: release-provenance.json
if-no-files-found: error
retention-days: 14

- name: Write summary
env:
REPOSITORY: ${{ github.repository }}
RUN_ID: ${{ github.run_id }}
VERSION: ${{ needs.validate-version.outputs.version }}
run: |
{
echo "## Prepared v${VERSION}"
echo
echo "Tests passed and the fsck binaries are built. Publish with:"
echo "Tests passed, binaries were built, and provenance was recorded."
echo "Distribute this exact run with:"
echo
echo '```sh'
echo "gh workflow run release.yml -R ${REPO} \\"
echo "gh workflow run release.yml -R ${REPOSITORY} \\"
echo " -f tag=v${VERSION} \\"
echo " -f prepare_run_id=${RUN_ID}"
echo '```'
echo
echo "If one stage fails, re-run the same command with only the"
echo "remaining stage enabled (e.g. \`-f publish_crate=false\`)."
echo "Nothing is recompiled."
echo "Retry only a failed stage by disabling the completed one."
} >> "$GITHUB_STEP_SUMMARY"
47 changes: 31 additions & 16 deletions .github/workflows/release-validate.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
# Reusable tag/version validation. Called by release-prepare.yml (from the
# pushed tag) and release.yml (from the dispatched tag), so the two entry
# points can never disagree about what a tag means.
# Reusable tag/version validation. Release Prepare calls this from the pushed
# tag, and Release calls it again from the manually dispatched tag. Both stages
# therefore agree on tag syntax, package version, and the exact tag commit.

name: Release Validate

on:
workflow_call:
inputs:
ref:
description: "Git ref to validate (a v* tag)"
description: "Release tag to validate (vX.Y.Z or vX.Y.Z-label.N)"
required: true
type: string
outputs:
Expand All @@ -18,6 +18,9 @@ on:
is_full_release:
description: "true when the version carries no prerelease suffix"
value: ${{ jobs.validate.outputs.is_full_release }}
commit:
description: "Commit referenced by the validated tag"
value: ${{ jobs.validate.outputs.commit }}

permissions:
contents: read
Expand All @@ -29,47 +32,59 @@ jobs:
outputs:
version: ${{ steps.version.outputs.version }}
is_full_release: ${{ steps.version.outputs.is_full_release }}
commit: ${{ steps.version.outputs.commit }}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
ref: ${{ inputs.ref }}
fetch-depth: 0

- name: Install Rust
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 # stable

- name: Validate tag against Cargo.toml
id: version
env:
TAG: ${{ inputs.ref }}
run: |
set -euo pipefail
TAG_VERSION="${TAG#v}"

if [[ ! "$TAG" =~ ^v([0-9]+\.[0-9]+\.[0-9]+)(-[A-Za-z][A-Za-z0-9-]*\.[0-9]+)?$ ]]; then
echo "::error::Invalid tag '$TAG'. Expected vX.Y.Z or vX.Y.Z-label.N"
exit 1
fi

TAG_VERSION="${TAG#v}"
TAG_BASE="${BASH_REMATCH[1]}"
CARGO_VERSION=$(cargo metadata --no-deps --format-version=1 \
| jq -r '.packages[] | select(.name == "pagedb") | .version')
CARGO_BASE=$(echo "$CARGO_VERSION" | grep -oP '^\d+\.\d+\.\d+')
if [[ ! "$CARGO_VERSION" =~ ^([0-9]+\.[0-9]+\.[0-9]+) ]]; then
echo "::error::Cargo.toml carries an invalid package version '$CARGO_VERSION'"
exit 1
fi
CARGO_BASE="${BASH_REMATCH[1]}"

echo "Tag version: $TAG_VERSION"
echo "Cargo.toml version: $CARGO_VERSION"
echo "Cargo.toml base: $CARGO_BASE"

if [[ ! "$TAG_VERSION" =~ ^([0-9]+\.[0-9]+\.[0-9]+)(-[a-zA-Z]+\.[0-9]+)?$ ]]; then
echo "::error::Invalid tag format '$TAG'. Expected: vX.Y.Z or vX.Y.Z-label.N"
if [[ "$TAG_BASE" != "$CARGO_BASE" ]]; then
echo "::error::Base version mismatch: tag '$TAG_BASE' != Cargo.toml '$CARGO_BASE'"
exit 1
fi

TAG_BASE="${BASH_REMATCH[1]}"

if [[ "$TAG_BASE" != "$CARGO_BASE" ]]; then
echo "::error::Base version mismatch! Tag '$TAG_BASE' != Cargo.toml '$CARGO_BASE'"
TAG_COMMIT=$(git rev-parse "refs/tags/${TAG}^{commit}")
HEAD_COMMIT=$(git rev-parse HEAD)
if [[ "$TAG_COMMIT" != "$HEAD_COMMIT" ]]; then
echo "::error::Checkout '$HEAD_COMMIT' does not match tag '$TAG_COMMIT'"
exit 1
fi

# Full release = no hyphen suffix (v0.1.0, not v0.1.0-beta.1)
if [[ "$TAG_VERSION" == *-* ]]; then
echo "is_full_release=false" >> "$GITHUB_OUTPUT"
else
echo "is_full_release=true" >> "$GITHUB_OUTPUT"
fi

echo "version=$TAG_VERSION" >> "$GITHUB_OUTPUT"
echo "commit=$TAG_COMMIT" >> "$GITHUB_OUTPUT"
Loading