name: CI on: push: # dev -> qa -> main. `dev` is where work lands and is meant to be pushed # to often: these are the FAST checks, so a mistake surfaces in minutes. # `qa` is the release candidate — it runs these too, plus the expensive # suite in qa.yml. `main` only ever moves at release time, to a tagged # commit that was already green on qa. branches: [main, dev, qa] pull_request: # Rapid dev pushes supersede their own in-progress runs instead of stacking. concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true # Least privilege: no job here writes to the repo. permissions: contents: read # freemkv-unlock is the BASE of the whole ecosystem: libfreemkv path-deps it, # bdemu path-deps it with the `emulation` feature, and every other crate pulls # it in transitively. It has no freemkv dependencies of its own, so no sibling # checkout is needed here — but it is also the crate whose breakage takes all # seven others down with it, which is why it gets the full OS matrix rather # than a token Linux run. # # Cargo.lock is now committed (issue #15), so the resolved dependency graph is # recorded and the `deny` job audits it. `--locked` is still deliberately NOT # used, matching the binary crates' ci.yml: CI's sibling path-patch layout # resolves the freemkv crates as path packages, which `--locked` can't satisfy. jobs: # ── Single source of truth for the MSRV ───────────────────────────────────── # # `rust-version` in Cargo.toml is the ONE place the MSRV is declared. This job # reads it; every job that must build/test AT the MSRV consumes # `needs.resolve-msrv.outputs.v` instead of a hardcoded pin, so the floor # cannot drift out of sync across workflow files. The `test` job building # green at this value IS the verification that the declared MSRV is real. # clippy deliberately does NOT use it — it runs on `stable` (lint debt is # caught against current Rust). resolve-msrv: runs-on: ubuntu-latest outputs: v: ${{ steps.msrv.outputs.v }} steps: - uses: actions/checkout@v5 - id: msrv shell: bash run: echo "v=$(grep -m1 '^rust-version' Cargo.toml | sed -E 's/.*\"([^\"]+)\".*/\1/')" >> "$GITHUB_OUTPUT" # CI owns the changelog: main may never carry a released version without a # CHANGELOG.md entry for it. The release orchestrator dates an existing # `## [X.Y.Z]` heading — it does NOT invent one — so a version bump that # forgot its changelog entry must fail here rather than ship a blank release. # Hard-fails on main (and PRs targeting main); warns elsewhere so an # in-progress dev/qa branch whose entry isn't written yet is not blocked. changelog: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - name: Require a CHANGELOG entry for the crate version on main shell: bash run: | set -euo pipefail version=$(grep -m1 '^version = ' Cargo.toml | sed -E 's/.*"([^"]+)".*/\1/') if grep -qE "^## \[${version}\]" CHANGELOG.md; then echo "CHANGELOG.md has an entry for ${version}." exit 0 fi msg="CHANGELOG.md has no '## [${version}]' entry" if [ "${GITHUB_REF_NAME:-}" = "main" ] || [ "${GITHUB_BASE_REF:-}" = "main" ]; then echo "::error::${msg} — main requires a changelog entry for the released version." exit 1 fi echo "::warning::${msg} (allowed on ${GITHUB_REF_NAME:-unknown}; required before it can reach main)." lint: needs: resolve-msrv runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 # fmt runs on the pinned MSRV toolchain; clippy runs on stable. Linting on # stable makes new-lint debt a BLOCKING gate the pinned version would # otherwise hide. The MSRV pin is installed second so it stays the default # toolchain (fmt uses it implicitly); clippy is invoked with `+stable`. - uses: dtolnay/rust-toolchain@stable with: components: clippy - uses: dtolnay/rust-toolchain@master with: toolchain: ${{ needs.resolve-msrv.outputs.v }} components: rustfmt - uses: Swatinem/rust-cache@v2 with: shared-key: freemkv-unlock-ci - run: cargo fmt --check # --all-targets so the test code is linted too, not just the library. - run: cargo +stable clippy --all-targets -- -D warnings test: needs: resolve-msrv runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@master with: toolchain: ${{ needs.resolve-msrv.outputs.v }} - uses: Swatinem/rust-cache@v2 with: shared-key: freemkv-unlock-ci - run: cargo test --tests # `emulation` is off by default and is the face bdemu builds against. A # feature nothing compiles is a feature that breaks at release time. - run: cargo test --tests --features emulation # `--tests` skips doctests; compile/run them so doc examples can't rot. - run: cargo test --doc --all-features # This crate's breakage takes every dependent down, so build the two direct # consumers against THIS commit. libfreemkv resolves ../freemkv-unlock by # layout; bdemu (emulation feature) also needs libfreemkv patched to the sibling. consumers: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 with: path: freemkv-unlock - uses: freemkv/.github/.github/actions/checkout-siblings@v1 with: checkout: libfreemkv bdemu patch: libfreemkv self: bdemu # Dependents track current Rust, not this crate's MSRV. - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 with: workspaces: | libfreemkv bdemu - run: cargo +stable build --all-targets working-directory: libfreemkv - run: cargo +stable build --all-targets working-directory: bdemu # Coverage is additive and NON-REQUIRED: it uploads to Codecov but never # gates a merge (fail_ci_if_error is false, and it is not wired into any # release gate). It mirrors the `test` job's environment — same pinned # toolchain, ubuntu-latest, no `if:` so it runs on every branch and PR just # as `test` does. `--locked` is intentionally omitted to match this crate's # policy (Cargo.lock is committed but --locked can't satisfy the path-patch layout). coverage: needs: resolve-msrv runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@master with: toolchain: ${{ needs.resolve-msrv.outputs.v }} components: llvm-tools-preview - uses: Swatinem/rust-cache@v2 with: shared-key: freemkv-unlock-coverage - uses: taiki-e/install-action@cargo-llvm-cov - run: cargo llvm-cov --workspace --ignore-run-fail --lcov --output-path lcov.info - uses: codecov/codecov-action@v7 with: token: ${{ secrets.CODECOV_TOKEN }} files: lcov.info slug: freemkv/freemkv-unlock fail_ci_if_error: false # cargo-deny gates the RESOLVED dependency graph: RUSTSEC advisories, a # permissive licence allow-list, and source/ban hygiene. It mirrors the # `test` job's checkout (single root checkout, no siblings) and runs on # `stable` — advisory/licence debt is caught against current Rust, not the # MSRV pin — so no `needs: resolve-msrv`. deny: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable - uses: EmbarkStudios/cargo-deny-action@v2 with: command: check advisories bans licenses sources manifest-path: Cargo.toml # The crate ships on macOS and Windows through its consumers, so it has to # compile there. Build the tests (not just `cargo check`) — that forces # codegen and catches errors in test-only code, which `check` skips. check-macos: # dev is the FAST lane: this job still runs, but on the release-candidate # branches rather than on every push to dev. Nothing is deleted and no # platform stops being checked before a release -- qa.yml independently # covers macOS and Windows, and the jobs unique to this file (the Intel # macOS build, the Windows release build) run here on qa and main. A push # to dev is meant to be cheap and frequent; waiting on three runner pools # to agree is what a release candidate is for. # # `if` SKIPS the job (it does not queue). A queued job would be far worse # than a slow one: a never-scheduled job leaves the run pending forever, so # the branch's required checks never report complete. needs: resolve-msrv if: github.ref_name == 'qa' || github.ref_name == 'main' runs-on: macos-latest steps: - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@master with: toolchain: ${{ needs.resolve-msrv.outputs.v }} - uses: Swatinem/rust-cache@v2 with: shared-key: freemkv-unlock-ci - run: cargo build --tests --all-features check-windows: # dev is the FAST lane: this job still runs, but on the release-candidate # branches rather than on every push to dev. Nothing is deleted and no # platform stops being checked before a release -- qa.yml independently # covers macOS and Windows, and the jobs unique to this file (the Intel # macOS build, the Windows release build) run here on qa and main. A push # to dev is meant to be cheap and frequent; waiting on three runner pools # to agree is what a release candidate is for. # # `if` SKIPS the job (it does not queue). A queued job would be far worse # than a slow one: a never-scheduled job leaves the run pending forever, so # the branch's required checks never report complete. needs: resolve-msrv if: github.ref_name == 'qa' || github.ref_name == 'main' runs-on: windows-latest steps: - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@master with: toolchain: ${{ needs.resolve-msrv.outputs.v }} - uses: Swatinem/rust-cache@v2 with: shared-key: freemkv-unlock-ci - run: cargo build --tests --all-features