Skip to content

Release

Release #45

Workflow file for this run

name: Release
on:
workflow_dispatch:
inputs:
version:
description: "Version to release (e.g. 0.19.0, 1.0.0-beta.1) — no v prefix"
required: true
type: string
crate:
description: "Publish to crates.io"
type: boolean
default: true
npm:
description: "Publish to npm"
type: boolean
default: true
github_release:
description: "Create GitHub Release + deploy docs"
type: boolean
default: true
permissions: {}
env:
CARGO_TERM_COLOR: always
GIT_VERSION: ${{ inputs.version }}
jobs:
# ───────────────────────────────────────────────────────────
# Validate inputs
# ───────────────────────────────────────────────────────────
validate:
name: Validate
runs-on: ubuntu-24.04
steps:
- name: Check version format
run: |
if ! echo "$GIT_VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$'; then
echo "::error::Invalid version format: $GIT_VERSION (expected semver without v prefix)"
exit 1
fi
- name: Check at least one target selected
run: |
if [[ "${{ inputs.crate }}" != "true" \
&& "${{ inputs.npm }}" != "true" \
&& "${{ inputs.github_release }}" != "true" ]]; then
echo "::error::No publish targets selected"
exit 1
fi
# ───────────────────────────────────────────────────────────
# Test and build binaries
# ───────────────────────────────────────────────────────────
test-and-build:
name: Test and Build (${{ matrix.target }})
needs: validate
if: inputs.npm || inputs.github_release
runs-on: ${{ matrix.os }}
defaults:
run:
shell: bash
strategy:
fail-fast: false
matrix:
include:
- build: windows
os: windows-latest
target: x86_64-pc-windows-msvc
run_tests: true
- build: windows-arm64
os: windows-11-arm
target: aarch64-pc-windows-msvc
run_tests: true
- build: linux
os: ubuntu-latest
target: x86_64-unknown-linux-musl
run_tests: true
- build: linux-arm64
os: ubuntu-latest
target: aarch64-unknown-linux-musl
run_tests: false
- build: macos
os: macos-latest
target: x86_64-apple-darwin
run_tests: true
- build: macos-m1
os: macos-latest
target: aarch64-apple-darwin
run_tests: false
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: main
fetch-depth: 1
- name: Cache
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
~/.cargo/registry
~/.cargo/git
~/.rustup
target
key: ${{ runner.os }}-stable
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
- name: Verify Changelog
run: node ./.backstage/changelog.cjs
- name: Install Tooling
if: matrix.os == 'ubuntu-latest'
run: |
sudo apt update -y
sudo apt install -y musl-tools musl-dev clang gcc-aarch64-linux-gnu
echo "TARGET_CC=clang" >> $GITHUB_ENV
echo "CFLAGS_aarch64_unknown_linux_musl=--sysroot=/usr/aarch64-linux-gnu" >> $GITHUB_ENV
echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER=/usr/aarch64-linux-gnu/bin/ld" >> $GITHUB_ENV
- name: Install Rust
run: |
rustup toolchain install stable --component rustfmt,clippy --target ${{ matrix.target }}
rustup default stable
- name: Ubuntu AppArmor fix
if: matrix.os == 'ubuntu-latest'
# Ubuntu >= 23 has AppArmor enabled by default, which breaks Chrome.
# See https://github.com/puppeteer/puppeteer/issues/12818 "No usable sandbox!"
# this is taken from the solution used in Puppeteer's CI: https://github.com/puppeteer/puppeteer/pull/13196
run: |
echo 0 | sudo tee /proc/sys/kernel/apparmor_restrict_unprivileged_userns
- name: Prepare Git
run: |
git config user.email "github@github.com"
git config user.name "Github Actions"
# Use throw-away branch so we don't push the changes to origin
git checkout -B deploy_branch
- name: Prepare Crates
run: |
# Update cargo version,
node ./.backstage/version.cjs
git add ./toolproof/Cargo.toml
# Commit changes so cargo doesn't complain about dirty repo
git commit -m "Deploy changes."
- name: Build
working-directory: ./toolproof
run: cargo build --release --target ${{ matrix.target }}
- name: Test Lib
if: matrix.run_tests
working-directory: ./toolproof
run: cargo test --release --target ${{ matrix.target }}
- name: Test CLI
if: matrix.run_tests
working-directory: ./toolproof
# toolproof tests itself when run
run: cargo run -- --placeholders toolproof_path="$(pwd)/../target/debug/toolproof" -c 1 --timeout 60
- name: Create Release Assets
run: |
EXEC_NAME="toolproof"
ASSET_PATH="$EXEC_NAME-v$GIT_VERSION-${{ matrix.target }}.tar.gz"
CHECKSUM_PATH="$ASSET_PATH.sha256"
if [ "$RUNNER_OS" == "Windows" ]; then
EXEC_NAME="toolproof.exe"
fi
if command -v gtar &> /dev/null; then
echo "Using gtar"
gtar czf $ASSET_PATH -C target/${{ matrix.target }}/release $EXEC_NAME
else
echo "Using system tar"
tar czf $ASSET_PATH -C target/${{ matrix.target }}/release $EXEC_NAME
fi
case $RUNNER_OS in
Windows)
sha256sum $ASSET_PATH > $CHECKSUM_PATH
;;
Linux)
sha256sum $ASSET_PATH > $CHECKSUM_PATH
;;
macOS)
shasum -a 256 $ASSET_PATH > $CHECKSUM_PATH
;;
esac
echo "ASSET_PATH=$ASSET_PATH" >> $GITHUB_ENV
echo "CHECKSUM_PATH=$CHECKSUM_PATH" >> $GITHUB_ENV
- name: Upload build artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release-${{ matrix.target }}
path: |
${{ env.ASSET_PATH }}
${{ env.CHECKSUM_PATH }}
- name: Upload checksum artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release-checksums-${{ matrix.target }}
path: |
${{ env.CHECKSUM_PATH }}
# ───────────────────────────────────────────────────────────
# Prepare binary NPM packages
# ───────────────────────────────────────────────────────────
prepare-packages:
name: Prepare Binary NPM Packages
runs-on: ubuntu-24.04
needs: test-and-build
if: inputs.npm
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-pc-windows-msvc
package: windows-x64
os: win32
cpu: x64
- target: aarch64-pc-windows-msvc
package: windows-arm64
os: win32
cpu: arm64
- target: x86_64-unknown-linux-musl
package: linux-x64
os: linux
cpu: x64
- target: aarch64-unknown-linux-musl
package: linux-arm64
os: linux
cpu: arm64
- target: x86_64-apple-darwin
package: darwin-x64
os: darwin
cpu: x64
- target: aarch64-apple-darwin
package: darwin-arm64
os: darwin
cpu: arm64
steps:
- name: Clone
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: main
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: release-${{ matrix.target }}
path: build-artifacts
- name: Create Package
run: |
mkdir -p ${{ matrix.package }}/bin/
cd ${{ matrix.package }}
node ../.backstage/create_package.cjs ${{ matrix.os }} ${{ matrix.cpu }}
- name: Extract Binary
working-directory: build-artifacts
run: |
tar xzf toolproof-v$GIT_VERSION-${{ matrix.target }}.tar.gz
rm *.tar.gz
mv * ../${{ matrix.package }}/bin/
- name: Prepare package
working-directory: ${{ matrix.package }}
run: |
npm version $GIT_VERSION --no-git-tag-version
npm pack
- name: Upload prepared package
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: prepared-${{ matrix.package }}
path: ${{ matrix.package }}/*.tgz
# ───────────────────────────────────────────────────────────
# Publish
# ───────────────────────────────────────────────────────────
publish:
name: Publish
runs-on: ubuntu-24.04
environment: production
needs: [validate, test-and-build, prepare-packages]
# Run even when the build/prepare jobs were skipped (e.g. a crate-only
# release), but not when any needed job actually failed.
if: ${{ !cancelled() && !failure() }}
permissions:
id-token: write
contents: write
defaults:
run:
working-directory: ./
steps:
# ── Setup ────────────────────────────────────────────
- name: Get Token
id: get_workflow_token
uses: peter-murray/workflow-application-token-action@d17e3a9a36850ea89f35db16c1067dd2b68ee343 # v4.0.1
with:
application_id: ${{ secrets.PF_BOT_ID }}
application_private_key: ${{ secrets.PF_BOT_PEM }}
- name: Clone
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: main
fetch-depth: 0
token: ${{ steps.get_workflow_token.outputs.token }}
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
registry-url: "https://registry.npmjs.org"
- name: Get Tag
run: echo GIT_TAG="$(node ./.backstage/get_tag.cjs)" >> $GITHUB_ENV
# ── Changelog ────────────────────────────────────────
- name: Build CHANGELOG
if: env.GIT_TAG == 'latest'
run: |
node ./.backstage/changelog.cjs write
if ! git diff --quiet CHANGELOG.md; then
echo "CHANGELOG_UPDATED=true" >> $GITHUB_ENV
# Use heredoc for multiline-safe env var
echo "CHANGELOG<<CHANGELOG_EOF" >> $GITHUB_ENV
base64 -w 0 -i CHANGELOG.md >> $GITHUB_ENV
echo "" >> $GITHUB_ENV
echo "CHANGELOG_EOF" >> $GITHUB_ENV
echo "SHA=$(git rev-parse main:CHANGELOG.md)" >> $GITHUB_ENV
else
echo "CHANGELOG unchanged (already committed), skipping commit."
fi
- name: Prepare prerelease notes
if: env.GIT_TAG != 'latest'
run: |
node ./.backstage/changelog.cjs write || true
if [ ! -s RELEASE.md ]; then
echo "## v$GIT_VERSION" > RELEASE.md
fi
- name: Commit new CHANGELOG
uses: octokit/request-action@b91aabaa861c777dcdb14e2387e30eddf04619ae # v3.0.0
if: env.CHANGELOG_UPDATED == 'true'
id: push_changes
with:
route: PUT /repos/{owner}/{repo}/contents/CHANGELOG.md
owner: pagefind
repo: toolproof
branch: main
message: Changelog for ${{ env.GIT_VERSION }}
sha: ${{ env.SHA }}
content: ${{ env.CHANGELOG }}
env:
GITHUB_TOKEN: ${{ steps.get_workflow_token.outputs.token }}
- name: Capture changelog commit SHA
if: env.CHANGELOG_UPDATED == 'true'
run: |
COMMIT_SHA=$(node -e "console.log(JSON.parse(process.env.PUSH_DATA).commit.sha)")
echo "CHANGELOG_COMMIT_SHA=$COMMIT_SHA" >> $GITHUB_ENV
env:
PUSH_DATA: ${{ steps.push_changes.outputs.data }}
# ── Tag ──────────────────────────────────────────────
- name: Create git tag
run: |
if git ls-remote --tags origin | grep -q "refs/tags/v$GIT_VERSION$"; then
echo "Tag v$GIT_VERSION already exists, skipping."
else
git config user.email "github@github.com"
git config user.name "Github Actions"
# If we just committed a changelog, fetch to pick it up.
# Retry fetch to handle GitHub API propagation delay.
if [ "$CHANGELOG_UPDATED" = "true" ]; then
EXPECTED_SHA="$CHANGELOG_COMMIT_SHA"
SYNCED=false
for i in 1 2 3 4 5; do
git fetch origin main
FETCHED_SHA=$(git rev-parse origin/main)
if [ -z "$EXPECTED_SHA" ] || [ "$FETCHED_SHA" = "$EXPECTED_SHA" ]; then
SYNCED=true
break
fi
echo "Waiting for changelog commit to propagate (attempt $i)..."
sleep 2
done
if [ "$SYNCED" != "true" ]; then
echo "::error::Changelog commit did not propagate after 5 attempts. Expected $EXPECTED_SHA, got $FETCHED_SHA"
exit 1
fi
else
git fetch origin main
fi
git tag -a "v$GIT_VERSION" origin/main -m "v$GIT_VERSION"
git push origin "v$GIT_VERSION"
fi
# ── Deploy Docs ──────────────────────────────────────
- name: Release documentation branch
uses: octokit/request-action@b91aabaa861c777dcdb14e2387e30eddf04619ae # v3.0.0
if: inputs.github_release && env.GIT_TAG == 'latest'
with:
route: POST /repos/{owner}/{repo}/merges
owner: pagefind
repo: toolproof
base: production-docs
head: main
commit_message: Release documentation for ${{ env.GIT_VERSION }}
env:
GITHUB_TOKEN: ${{ steps.get_workflow_token.outputs.token }}
# ── GitHub Release ───────────────────────────────────
- name: Download release artifacts
if: inputs.github_release
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
path: build-artifacts
pattern: release-*
merge-multiple: true
- name: Create GitHub Release
if: inputs.github_release
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
with:
tag_name: v${{ inputs.version }}
token: ${{ steps.get_workflow_token.outputs.token }}
prerelease: ${{ env.GIT_TAG != 'latest' }}
body_path: RELEASE.md
files: |
build-artifacts/*
# ── NPM ──────────────────────────────────────────────
- name: Download prepared NPM packages
if: inputs.npm
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: prepared-*
- name: Download checksums
if: inputs.npm
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
path: wrappers/node/checksums
pattern: release-checksums-*
merge-multiple: true
- name: Version optional dependencies
if: inputs.npm
run: node ./.backstage/version_main_package.cjs
- name: Prepare main NPM package
if: inputs.npm
working-directory: ./wrappers/node
run: npm version $GIT_VERSION --no-git-tag-version
- name: Publish binary NPM packages
if: inputs.npm
run: |
for pkg in windows-x64 windows-arm64 linux-x64 linux-arm64 darwin-x64 darwin-arm64; do
echo "Publishing @toolproof/$pkg..."
if npm view "@toolproof/$pkg@$GIT_VERSION" version >/dev/null 2>&1; then
echo " Already published, skipping."
else
npm publish ./prepared-$pkg/*.tgz --provenance --tag $GIT_TAG --access public
fi
done
- name: Publish main NPM package
if: inputs.npm
working-directory: ./wrappers/node
run: |
if npm view "toolproof@$GIT_VERSION" version >/dev/null 2>&1; then
echo "Already published, skipping."
else
npm publish --provenance --tag $GIT_TAG
fi
# ── Crate ────────────────────────────────────────────
- name: Install Rust
if: inputs.crate
run: |
rustup toolchain install stable --component rustfmt,clippy
rustup default stable
- name: Prepare Git (crate)
if: inputs.crate
run: |
git config user.email "github@github.com"
git config user.name "Github Actions"
# Use throw-away branch so we don't push the changes to origin
git checkout -B deploy_branch
- name: Prepare Crates
if: inputs.crate
run: |
# Update cargo version,
node ./.backstage/version.cjs
git add ./toolproof/Cargo.toml
# Commit changes so cargo doesn't complain about dirty repo
git commit -m "Deploy changes."
- name: Build
if: inputs.crate
run: cargo build --release --verbose
- name: Authenticate with crates.io
if: inputs.crate
id: crates-io-auth
uses: rust-lang/crates-io-auth-action@bbd81622f20ce9e2dd9622e3218b975523e45bbe # v1.0.4
- name: Publish Crate
if: inputs.crate
working-directory: ./toolproof
run: |
if curl -sf -A "toolproof-release/1.0 (github.com/pagefind/toolproof)" "https://crates.io/api/v1/crates/toolproof/$GIT_VERSION" | grep -q '"num"'; then
echo "Crate already published, skipping."
else
cargo publish --allow-dirty
fi
env:
CARGO_REGISTRY_TOKEN: ${{ steps.crates-io-auth.outputs.token }}