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 # WHY THE SIBLING CHECKOUTS — this is NOT a monorepo. # # The committed Cargo.toml resolves libfreemkv from a git TAG (it is off # crates.io). That means CI was building this branch against the LAST RELEASE # of libfreemkv, never against libfreemkv's dev tip — so a coordinated change # across the two repos could not be verified until release-orchestrate.yml cut the tags. # Green here meant "compiles against last month's library". # # The fix mirrors the developer's local layout via the shared composite # freemkv/.github//actions/checkout-siblings: every sibling repo is checked out # into a subdirectory of $GITHUB_WORKSPACE, reproducing # ~/Developer/freemkv/{freemkv-keysources,libfreemkv,freemkv-unlock}, and a # generated .cargo/config.toml redirects the git-tag patch to those paths. # Config-level [patch.crates-io] wins over the manifest one, which is the same # mechanism local development already uses (see the note in Cargo.toml). # # Subdirectories rather than `../libfreemkv` because actions/checkout refuses # any `path:` outside $GITHUB_WORKSPACE, and the workspace IS the repo dir. # Repos stay separate, tags stay separate, the release cascade is untouched. # # libfreemkv path-deps ../freemkv-unlock, which resolves to the workspace root # under this layout — hence the third checkout. # # `--locked` is deliberately absent: Cargo.lock is now committed (issue #15) and # the `deny` job audits the resolved graph, but CI's sibling path-patch layout # resolves the freemkv crates as path packages, which `--locked` can't satisfy — # the same reason the binary crates omit it. The lock that has to hold is the one # release-orchestrate.yml regenerates in each consuming binary. jobs: # Single source of truth for the MSRV — read rust-version from Cargo.toml once # and export it; MSRV-bound jobs consume needs.resolve-msrv.outputs.v. clippy # runs on stable instead (see CI.md's Toolchain policy). 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 with: path: freemkv-keysources - uses: freemkv/.github/.github/actions/checkout-siblings@v1 with: checkout: libfreemkv freemkv-unlock patch: libfreemkv freemkv-unlock self: freemkv-keysources # fmt runs on the MSRV pin; clippy runs on stable so new-lint debt is a # blocking gate the pinned toolchain would hide. Stable is installed first # for clippy; the MSRV pin goes second so it stays fmt's default toolchain. - 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: workspaces: freemkv-keysources - run: cargo fmt --check working-directory: freemkv-keysources # --all-targets so the test code is linted too, not just the library. - run: cargo +stable clippy --all-targets -- -D warnings working-directory: freemkv-keysources test: needs: resolve-msrv runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 with: path: freemkv-keysources - uses: freemkv/.github/.github/actions/checkout-siblings@v1 with: checkout: libfreemkv freemkv-unlock patch: libfreemkv freemkv-unlock self: freemkv-keysources - uses: dtolnay/rust-toolchain@master with: toolchain: ${{ needs.resolve-msrv.outputs.v }} - uses: Swatinem/rust-cache@v2 with: workspaces: freemkv-keysources - run: cargo test --tests working-directory: freemkv-keysources # Dependency-audit gate. Mirrors the `test` job's sibling-checkout layout and # generated .cargo/config.toml so `cargo metadata` resolves the git-tag patch # to the local sibling paths; cargo-deny then audits advisories, bans, # licenses, and sources against the committed deny.toml. deny: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 with: path: freemkv-keysources - uses: freemkv/.github/.github/actions/checkout-siblings@v1 with: checkout: libfreemkv freemkv-unlock patch: libfreemkv freemkv-unlock self: freemkv-keysources - uses: dtolnay/rust-toolchain@stable - uses: taiki-e/install-action@cargo-deny # Run from the repo dir so the generated .cargo/config.toml path patches apply. - run: cargo deny --all-features check advisories bans licenses sources working-directory: freemkv-keysources # This crate ships wherever freemkv and autorip ship, and it drags in all of # libfreemkv's platform code (SCSI transports included). Build the tests, not # `cargo check` — check skips codegen and never compiles test-only code. 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 with: path: freemkv-keysources - uses: freemkv/.github/.github/actions/checkout-siblings@v1 with: checkout: libfreemkv freemkv-unlock patch: libfreemkv freemkv-unlock self: freemkv-keysources - uses: dtolnay/rust-toolchain@master with: toolchain: ${{ needs.resolve-msrv.outputs.v }} - uses: Swatinem/rust-cache@v2 with: workspaces: freemkv-keysources - run: cargo build --tests working-directory: freemkv-keysources # Additive, NON-REQUIRED coverage. Mirrors the `test` job's sibling-checkout # layout and pinned toolchain (nothing in the release cascade depends on it, # and codecov upload is tokenless with fail_ci_if_error:false, so a flaky # upload never reddens the run). `--locked` is deliberately absent here for # the same reason it is in `test`: CI's sibling path-patch layout resolves the # freemkv crates as path packages, which `--locked` can't satisfy. coverage: needs: resolve-msrv runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 with: path: freemkv-keysources - uses: freemkv/.github/.github/actions/checkout-siblings@v1 with: checkout: libfreemkv freemkv-unlock patch: libfreemkv freemkv-unlock self: freemkv-keysources - uses: dtolnay/rust-toolchain@master with: toolchain: ${{ needs.resolve-msrv.outputs.v }} components: llvm-tools-preview - uses: Swatinem/rust-cache@v2 with: workspaces: freemkv-keysources - uses: taiki-e/install-action@cargo-llvm-cov - run: cargo llvm-cov --workspace --ignore-run-fail --lcov --output-path lcov.info working-directory: freemkv-keysources - uses: codecov/codecov-action@v7 with: files: freemkv-keysources/lcov.info slug: freemkv/freemkv-keysources fail_ci_if_error: false token: ${{ secrets.CODECOV_TOKEN }} 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 with: path: freemkv-keysources - uses: freemkv/.github/.github/actions/checkout-siblings@v1 with: checkout: libfreemkv freemkv-unlock patch: libfreemkv freemkv-unlock self: freemkv-keysources - uses: dtolnay/rust-toolchain@master with: toolchain: ${{ needs.resolve-msrv.outputs.v }} - uses: Swatinem/rust-cache@v2 with: workspaces: freemkv-keysources - run: cargo build --tests working-directory: freemkv-keysources