Release #45
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 }} |