Add the qa gate: dev for speed, qa for proof

dev -> qa -> main, across every public repo.

dev is where work lands and is meant to be pushed to often, so ci.yml stays
the fast answer: fmt, clippy, unit tests, leak-guard. qa is the release
candidate, and qa.yml is the claim that a commit is production worth —
release-profile tests on all three platforms, and the Linux cross-target
clippy that the local precommit gate has always run but CI never did.

Release profile matters as its own gate: overflow checks are off, debug_assert
is compiled out, and inlining changes what the optimiser can prove. A test
that only passes in debug never guarded the binary anyone ships.

qa.yml checks siblings out at qa rather than dev. This is not a monorepo, and
a qa run resolving its dependencies from unreleased dev tips would go green on
a combination that is not the one shipping — the exact mismatch the branch
model exists to prevent. ci.yml now tracks whichever branch triggered it for
the same reason, since it also fires on qa.

The consequence is that repos move to qa together, in dependency order. qa is
backfilled from main so an unchanged crate still presents working code to the
crates built on it.
This commit is contained in:
Matthew Jackson
2026-08-05 20:59:34 -07:00
parent a018e1adc4
commit c64bc311ff
2 changed files with 92 additions and 15 deletions
+17 -15
View File
@@ -2,10 +2,12 @@ name: CI
on: on:
push: push:
# `dev` is where work lands and where CI must be green. `main` only ever # dev -> qa -> main. `dev` is where work lands and is meant to be pushed
# moves at release time, to the tagged commit, so a push to it is the # to often: these are the FAST checks, so a mistake surfaces in minutes.
# release validation run rather than day-to-day feedback. # `qa` is the release candidate — it runs these too, plus the expensive
branches: [main, dev] # 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: pull_request:
jobs: jobs:
@@ -27,7 +29,7 @@ jobs:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
with: with:
repository: freemkv/freemkv-unlock repository: freemkv/freemkv-unlock
ref: dev ref: "${{ github.ref_name == 'qa' && 'qa' || 'dev' }}"
path: freemkv-unlock path: freemkv-unlock
- uses: dtolnay/rust-toolchain@1.97.0 - uses: dtolnay/rust-toolchain@1.97.0
with: with:
@@ -65,7 +67,7 @@ jobs:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
with: with:
repository: freemkv/freemkv-unlock repository: freemkv/freemkv-unlock
ref: dev ref: "${{ github.ref_name == 'qa' && 'qa' || 'dev' }}"
path: freemkv-unlock path: freemkv-unlock
- uses: dtolnay/rust-toolchain@1.97.0 - uses: dtolnay/rust-toolchain@1.97.0
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
@@ -92,7 +94,7 @@ jobs:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
with: with:
repository: freemkv/freemkv-unlock repository: freemkv/freemkv-unlock
ref: dev ref: "${{ github.ref_name == 'qa' && 'qa' || 'dev' }}"
path: freemkv-unlock path: freemkv-unlock
- uses: dtolnay/rust-toolchain@1.97.0 - uses: dtolnay/rust-toolchain@1.97.0
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
@@ -119,7 +121,7 @@ jobs:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
with: with:
repository: freemkv/freemkv-unlock repository: freemkv/freemkv-unlock
ref: dev ref: "${{ github.ref_name == 'qa' && 'qa' || 'dev' }}"
path: freemkv-unlock path: freemkv-unlock
- uses: dtolnay/rust-toolchain@1.97.0 - uses: dtolnay/rust-toolchain@1.97.0
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
@@ -150,19 +152,19 @@ jobs:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
with: { path: libfreemkv } with: { path: libfreemkv }
- uses: actions/checkout@v5 - uses: actions/checkout@v5
with: { repository: freemkv/freemkv-unlock, ref: dev, path: freemkv-unlock } with: { repository: freemkv/freemkv-unlock, ref: "${{ github.ref_name == 'qa' && 'qa' || 'dev' }}", path: freemkv-unlock }
- uses: actions/checkout@v5 - uses: actions/checkout@v5
with: { repository: freemkv/freemkv-keysources, ref: dev, path: freemkv-keysources } with: { repository: freemkv/freemkv-keysources, ref: "${{ github.ref_name == 'qa' && 'qa' || 'dev' }}", path: freemkv-keysources }
- uses: actions/checkout@v5 - uses: actions/checkout@v5
with: { repository: freemkv/freemkv-engine, ref: dev, path: freemkv-engine } with: { repository: freemkv/freemkv-engine, ref: "${{ github.ref_name == 'qa' && 'qa' || 'dev' }}", path: freemkv-engine }
- uses: actions/checkout@v5 - uses: actions/checkout@v5
with: { repository: freemkv/freemkv-i18n, ref: dev, path: freemkv-i18n } with: { repository: freemkv/freemkv-i18n, ref: "${{ github.ref_name == 'qa' && 'qa' || 'dev' }}", path: freemkv-i18n }
- uses: actions/checkout@v5 - uses: actions/checkout@v5
with: { repository: freemkv/freemkv, ref: dev, path: freemkv } with: { repository: freemkv/freemkv, ref: "${{ github.ref_name == 'qa' && 'qa' || 'dev' }}", path: freemkv }
- uses: actions/checkout@v5 - uses: actions/checkout@v5
with: { repository: freemkv/autorip, ref: dev, path: autorip } with: { repository: freemkv/autorip, ref: "${{ github.ref_name == 'qa' && 'qa' || 'dev' }}", path: autorip }
- uses: actions/checkout@v5 - uses: actions/checkout@v5
with: { repository: freemkv/bdemu, ref: dev, path: bdemu } with: { repository: freemkv/bdemu, ref: "${{ github.ref_name == 'qa' && 'qa' || 'dev' }}", path: bdemu }
- name: Point every dependent at THIS libfreemkv commit - name: Point every dependent at THIS libfreemkv commit
shell: bash shell: bash
run: | run: |
+75
View File
@@ -0,0 +1,75 @@
name: qa
# ── The qa gate: "is this production worth?" ────────────────────────────────
#
# dev -> qa -> main.
#
# `dev` is for committing often. ci.yml answers "is it green" in minutes with
# fmt, clippy and the unit suite, so a mistake surfaces while it is still cheap
# to fix. `qa` is the release-candidate branch, and THIS workflow is the claim
# that a commit is production worth: everything expensive that can run without
# physical media. `main` only ever receives a qa that went green here.
#
# Sibling repos are checked out at `qa`, NOT `dev`. A qa run that resolved its
# dependencies from dev tips would be validating a combination that is not the
# one being released, which is the exact failure this branch exists to prevent.
#
# What this gate CANNOT cover: `disc://` and real `iso://` need physical media,
# and no hosted runner has an optical drive or the image hoard. Those run on a
# self-hosted runner (see the media job at the end) and are the one leg that
# stays on hardware.
on:
push:
branches: [qa]
workflow_dispatch:
jobs:
# The debug suite runs on every dev push. Release is a DIFFERENT build:
# overflow checks are off, debug_assert! is compiled out, and inlining
# changes what the optimiser can prove. A test that only passes in debug is
# a test that never guarded the binary anyone actually ships.
release-tests:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v5
with:
path: libfreemkv
- uses: actions/checkout@v5
with:
repository: freemkv/freemkv-unlock
ref: qa
path: freemkv-unlock
- uses: dtolnay/rust-toolchain@1.97.0
- uses: Swatinem/rust-cache@v2
with:
workspaces: libfreemkv
- run: cargo test --release --tests
working-directory: libfreemkv
# clippy's output is target-dependent: cfg-gated code only gets linted on
# the target it compiles for. Linting solely on the dev machine's host
# target is how a lint that CI rejects reaches a push.
cross-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
path: libfreemkv
- uses: actions/checkout@v5
with:
repository: freemkv/freemkv-unlock
ref: qa
path: freemkv-unlock
- uses: dtolnay/rust-toolchain@1.97.0
with:
components: clippy
- uses: Swatinem/rust-cache@v2
with:
workspaces: libfreemkv
- run: rustup target add x86_64-unknown-linux-gnu
- run: cargo clippy --all-targets --target x86_64-unknown-linux-gnu -- -D warnings
working-directory: libfreemkv