omni-dev 0.29.0

A powerful Git commit message analysis and amendment toolkit
Documentation
name: CI

on:
  push:
    branches: [ main ]
    tags: [ 'v*' ]
  pull_request:
    branches: [ main ]

env:
  CARGO_TERM_COLOR: always

jobs:
  test:
    name: Test
    runs-on: ubuntu-latest
    strategy:
      matrix:
        rust:
          - stable
          - beta
          - nightly
        features:
          - ""
          - "mcp"
    steps:
    - uses: actions/checkout@v6
    - uses: dtolnay/rust-toolchain@master
      with:
        toolchain: ${{ matrix.rust }}
        components: rustfmt
    - uses: Swatinem/rust-cache@v2
    - name: Install ALSA headers for cpal
      run: sudo apt-get update && sudo apt-get install -y libasound2-dev
    - name: Run tests
      run: |
        if [ -z "${{ matrix.features }}" ]; then
          cargo test --verbose
        else
          cargo test --features "${{ matrix.features }}" --verbose
        fi

  mcp-build:
    name: MCP Release Build
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v6
    - uses: dtolnay/rust-toolchain@stable
    - uses: Swatinem/rust-cache@v2
    - name: Install ALSA headers for cpal
      run: sudo apt-get update && sudo apt-get install -y libasound2-dev
    - name: Build omni-dev-mcp (release)
      run: cargo build --release --features mcp --bin omni-dev-mcp

  fmt:
    name: Rustfmt
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v6
    - uses: dtolnay/rust-toolchain@stable
      with:
        components: rustfmt
    - name: Check formatting
      run: cargo fmt --all -- --check

  clippy:
    name: Clippy
    runs-on: ubuntu-latest
    strategy:
      matrix:
        features:
          - ""
          - "mcp"
    steps:
    - uses: actions/checkout@v6
    - uses: dtolnay/rust-toolchain@stable
      with:
        components: clippy
    - uses: Swatinem/rust-cache@v2
    - name: Install ALSA headers for cpal
      run: sudo apt-get update && sudo apt-get install -y libasound2-dev
    - name: Run clippy
      run: |
        if [ -z "${{ matrix.features }}" ]; then
          cargo clippy --all-targets -- -D warnings
        else
          cargo clippy --all-targets --features "${{ matrix.features }}" -- -D warnings
        fi

  docs:
    name: Docs
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v6
    - uses: dtolnay/rust-toolchain@stable
    - uses: Swatinem/rust-cache@v2
    - name: Install ALSA headers for cpal
      run: sudo apt-get update && sudo apt-get install -y libasound2-dev
    - name: Build docs
      run: cargo doc --no-deps --document-private-items

  coverage:
    name: Coverage
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: write        # required to post the coverage comment
    steps:
    - uses: actions/checkout@v6
      with:
        fetch-depth: 0          # full history so `git merge-base` can resolve the PR's fork point
    - uses: dtolnay/rust-toolchain@stable
      with:
        components: llvm-tools-preview
    - uses: Swatinem/rust-cache@v2
    - name: Install ALSA headers for cpal
      run: sudo apt-get update && sudo apt-get install -y libasound2-dev
    - name: Install cargo-llvm-cov
      uses: taiki-e/install-action@cargo-llvm-cov
    # Every cargo step below sources cargo-llvm-cov's instrumentation env (rustc
    # wrapper, flags, profile path) so they all share ONE set of dependency
    # artifacts: the tests build them, and the omni-dev binary build reuses them
    # (a plain `cargo build` differs in RUSTC_WRAPPER and would recompile the
    # whole tree). `show-env` is deterministic, so sourcing it per step
    # re-establishes the same env without leaking it into the worktree fallback
    # below (which must run its own, separate coverage at the merge-base).
    # This is the documented manual equivalent of the all-in-one `cargo llvm-cov`.
    - name: Run tests with coverage
      run: |
        # shellcheck disable=SC1090
        source <(cargo llvm-cov show-env --sh)
        cargo llvm-cov clean --workspace
        cargo test --all-features --workspace
    - name: Generate codecov.json
      # Kept ready so the disabled upload below works as soon as it is re-enabled.
      run: |
        # shellcheck disable=SC1090
        source <(cargo llvm-cov show-env --sh)
        cargo llvm-cov report --codecov --output-path codecov.json
    - name: Generate head coverage report (lcov)
      # Per-line lcov for the head commit. This is the input to
      # `omni-dev coverage diff`: it drives patch coverage (of the lines this PR
      # added, how many are covered — needs only the head report + diff) and,
      # paired with the merge-base baseline below, the per-file deltas and
      # indirect-change detection. Published as the baseline on `main` for PRs.
      run: |
        # shellcheck disable=SC1090
        source <(cargo llvm-cov show-env --sh)
        cargo llvm-cov report --lcov --output-path coverage-head.lcov
    - name: Build coverage summary
      # Full per-file summary: appended to the run's Summary tab and published as
      # a build artifact (see "Upload coverage artifacts"). It is intentionally
      # NOT inlined into the PR comment, which shows changed files and patch lines.
      run: |
        # shellcheck disable=SC1090
        source <(cargo llvm-cov show-env --sh)
        cargo llvm-cov report --summary-only | tee coverage-summary.txt
        {
          echo '## Coverage'
          echo '```'
          cat coverage-summary.txt
          echo '```'
        } >> "$GITHUB_STEP_SUMMARY"
    # No separate binary build: `cargo test` above already compiled the
    # `omni-dev` executable (target/debug/omni-dev, part of the test build graph)
    # with the same instrumentation env. The PR-comment step below runs it
    # directly. Building it separately with `cargo build --bin` would recompile a
    # dozen crates whose features differ once dev-dependencies drop out.
    - name: Upload coverage artifacts
      # The full summary lives here (not in the PR comment); codecov.json kept for
      # when the upload below is re-enabled. Runs before the comment so the comment
      # can link to this artifact via the step's `artifact-url` output.
      id: upload
      uses: actions/upload-artifact@v7
      with:
        name: coverage-summary
        path: |
          coverage-summary.txt
          codecov.json
          coverage-head.lcov
    - name: Determine merge-base
      # The PR's fork point on main. Pinning the baseline to this immutable commit
      # (rather than "latest main") makes per-file deltas attributable to THIS PR
      # alone — other PRs merging into main while this one is open can't leak into
      # the numbers, and there is no race with in-flight main runs.
      id: mb
      if: github.event_name == 'pull_request'
      run: echo "sha=$(git merge-base origin/main HEAD)" >> "$GITHUB_OUTPUT"
    - name: Download baseline coverage for merge-base
      # Pulls the per-line lcov published by the main run for the exact merge-base
      # commit. A miss (fork point predates the feature / artifact retention) is a
      # warning, not a failure — the fallback below recomputes it.
      id: baseline
      if: github.event_name == 'pull_request'
      uses: dawidd6/action-download-artifact@v21
      with:
        name: coverage-baseline
        commit: ${{ steps.mb.outputs.sha }}
        workflow: ci.yml
        path: baseline
        if_no_artifact_found: warn
    - name: Compute baseline from merge-base (fallback)
      # Runs whenever the merge-base has no usable lcov baseline: a cold start,
      # an expired artifact, or a pre-migration aggregate-only artifact (which
      # the download above fetches but which lacks the lcov). Builds coverage at
      # that exact commit in a worktree, so the comparison stays correct (and
      # still PR-attributable). A no-op when the lcov baseline was downloaded.
      if: github.event_name == 'pull_request'
      run: |
        if [ -f baseline/coverage-head.lcov ]; then
          echo "lcov baseline already present for the merge-base; skipping recompute"
          exit 0
        fi
        git worktree add ../base "${{ steps.mb.outputs.sha }}"
        ( cd ../base && cargo llvm-cov --all-features --workspace --no-report \
            && cargo llvm-cov report --lcov --output-path coverage-head.lcov )
        mkdir -p baseline
        # Rewrite the worktree's absolute SF paths to the main workspace prefix so
        # `omni-dev coverage diff` strips one prefix for both head and baseline.
        sed "s#$(cd ../base && pwd)/#$GITHUB_WORKSPACE/#g" \
          ../base/coverage-head.lcov > baseline/coverage-head.lcov
    - name: Build PR coverage comment
      # Single comprehensive comment: total + delta vs main, the per-file
      # before/after table, PATCH COVERAGE (what this PR's added lines cover, with
      # the exact uncovered file:line list), and indirect coverage flips. The
      # baseline is optional — without it, patch coverage and the uncovered-line
      # list still render (deltas/indirect are then omitted with an explanation).
      if: github.event_name == 'pull_request'
      env:
        ARTIFACT_URL: ${{ steps.upload.outputs.artifact-url }}
        RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
        BASE_SHA: ${{ steps.mb.outputs.sha }}
        HEAD_SHA: ${{ github.event.pull_request.head.sha }}
        COMMIT_URL: ${{ github.server_url }}/${{ github.repository }}/commit
      run: |
        args=(coverage diff
          --report coverage-head.lcov
          --base-ref "$BASE_SHA"
          --format markdown
          --collapse-ranges
          --artifact-url "$ARTIFACT_URL"
          --run-url "$RUN_URL"
          --base-sha "$BASE_SHA"
          --head-sha "$HEAD_SHA"
          --commit-url "$COMMIT_URL")
        # Diff is computed merge-base..HEAD (the checked-out commit the coverage
        # was measured on), attributing patch coverage to exactly this PR.
        if [ -f baseline/coverage-head.lcov ]; then
          args+=(--baseline-report baseline/coverage-head.lcov)
        fi
        ./target/debug/omni-dev "${args[@]}" > coverage.md
    - name: Comment coverage on PR
      if: github.event_name == 'pull_request'
      uses: marocchino/sticky-pull-request-comment@v3
      with:
        header: coverage          # sticky key: updates the same comment each run
        path: coverage.md
    - name: Publish coverage baseline
      # On main only: this run's per-line lcov becomes the baseline that
      # subsequent PRs download above to compute deltas and indirect changes.
      if: github.ref == 'refs/heads/main' && github.event_name == 'push'
      uses: actions/upload-artifact@v7
      with:
        name: coverage-baseline
        path: coverage-head.lcov
    # Publishing to codecov.io is temporarily DISABLED: an upstream outage in the
    # codecov-action GPG uploader-key import randomly fails signature verification
    # (codecov/codecov-action#1876). The printed summary + PR comment above stand
    # in for it. Re-enable by changing `if: false` back to a real condition once
    # Codecov's uploader key server recovers.
    - name: Upload to codecov.io
      if: false
      uses: codecov/codecov-action@v7
      with:
        token: ${{ secrets.CODECOV_TOKEN }}
        files: codecov.json
        fail_ci_if_error: true
    - name: Enforce coverage threshold
      # Runs last so the summary and PR comment are posted even when the gate fails.
      run: |
        # shellcheck disable=SC1090
        source <(cargo llvm-cov show-env --sh)
        cargo llvm-cov report --summary-only --fail-under-lines 30

  audit:
    name: Security Audit
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v6
    - name: Install cargo-audit
      run: cargo install cargo-audit --locked
    - name: Run cargo-audit
      run: cargo audit

  deny:
    name: Dependency Policy
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v6
    - uses: EmbarkStudios/cargo-deny-action@v2.0.20
      with:
        command: check all

  secrets:
    name: Secret Scanning
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v6
      with:
        fetch-depth: 0
    - name: TruffleHog scan
      uses: trufflesecurity/trufflehog@main
      with:
        extra_args: --only-verified