ferrflow 2.17.0

Universal semantic versioning for monorepos and classic repos
Documentation
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
  workflow_dispatch:
    inputs:
      dry_run:
        description: 'Dry run'
        type: boolean
        default: false

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@v6
      - uses: dtolnay/rust-toolchain@nightly
        with:
          components: clippy, rustfmt
      - uses: Swatinem/rust-cache@v2
      - name: Format check
        run: cargo fmt --check
      - name: Clippy
        run: cargo clippy -- -D warnings
        env:
          FERRFLOW_HMAC_SECRET: ${{ secrets.FERRFLOW_HMAC_SECRET }}
      - name: Tests
        run: cargo test
        env:
          FERRFLOW_HMAC_SECRET: ${{ secrets.FERRFLOW_HMAC_SECRET }}

  coverage:
    name: Coverage
    if: github.event_name != 'push' || !startsWith(github.event.head_commit.message, 'chore(release):')
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: dtolnay/rust-toolchain@nightly
      - uses: Swatinem/rust-cache@v2
      - name: Install cargo-tarpaulin
        run: cargo install cargo-tarpaulin
      - name: Generate coverage
        run: cargo tarpaulin --out xml --skip-clean
        env:
          FERRFLOW_HMAC_SECRET: ${{ secrets.FERRFLOW_HMAC_SECRET }}
      - uses: codecov/codecov-action@v6
        with:
          files: cobertura.xml
          token: ${{ secrets.CODECOV_TOKEN }}
          fail_ci_if_error: false

  fixture-tests:
    name: Fixture Tests
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: dtolnay/rust-toolchain@nightly
      - uses: Swatinem/rust-cache@v2
      - name: Build ferrflow
        run: cargo build --release
        env:
          FERRFLOW_HMAC_SECRET: ${{ secrets.FERRFLOW_HMAC_SECRET }}
      - name: Generate fixtures
        id: fixtures
        uses: FerrFlow-Org/Fixtures@v0
        with:
          definitions: tests/fixtures/definitions
      - name: Run fixture tests
        env:
          FERRFLOW_BIN: ./target/release/ferrflow
        run: bash tests/fixtures/run-tests.sh ${{ steps.fixtures.outputs.generated-path }}

  micro-bench:
    name: Micro Benchmarks
    if: github.event_name == 'pull_request'
    runs-on: ubuntu-latest
    permissions:
      pull-requests: write
    steps:
      - uses: actions/checkout@v6
      - uses: dtolnay/rust-toolchain@nightly
      - uses: Swatinem/rust-cache@v2
      - uses: FerrFlow-Org/Benchmarks@v2
        with:
          type: micro
          definitions: benchmarks/fixtures/definitions
          ferrflow-token: ${{ secrets.GITHUB_TOKEN }}
        env:
          FERRFLOW_HMAC_SECRET: ${{ secrets.FERRFLOW_HMAC_SECRET }}

  benchmark:
    name: Benchmark
    needs: test
    runs-on: ubuntu-latest
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v6
      - uses: dtolnay/rust-toolchain@nightly
      - uses: Swatinem/rust-cache@v2
      - uses: actions/setup-node@v6
        with:
          node-version: '24'
      - uses: FerrFlow-Org/Benchmarks@v2
        with:
          type: full
          definitions: benchmarks/fixtures/definitions
          ferrflow-token: ${{ secrets.GITHUB_TOKEN }}
        env:
          FERRFLOW_HMAC_SECRET: ${{ secrets.FERRFLOW_HMAC_SECRET }}

  release:
    name: Release
    needs: [test, fixture-tests, benchmark]
    runs-on: ubuntu-latest
    concurrency:
      group: release-${{ github.workflow }}-${{ github.ref }}
      cancel-in-progress: false
    if: |
      always() &&
      needs.test.result == 'success' &&
      needs.fixture-tests.result == 'success' &&
      (needs.benchmark.result == 'success' || needs.benchmark.result == 'skipped') &&
      (
        (github.event_name == 'push' && github.ref == 'refs/heads/main') ||
        github.event_name == 'workflow_dispatch'
      )
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@v6
        with:
          fetch-depth: 0
          token: ${{ secrets.FERRFLOW_TOKEN }}
      - uses: dtolnay/rust-toolchain@nightly
      - uses: Swatinem/rust-cache@v2
      - name: Build ferrflow
        run: cargo build --release
        env:
          FERRFLOW_HMAC_SECRET: ${{ secrets.FERRFLOW_HMAC_SECRET }}
      - name: Run ferrflow release
        run: ./target/release/ferrflow ${{ inputs.dry_run == 'true' && '--dry-run' || '' }} release --draft
        env:
          FERRFLOW_TOKEN: ${{ secrets.FERRFLOW_TOKEN }}
          GITHUB_TOKEN: ${{ secrets.FERRFLOW_TOKEN }}
      - name: Download benchmark summary
        if: needs.benchmark.result == 'success'
        uses: actions/download-artifact@v8
        with:
          name: benchmark-release-summary
          path: benchmark-summary/
        continue-on-error: true
      - name: Append benchmark results to draft release
        if: needs.benchmark.result == 'success'
        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"
        env:
          GH_TOKEN: ${{ secrets.FERRFLOW_TOKEN }}