chore(deps): update ferrlabs/.github digest to 006179d (#1173) #2160
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: CI | |
| on: | |
| push: | |
| branches: [main] | |
| # The release job lives in this workflow, and .ferrflow declares one | |
| # package at path ".", so any conventional commit here drives the CLI | |
| # version whatever files it touches. A docs-only merge must not be able | |
| # to publish a binary to crates.io and eight npm packages, none of which | |
| # can be withdrawn. A push touching anything outside docs/site still | |
| # matches and runs the full workflow, so a change that ships code and its | |
| # documentation together releases as normal. | |
| paths-ignore: ['docs/site/**'] | |
| pull_request: | |
| branches: [main] | |
| workflow_dispatch: | |
| inputs: | |
| dry_run: | |
| description: 'Dry run' | |
| type: boolean | |
| default: false | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }}${{ startsWith(github.event.head_commit.message, 'chore(release):') && format('-{0}', github.sha) || '' }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| permissions: | |
| contents: read | |
| jobs: | |
| test: | |
| name: Test | |
| if: github.event_name != 'push' || !startsWith(github.event.head_commit.message, 'chore(release):') | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| - uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable | |
| with: | |
| components: clippy, rustfmt | |
| - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 | |
| - uses: taiki-e/install-action@67729d5c413db75907f0ad1e39bb04b9c868ff60 # v2 | |
| with: | |
| tool: cargo-nextest | |
| - name: Format check | |
| run: cargo fmt --check | |
| - name: Clippy | |
| run: cargo clippy -- -D warnings | |
| # The wasm package builds the library with default-features off, a | |
| # configuration nothing else in CI compiles. Without this, a module | |
| # that reaches for a cli-gated dependency only fails at publish time. | |
| - name: Check the no-cli build surface | |
| run: cargo check -p ferrflow-wasm | |
| - name: Tests (nextest) | |
| run: cargo nextest run --no-fail-fast | |
| security: | |
| name: Cargo Security | |
| if: github.event_name != 'push' || !startsWith(github.event.head_commit.message, 'chore(release):') | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable | |
| - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 | |
| - uses: taiki-e/install-action@67729d5c413db75907f0ad1e39bb04b9c868ff60 # v2 | |
| with: | |
| tool: cargo-audit,cargo-deny,cargo-machete,cargo-vet | |
| - name: cargo audit (RustSec advisories) | |
| run: cargo audit --deny warnings | |
| - name: cargo deny (licenses + bans + sources) | |
| run: cargo deny --all-features check | |
| - name: cargo machete (unused deps) | |
| run: cargo machete | |
| # A Renovate pull request that only moves Cargo.lock is a refresh of the | |
| # transitive tree, and cargo vet then reports every crate that changed | |
| # version as uncertified. That is tens of thousands of lines nobody reads, | |
| # every week, on a required check, which teaches people to click through a | |
| # gate that is supposed to mean something. Where the answer is always the | |
| # same the result is recorded rather than enforced. | |
| # | |
| # The author check is load-bearing, not decoration. Editing Cargo.lock | |
| # alone is enough to move a transitive crate to another version the | |
| # existing Cargo.toml constraints already allow, which is precisely the | |
| # unreviewed-code case cargo vet exists for and the one cargo audit, deny | |
| # and osv-scanner do not cover: they catch known-bad crates, not new ones. | |
| # Anyone but the bot keeps the hard gate, and so does main. | |
| - name: Is this a lockfile-only refresh | |
| id: vet_scope | |
| if: >- | |
| github.event_name == 'pull_request' && | |
| github.event.pull_request.user.login == 'ferrlabs-renovate[bot]' | |
| env: | |
| BASE: ${{ github.event.pull_request.base.sha }} | |
| run: | | |
| changed=$(git diff --name-only "$BASE...HEAD") | |
| if echo "$changed" | grep -qE '(^|/)Cargo\.toml$'; then | |
| echo "advisory=false" >> "$GITHUB_OUTPUT" | |
| elif echo "$changed" | grep -qE '(^|/)Cargo\.lock$'; then | |
| echo "advisory=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "advisory=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: cargo vet (supply-chain audit) | |
| id: vet | |
| continue-on-error: ${{ steps.vet_scope.outputs.advisory == 'true' }} | |
| run: cargo vet | |
| - name: Record an advisory cargo vet failure | |
| if: steps.vet.outcome == 'failure' && steps.vet_scope.outputs.advisory == 'true' | |
| run: | | |
| { | |
| echo '### cargo vet did not pass, and did not fail the build' | |
| echo | |
| echo 'This is a Renovate pull request that moves `Cargo.lock` without touching any' | |
| echo '`Cargo.toml`, where the step is advisory. It failed; the log above holds the' | |
| echo 'reason. Usually that is the refreshed crates being uncertified, but the' | |
| echo 'exemption is not conditional on the reason, so read the output before assuming' | |
| echo 'it is routine.' | |
| echo | |
| echo 'It still fails the build on `main`, on any pull request that edits a' | |
| echo '`Cargo.toml`, and on anything not opened by the bot. See FerrLabs/FerrFlow#1017.' | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| build: | |
| name: Build Release Binary | |
| # Deliberately not `needs: test`. Nothing here consumes a test result, and | |
| # the wait put the release build, the fixture jobs behind it and therefore | |
| # the whole merge gate 1m10 later than they need to be. A failing test still | |
| # fails the run; it just no longer holds the queue. | |
| if: github.event_name != 'push' || !startsWith(github.event.head_commit.message, 'chore(release):') | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| - uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable | |
| - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 | |
| - name: Build ferrflow | |
| run: cargo build --release | |
| - name: Upload binary | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| with: | |
| name: ferrflow-binary | |
| path: target/release/ferrflow | |
| retention-days: 1 | |
| fixture-generate: | |
| name: Generate Fixtures | |
| if: github.event_name != 'push' || !startsWith(github.event.head_commit.message, 'chore(release):') | |
| runs-on: ubuntu-latest | |
| outputs: | |
| cache-key: ${{ steps.fixture-cache.outputs.cache-primary-key }} | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| - name: Look up generated fixtures | |
| id: fixture-cache | |
| uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 | |
| with: | |
| path: fixtures-generated | |
| key: fixtures-${{ hashFiles('tests/fixtures/definitions/**') }} | |
| lookup-only: true | |
| - name: Generate fixtures | |
| if: steps.fixture-cache.outputs.cache-hit != 'true' | |
| uses: FerrLabs/Fixtures@7b04ba1b93ababf2b557ca4521a243c23217ef0a # v1.2.2 (packed repos) | |
| with: | |
| definitions: tests/fixtures/definitions | |
| - name: Resolve generated path | |
| if: steps.fixture-cache.outputs.cache-hit != 'true' | |
| run: | | |
| if [ ! -d "fixtures-generated" ]; then | |
| for d in /tmp/tmp.*/generated; do | |
| if [ -d "$d" ]; then | |
| cp -r "$d" fixtures-generated | |
| break | |
| fi | |
| done | |
| fi | |
| - name: Save generated fixtures | |
| if: steps.fixture-cache.outputs.cache-hit != 'true' | |
| uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 | |
| with: | |
| path: fixtures-generated | |
| key: ${{ steps.fixture-cache.outputs.cache-primary-key }} | |
| fixture-monorepo: | |
| name: Fixtures — Monorepo | |
| needs: [build, fixture-generate] | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 | |
| with: | |
| name: ferrflow-binary | |
| path: bin/ | |
| - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 | |
| with: | |
| path: fixtures-generated | |
| key: ${{ needs.fixture-generate.outputs.cache-key }} | |
| fail-on-cache-miss: true | |
| - name: Run monorepo fixture tests | |
| env: | |
| FERRFLOW_BIN: ./bin/ferrflow | |
| DIFF_DIR: fixture-diffs | |
| run: | | |
| chmod +x ./bin/ferrflow | |
| mkdir -p filtered-fixtures | |
| for d in fixtures-generated/monorepo-*; do | |
| [ -d "$d" ] && cp -r "$d" filtered-fixtures/ | |
| done | |
| bash tests/fixtures/run-tests.sh filtered-fixtures | |
| - name: Upload snapshot diffs | |
| if: failure() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| with: | |
| name: fixture-diffs-monorepo | |
| path: fixture-diffs/ | |
| retention-days: 7 | |
| if-no-files-found: ignore | |
| fixture-single: | |
| name: Fixtures — Single Package | |
| needs: [build, fixture-generate] | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 | |
| with: | |
| name: ferrflow-binary | |
| path: bin/ | |
| - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 | |
| with: | |
| path: fixtures-generated | |
| key: ${{ needs.fixture-generate.outputs.cache-key }} | |
| fail-on-cache-miss: true | |
| - name: Run single-package fixture tests | |
| env: | |
| FERRFLOW_BIN: ./bin/ferrflow | |
| DIFF_DIR: fixture-diffs | |
| run: | | |
| chmod +x ./bin/ferrflow | |
| mkdir -p filtered-fixtures | |
| for d in fixtures-generated/single-* fixtures-generated/multiple-* fixtures-generated/no-tags-initial-* fixtures-generated/no-versioned-* fixtures-generated/version-*; do | |
| [ -d "$d" ] && cp -r "$d" filtered-fixtures/ | |
| done | |
| bash tests/fixtures/run-tests.sh filtered-fixtures | |
| - name: Upload snapshot diffs | |
| if: failure() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| with: | |
| name: fixture-diffs-single | |
| path: fixture-diffs/ | |
| retention-days: 7 | |
| if-no-files-found: ignore | |
| fixture-config: | |
| name: Fixtures — Config & Formats | |
| needs: [build, fixture-generate] | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 | |
| with: | |
| name: ferrflow-binary | |
| path: bin/ | |
| - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 | |
| with: | |
| path: fixtures-generated | |
| key: ${{ needs.fixture-generate.outputs.cache-key }} | |
| fail-on-cache-miss: true | |
| - name: Run config fixture tests | |
| env: | |
| FERRFLOW_BIN: ./bin/ferrflow | |
| DIFF_DIR: fixture-diffs | |
| run: | | |
| chmod +x ./bin/ferrflow | |
| mkdir -p filtered-fixtures | |
| for d in fixtures-generated/config-* fixtures-generated/format-* fixtures-generated/multi-versioned-*; do | |
| [ -d "$d" ] && cp -r "$d" filtered-fixtures/ | |
| done | |
| bash tests/fixtures/run-tests.sh filtered-fixtures | |
| - name: Upload snapshot diffs | |
| if: failure() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| with: | |
| name: fixture-diffs-config | |
| path: fixture-diffs/ | |
| retention-days: 7 | |
| if-no-files-found: ignore | |
| fixture-advanced: | |
| name: Fixtures — Advanced Features | |
| needs: [build, fixture-generate] | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 | |
| with: | |
| name: ferrflow-binary | |
| path: bin/ | |
| - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 | |
| with: | |
| path: fixtures-generated | |
| key: ${{ needs.fixture-generate.outputs.cache-key }} | |
| fail-on-cache-miss: true | |
| - name: Run advanced fixture tests | |
| env: | |
| FERRFLOW_BIN: ./bin/ferrflow | |
| DIFF_DIR: fixture-diffs | |
| run: | | |
| chmod +x ./bin/ferrflow | |
| mkdir -p filtered-fixtures | |
| for d in fixtures-generated/prerelease-* fixtures-generated/hooks-* fixtures-generated/floating-* \ | |
| fixtures-generated/tag-* fixtures-generated/changelog-* fixtures-generated/versioning-* \ | |
| fixtures-generated/orphaned-* fixtures-generated/recover-* fixtures-generated/release-* \ | |
| fixtures-generated/head-* fixtures-generated/merge-* fixtures-generated/commit-* \ | |
| fixtures-generated/skip-* fixtures-generated/no-tags-monorepo-*; do | |
| [ -d "$d" ] && cp -r "$d" filtered-fixtures/ | |
| done | |
| bash tests/fixtures/run-tests.sh filtered-fixtures | |
| - name: Upload snapshot diffs | |
| if: failure() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| with: | |
| name: fixture-diffs-advanced | |
| path: fixture-diffs/ | |
| retention-days: 7 | |
| if-no-files-found: ignore | |
| # Compile the bench binary ONCE per PR and hand it to the matrix shards | |
| # below. Before this split, every shard ran `cargo bench` with its own | |
| # cold rust-cache (all 11 shards start in parallel and share a key, so | |
| # nobody hits a sibling's cache), paying the ~5-7 min build cost 11 | |
| # times. With a pre-built binary the matrix only does the actual | |
| # measurement step. See FerrLabs/FerrFlow#397. | |
| micro-bench-build: | |
| name: Build micro bench binary | |
| if: >- | |
| (github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| && !startsWith(github.event.head_commit.message, 'chore(release):')) | |
| || github.event_name == 'workflow_dispatch' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| - uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable | |
| - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 | |
| - name: Compile bench binary (no run) | |
| run: cargo bench --bench ferrflow_benchmarks --no-run | |
| - name: Locate compiled binary | |
| id: locate | |
| run: | | |
| set -euo pipefail | |
| # criterion + cargo emit a hashed binary name like | |
| # ferrflow_benchmarks-1234abcd. Pick the most recent matching | |
| # executable and skip dSYMs, .d files, and pdbs. | |
| bin=$(find target/release/deps -maxdepth 1 -type f \ | |
| -name 'ferrflow_benchmarks-*' \ | |
| ! -name '*.d' ! -name '*.pdb' ! -name '*.dSYM' \ | |
| -printf '%T@ %p\n' \ | |
| | sort -nr | head -n1 | cut -d' ' -f2-) | |
| if [ -z "$bin" ] || [ ! -x "$bin" ]; then | |
| echo "::error::could not find compiled bench binary under target/release/deps" | |
| ls -la target/release/deps/ferrflow_benchmarks-* 2>/dev/null || true | |
| exit 1 | |
| fi | |
| echo "bin=$bin" >> "$GITHUB_OUTPUT" | |
| # Strip the hash so the matrix doesn't have to re-derive it. | |
| cp "$bin" target/release/deps/ferrflow_benchmarks | |
| - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| with: | |
| name: micro-bench-binary | |
| path: target/release/deps/ferrflow_benchmarks | |
| retention-days: 1 | |
| if-no-files-found: error | |
| micro-bench: | |
| name: Micro Benchmark - ${{ matrix.group }} | |
| needs: micro-bench-build | |
| if: >- | |
| (github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| && !startsWith(github.event.head_commit.message, 'chore(release):')) | |
| || github.event_name == 'workflow_dispatch' | |
| runs-on: ubuntu-latest | |
| continue-on-error: true | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| group: | |
| - commit_parsing | |
| - changelog | |
| - version_files | |
| - config_loading | |
| - git_commits | |
| - git_find_tag | |
| - git_collect_tags | |
| - git_changed_files | |
| - git_changed_since_tag | |
| - validate | |
| - full_check_flow | |
| - full_monorepo_flow | |
| steps: | |
| - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 | |
| with: | |
| name: micro-bench-binary | |
| path: bench-bin/ | |
| - name: Run group ${{ matrix.group }} | |
| # Mirrors what FerrLabs/Benchmarks@v3 used to do for the micro | |
| # path: invoke the bench binary directly with the criterion | |
| # positional filter, capture bencher-format stdout, and keep | |
| # only the well-formed `test ... bench: ...` rows so the aggregate | |
| # job's benchmark-action consumer doesn't choke on noise. | |
| # The bench binary's fixtures are all in-process (tempdir + | |
| # libgit2), so no checkout is needed in this job. | |
| run: | | |
| set +e | |
| chmod +x bench-bin/ferrflow_benchmarks | |
| ./bench-bin/ferrflow_benchmarks --bench --output-format bencher \ | |
| "${{ matrix.group }}" \ | |
| 2> bench-stderr.log | tee raw-output.txt | |
| bench_exit=${PIPESTATUS[0]} | |
| set -e | |
| if [ "$bench_exit" != "0" ]; then | |
| echo "::warning::bench binary exited ${bench_exit} — tolerating as long as rows were captured. Stderr tail:" | |
| tail -40 bench-stderr.log || true | |
| fi | |
| grep -E '^test .+ \.\.\. bench:[[:space:]]+[0-9,]+ ns/iter' raw-output.txt > output.txt || true | |
| echo "---" | |
| echo "Filtered bencher output ($(wc -l < output.txt) lines kept):" | |
| cat output.txt | |
| if [ ! -s output.txt ]; then | |
| echo "::error::no well-formed benchmark rows captured; bench exit was ${bench_exit}" | |
| exit 1 | |
| fi | |
| - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| with: | |
| name: micro-partial-${{ matrix.group }} | |
| path: output.txt | |
| retention-days: 1 | |
| micro-bench-aggregate: | |
| name: Micro Benchmark (aggregate) | |
| needs: micro-bench | |
| if: >- | |
| (github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| && !startsWith(github.event.head_commit.message, 'chore(release):')) | |
| || github.event_name == 'workflow_dispatch' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| # A job-level block zeroes every scope it omits. The artifact lookup and | |
| # the cross-run download both read the Actions API, which happens to work | |
| # unauthenticated on a public repository and would 403 the day this one | |
| # turns private. | |
| actions: read | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 | |
| with: | |
| pattern: micro-partial-* | |
| merge-multiple: true | |
| path: shards/ | |
| - name: Merge partials | |
| run: | | |
| cat shards/* > output.txt | |
| echo "Merged bench rows: $(wc -l < output.txt)" | |
| - name: Find baseline artifact from main | |
| id: find-baseline | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| RUN_ID=$(gh api "repos/${{ github.repository }}/actions/artifacts?name=criterion-baseline&per_page=1" \ | |
| --jq '.artifacts[0].workflow_run.id // empty') | |
| echo "run_id=${RUN_ID}" >> "$GITHUB_OUTPUT" | |
| continue-on-error: true | |
| - name: Download baseline | |
| if: steps.find-baseline.outputs.run_id | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 | |
| with: | |
| name: criterion-baseline | |
| path: baseline/ | |
| run-id: ${{ steps.find-baseline.outputs.run_id }} | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| continue-on-error: true | |
| - name: Ensure baseline file | |
| run: | | |
| if [[ ! -f baseline/benchmark-data.json ]]; then | |
| mkdir -p baseline | |
| echo '[]' > baseline/benchmark-data.json | |
| fi | |
| - uses: benchmark-action/github-action-benchmark@4322e5726e6334590d251fc4f92bec0efafc45dc # v1.22.2 | |
| with: | |
| tool: cargo | |
| output-file-path: output.txt | |
| external-data-json-path: baseline/benchmark-data.json | |
| comment-on-alert: true | |
| alert-threshold: '120%' | |
| fail-on-alert: false | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| comment-always: true | |
| # The step above appends this run to benchmark-data.json in place. Nothing | |
| # published it, so the download above has been finding only artifacts from | |
| # before #379, all long expired, and every comparison has been against an | |
| # empty history. Publishing it closes the loop. | |
| - name: Publish the updated baseline | |
| # Read on any trigger, write only from a push. A dispatch run off a | |
| # feature branch would otherwise become the newest baseline and the | |
| # next main run would compare against another machine's numbers. | |
| if: github.event_name == 'push' | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| with: | |
| name: criterion-baseline | |
| path: baseline/benchmark-data.json | |
| retention-days: 90 | |
| if-no-files-found: error | |
| bench-fixtures: | |
| name: Generate Benchmark Fixtures | |
| needs: test | |
| runs-on: ubuntu-latest | |
| if: (github.event_name == 'push' && github.ref == 'refs/heads/main') || github.event_name == 'workflow_dispatch' | |
| outputs: | |
| cache-key: ${{ steps.bench-cache.outputs.cache-primary-key }} | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| - name: Look up benchmark fixtures | |
| id: bench-cache | |
| uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 | |
| with: | |
| path: bench-fixtures | |
| key: bench-fixtures-${{ hashFiles('benchmarks/fixtures/definitions/**') }} | |
| lookup-only: true | |
| - name: Generate benchmark fixtures | |
| if: steps.bench-cache.outputs.cache-hit != 'true' | |
| uses: FerrLabs/Fixtures@7b04ba1b93ababf2b557ca4521a243c23217ef0a # v1.2.2 (packed repos) | |
| with: | |
| definitions: benchmarks/fixtures/definitions | |
| generated-dir: bench-fixtures | |
| - name: Save benchmark fixtures | |
| if: steps.bench-cache.outputs.cache-hit != 'true' | |
| uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 | |
| with: | |
| path: bench-fixtures | |
| key: ${{ steps.bench-cache.outputs.cache-primary-key }} | |
| benchmark: | |
| name: Benchmark - ${{ matrix.fixture }} | |
| # One shard per fixture, so wall-clock is the slowest fixture rather than | |
| # the sum of all of them (mono-large dominates). Sharded per fixture and | |
| # never per tool: ferrflow-vs-competitor only means something when both ran | |
| # on the same machine, so every fixture's tools stay inside one runner. | |
| # | |
| # No Rust toolchain here on purpose: the binary comes from `build` and the | |
| # fixtures from `bench-fixtures`, so nothing in this job compiles. | |
| needs: [test, build, bench-fixtures] | |
| runs-on: ubuntu-latest | |
| if: (github.event_name == 'push' && github.ref == 'refs/heads/main') || github.event_name == 'workflow_dispatch' | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| fixture: [single, mono-small, mono-medium, mono-100-1k, mono-50-5k, mono-large, complex] | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 | |
| with: | |
| name: ferrflow-binary | |
| path: bin/ | |
| - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 | |
| with: | |
| path: bench-fixtures | |
| key: ${{ needs.bench-fixtures.outputs.cache-key }} | |
| fail-on-cache-miss: true | |
| - name: Isolate this shard's fixture | |
| env: | |
| FIXTURE: ${{ matrix.fixture }} | |
| run: | | |
| if [[ ! -d "bench-fixtures/$FIXTURE" ]]; then | |
| echo "::error::generated fixtures contain no '$FIXTURE'" | |
| ls -la bench-fixtures || true | |
| exit 1 | |
| fi | |
| mkdir -p shard-fixtures | |
| cp -r "bench-fixtures/$FIXTURE" shard-fixtures/ | |
| # The floor rides along in every shard, never as a shard of its own: | |
| # startup is a property of this runner, and shards land on different | |
| # machines, so subtracting one shard's floor from another's total | |
| # would measure the hardware gap instead of the work. | |
| if [[ ! -d "bench-fixtures/floor" ]]; then | |
| echo "::error::generated fixtures contain no 'floor'" | |
| exit 1 | |
| fi | |
| cp -r bench-fixtures/floor shard-fixtures/ | |
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 | |
| with: | |
| node-version: '24' | |
| # Cache ~/.npm so the competitor npm-global installs that | |
| # `Benchmarks/scripts/run.sh` does (`npm install -g | |
| # @changesets/cli`, `commit-and-tag-version`, `standard-version`, | |
| # `semantic-release@23`, the `npx --yes release-please` size | |
| # measurement) reuse downloaded tarballs across runs instead of | |
| # re-fetching them every time. Key on the file that pins the | |
| # competitor set + their versions, with a wide fallback so a | |
| # version change still benefits from the previous cache shape. | |
| - name: Cache competitor npm globals | |
| uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 | |
| with: | |
| path: | | |
| ~/.npm | |
| ~/.cache/npx | |
| key: bench-npm-cache-${{ hashFiles('.github/workflows/ci.yml', 'benchmarks/fixtures/definitions/*.json') }} | |
| restore-keys: | | |
| bench-npm-cache- | |
| - uses: FerrLabs/Benchmarks@db1353fe328ac827ac3e1583167e43443c308655 # v5.6.3 (competitors bucketed as release-dry-run for a like-for-like head-to-head) | |
| with: | |
| type: full | |
| definitions: benchmarks/fixtures/definitions | |
| ferrflow-token: ${{ secrets.GITHUB_TOKEN }} | |
| skip-competitors: false | |
| verbose: true | |
| binary-dir: bin | |
| fixtures-dir: shard-fixtures | |
| shard: true | |
| # Sharding put wall-clock back at the slowest fixture, so spend the | |
| # headroom on samples: 30 runs tightens the median's standard error | |
| # by ~1.7x over the action's default of 10. | |
| runs: 30 | |
| # The action defaults to 2. Three is what the published methodology | |
| # has always claimed, and the extra pass is cheap now that each shard | |
| # only carries one fixture. | |
| warmup: 3 | |
| - name: Upload shard results | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| with: | |
| name: bench-partial-${{ matrix.fixture }} | |
| path: benchmarks/results/latest.json | |
| retention-days: 1 | |
| if-no-files-found: error | |
| benchmark-aggregate: | |
| name: Benchmark (aggregate) | |
| # Merges the shards and runs the regression check, comment and uploads once | |
| # over the whole result — the shards deliberately skip all of that. | |
| needs: benchmark | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 | |
| with: | |
| pattern: bench-partial-* | |
| path: partials/ | |
| - uses: FerrLabs/Benchmarks@db1353fe328ac827ac3e1583167e43443c308655 # v5.6.3 (competitors bucketed as release-dry-run for a like-for-like head-to-head) | |
| with: | |
| type: full | |
| definitions: benchmarks/fixtures/definitions | |
| ferrflow-token: ${{ secrets.GITHUB_TOKEN }} | |
| merge-partials: partials | |
| release: | |
| name: Release | |
| needs: [test, fixture-monorepo, fixture-single, fixture-config, fixture-advanced, benchmark-aggregate] | |
| runs-on: ubuntu-latest | |
| concurrency: | |
| group: release-${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: false | |
| if: | | |
| always() && | |
| needs.test.result == 'success' && | |
| needs.fixture-monorepo.result == 'success' && | |
| needs.fixture-single.result == 'success' && | |
| needs.fixture-config.result == 'success' && | |
| needs.fixture-advanced.result == 'success' && | |
| (needs.benchmark-aggregate.result == 'success' || needs.benchmark-aggregate.result == 'skipped') && | |
| ( | |
| (github.event_name == 'push' && github.ref == 'refs/heads/main') || | |
| github.event_name == 'workflow_dispatch' | |
| ) | |
| permissions: | |
| contents: write | |
| id-token: write | |
| env: | |
| # Only whether the key exists: `secrets` is not available in a step `if`, | |
| # and the key itself belongs to the one step that uses it rather than to | |
| # every step in this job, `cargo build` and its build scripts included. | |
| HAS_TAG_SIGNING_KEY: ${{ secrets.TAG_SIGNING_KEY != '' }} | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| with: | |
| fetch-depth: 0 | |
| # Don't bake GITHUB_TOKEN into the git remote — ferrflow's OIDC | |
| # exchange yields an App installation token that authenticates as | |
| # ferrflow[bot]. Without this, every git push silently reverts | |
| # to GITHUB_TOKEN (i.e. github-actions[bot]) which isn't in the | |
| # branch-rule bypass list and can't push to main. | |
| persist-credentials: false | |
| - uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable | |
| - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 | |
| - name: Build ferrflow | |
| run: cargo build --release | |
| # Git identity is configured by the binary itself (see | |
| # ensure_bot_token in src/bot_token.rs) when FERRFLOW_BOT=true. | |
| # Before the release, not after: `auto_stage_new_files` diffs the dirty | |
| # set around each hook, so anything already modified when ferrflow starts | |
| # is invisible to it. The hook reads this path and writes into | |
| # docs/site/data, which is how the numbers reach the published package. | |
| - name: Download benchmark data for the release | |
| id: bench | |
| if: needs.benchmark-aggregate.result == 'success' | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 | |
| with: | |
| name: hyperfine-baseline | |
| path: ${{ runner.temp }}/bench | |
| continue-on-error: true | |
| # Same setup the published action does for its callers. ferrflow[bot] is | |
| # a GitHub App, and an App holds no signing key: GitHub signs the commits | |
| # it creates through the API, but never a tag object, so the tag is | |
| # signed here with a key belonging to an account GitHub can check it | |
| # against. | |
| - name: Configure tag signing | |
| if: env.HAS_TAG_SIGNING_KEY == 'true' | |
| env: | |
| TAG_SIGNING_KEY: ${{ secrets.TAG_SIGNING_KEY }} | |
| TAG_SIGNING_NAME: ${{ vars.TAG_SIGNING_NAME }} | |
| TAG_SIGNING_EMAIL: ${{ vars.TAG_SIGNING_EMAIL }} | |
| run: | | |
| set -euo pipefail | |
| if [ -z "$TAG_SIGNING_NAME" ] || [ -z "$TAG_SIGNING_EMAIL" ]; then | |
| echo "::error::TAG_SIGNING_KEY needs the TAG_SIGNING_NAME and TAG_SIGNING_EMAIL repository variables." | |
| exit 1 | |
| fi | |
| key="${RUNNER_TEMP}/ferrflow-tag-signing-key" | |
| umask 077 | |
| printf '%s\n' "$TAG_SIGNING_KEY" > "$key" | |
| if ! ssh-keygen -y -f "$key" > "${key}.pub" 2>/dev/null; then | |
| echo "::error::TAG_SIGNING_KEY is not a usable OpenSSH private key. It must be unencrypted." | |
| exit 1 | |
| fi | |
| git config gpg.format ssh | |
| git config user.signingkey "$key" | |
| git config tag.gpgSign true | |
| { | |
| echo "GIT_COMMITTER_NAME=${TAG_SIGNING_NAME}" | |
| echo "GIT_COMMITTER_EMAIL=${TAG_SIGNING_EMAIL}" | |
| } >> "$GITHUB_ENV" | |
| - name: Run ferrflow release | |
| env: | |
| FERRFLOW_BOT: "true" | |
| FERRFLOW_BENCHMARK_JSON: ${{ steps.bench.outcome == 'success' && format('{0}/bench/latest.json', runner.temp) || '' }} | |
| run: ./target/release/ferrflow ${{ inputs.dry_run && '--dry-run' || '' }} release --draft | |
| - name: Download benchmark summary | |
| if: needs.benchmark-aggregate.result == 'success' | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 | |
| with: | |
| name: benchmark-release-summary | |
| path: benchmark-summary/ | |
| continue-on-error: true | |
| - name: Append benchmark results to draft release | |
| if: needs.benchmark-aggregate.result == 'success' | |
| id: append-bench | |
| run: | | |
| BENCH_FILE="benchmark-summary/release-summary.md" | |
| if [[ ! -f "$BENCH_FILE" ]]; then | |
| echo "No benchmark summary found, skipping" | |
| exit 0 | |
| fi | |
| TAG=$(./target/release/ferrflow tag --json 2>/dev/null | jq -r '.tag // empty') | |
| if [[ -z "$TAG" ]]; then | |
| echo "No tag found, skipping" | |
| exit 0 | |
| fi | |
| CURRENT_BODY=$(gh release view "$TAG" --json body --jq '.body' 2>/dev/null || echo "") | |
| BENCH=$(cat "$BENCH_FILE") | |
| # Remove existing Performance section to avoid duplicates | |
| CLEAN_BODY=$(echo "$CURRENT_BODY" | sed '/^## Performance$/,$d') | |
| printf -v NEW_BODY '%s\n\n%s' "$CLEAN_BODY" "$BENCH" | |
| gh release edit "$TAG" --notes "$NEW_BODY" | |
| echo "tag=$TAG" >> "$GITHUB_OUTPUT" | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |