Release v0.4.11 #196
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # SPDX-License-Identifier: AGPL-3.0-only | |
| # Copyright (C) 2026 RS-Key contributors | |
| # Seven jobs: | |
| # changes — what this change can possibly affect (`scripts/ci-scope.sh`), so a | |
| # documentation edit does not build 24 firmware images. Gating is by | |
| # `if:` on the jobs rather than `paths:` on the trigger: a skipped | |
| # JOB still reports, a skipped WORKFLOW reports nothing, and the | |
| # second kind is the one that quietly hides a missing check. | |
| # docs — the mdBook build + link check, gitleaks, and the workflow lint. | |
| # Always. It is the only job a documentation-only change runs, so the | |
| # secret scan lives here too — `check.sh` runs it as well, and one | |
| # that can be skipped is not a secret scan. Same argument for | |
| # actionlint, and it has a case: `pages.yml` is inside ci-scope.sh's | |
| # DOCS_ONLY set, so a change to that workflow alone skips `check` | |
| # entirely and would never be parsed by anything. | |
| # check — the merge gate, the same single source of truth as local work: | |
| # `nix develop -c ./scripts/check.sh` (docs/testing.md, "CI parity"). | |
| # Green locally means green here; no CI-only logic to drift. | |
| # proofs — the Kani harnesses this change can have broken: the fast tier of | |
| # `scripts/kani.sh` on any crate edit, plus the security-state tier | |
| # (rsk-fido and rsk-fs — twelve minutes, one harness peaking at | |
| # 9.3 GiB) when the diff reaches the state those sequence proofs are | |
| # about. Everything slower — the phy round-trip, the | |
| # functional division specs — stays in the daily `deep-checks` row, | |
| # which runs the whole roster. Not part of `check.sh`: Kani is the one | |
| # tool deliberately outside the nix shell (docs/testing.md). | |
| # flavors — every feature combination of the firmware (no-touch × | |
| # advertise-pqc × fips-profile), the strong-pin / always-uv / | |
| # strict-up behavior flavors (each with a pqc pair), and the | |
| # trusted-display build, each packaged as a downloadable | |
| # firmware-<flavor>.uf2 artifact. | |
| # knob-builds — build smokes for the compile-time env knobs (docs/build.md), | |
| # five parallel rows over `scripts/ci-knobs.sh`: all VIDPID presets, | |
| # FW_VERSION, XOSC_DELAY_MULT, FAKE_* test keys, and a check that the | |
| # default build bakes this project's own RS-Key identity while the | |
| # opt-in Yubico flavor bakes Yubico's (the vendor-mimicking ids are | |
| # local interop only, never shipped). | |
| # knobs — the aggregate of those rows, and the name `main`'s ruleset requires. | |
| # A required check has to be reported by a fixed name; a matrix | |
| # reports one per row. | |
| name: ci | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: ci-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| changes: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| outputs: | |
| firmware: ${{ steps.scope.outputs.firmware }} | |
| docs_only: ${{ steps.scope.outputs.docs_only }} | |
| proofs: ${{ steps.scope.outputs.proofs }} | |
| proofs_state: ${{ steps.scope.outputs.proofs_state }} | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| # The refs go through `env`, never through `${{ }}` inside the script: a | |
| # branch name is attacker-supplied on a fork PR, and the difference is a | |
| # shell injection. | |
| - name: what changed | |
| id: scope | |
| env: | |
| EVENT: ${{ github.event_name }} | |
| BASE: ${{ github.base_ref }} | |
| BEFORE: ${{ github.event.before }} | |
| run: | | |
| files="" | |
| if [ "$EVENT" = "pull_request" ]; then | |
| files="$(git diff --name-only "origin/$BASE...HEAD")" | |
| elif [ -n "$BEFORE" ] && git cat-file -e "$BEFORE^{commit}" 2>/dev/null; then | |
| files="$(git diff --name-only "$BEFORE" HEAD)" | |
| fi | |
| # An empty list means we could not work out a base — ci-scope.sh reads | |
| # that as "everything", which is the direction a mistake should fail in. | |
| printf '%s\n' "$files" | |
| printf '%s\n' "$files" | ./scripts/ci-scope.sh | tee -a "$GITHUB_OUTPUT" | |
| docs: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - uses: DeterminateSystems/nix-installer-action@ef8a148080ab6020fd15196c2084a2eea5ff2d25 # v22 | |
| - uses: nix-community/cache-nix-action@7df957e333c1e5da7721f60227dbba6d06080569 # v7.0.2 | |
| with: | |
| primary-key: nix-${{ runner.os }}-${{ hashFiles('flake.lock') }} | |
| restore-prefixes-first-match: nix-${{ runner.os }}- | |
| - name: docs build + link check | |
| run: nix develop -c ./scripts/docs.sh check | |
| - name: gitleaks | |
| run: nix develop -c gitleaks detect --no-banner --redact | |
| # The same command check.sh's first row runs, for the one change shape that | |
| # skips check.sh: a `pages.yml`-only edit reads as docs_only. | |
| - name: workflow lint | |
| run: nix develop -c actionlint -no-color -oneline | |
| check: | |
| needs: changes | |
| if: needs.changes.outputs.docs_only != 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 90 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - uses: DeterminateSystems/nix-installer-action@ef8a148080ab6020fd15196c2084a2eea5ff2d25 # v22 | |
| - uses: nix-community/cache-nix-action@7df957e333c1e5da7721f60227dbba6d06080569 # v7.0.2 | |
| with: | |
| primary-key: nix-${{ runner.os }}-${{ hashFiles('flake.lock') }} | |
| restore-prefixes-first-match: nix-${{ runner.os }}- | |
| - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 | |
| with: | |
| path: | | |
| ~/.cargo/registry | |
| ~/.cargo/git | |
| target | |
| key: cargo-check-${{ runner.os }}-${{ hashFiles('Cargo.lock', 'flake.lock') }} | |
| restore-keys: cargo-check-${{ runner.os }}- | |
| - name: check.sh — fmt, clippy, host tests, fips flavor, firmware builds, audit, deny, gitleaks | |
| run: nix develop -c ./scripts/check.sh | |
| env: | |
| HOST_TARGET: x86_64-unknown-linux-gnu | |
| proofs: | |
| needs: changes | |
| if: needs.changes.outputs.proofs == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 60 | |
| env: | |
| KANI_VERSION: "0.67.0" | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| # Same key the daily row writes, so a pull request restores the CBMC bundle | |
| # that job already downloaded rather than fetching its own. | |
| - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 | |
| with: | |
| path: | | |
| ~/.kani | |
| ~/.cargo/bin/cargo-kani | |
| ~/.cargo/bin/kani | |
| ~/.cargo/registry | |
| key: kani-${{ runner.os }}-${{ env.KANI_VERSION }} | |
| # Kani builds under its own pinned nightly into its own target dir, so this | |
| # shares nothing with the `check` job's cache and needs a key of its own. | |
| - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 | |
| with: | |
| path: target | |
| key: cargo-kani-${{ runner.os }}-${{ hashFiles('Cargo.lock') }} | |
| restore-keys: cargo-kani-${{ runner.os }}- | |
| - name: install kani (rustup-based — not packaged in nixpkgs) | |
| run: | | |
| command -v cargo-kani >/dev/null || cargo install --locked kani-verifier --version "$KANI_VERSION" | |
| cargo kani setup | |
| - name: prove the fast tier | |
| run: ./scripts/kani.sh pr | |
| # The expensive half, run only when the change reaches the state it is about | |
| # (`scripts/ci-scope.sh`, PROOFS_STATE). A step, not a second job: the Kani | |
| # install and the build cache above are most of the cost, and paying them | |
| # twice to skip twelve minutes of solving is the wrong trade. | |
| - name: prove the security-state crates | |
| if: needs.changes.outputs.proofs_state == 'true' | |
| run: ./scripts/kani.sh state | |
| flavors: | |
| needs: changes | |
| if: needs.changes.outputs.firmware == 'true' | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - name: default | |
| args: "" | |
| - name: pqc | |
| args: "--features advertise-pqc" | |
| - name: fips | |
| args: "--features fips-profile" | |
| - name: fips-pqc | |
| args: "--features fips-profile,advertise-pqc" | |
| - name: strong-pin | |
| args: "--features strong-pin" | |
| - name: strong-pin-pqc | |
| args: "--features strong-pin,advertise-pqc" | |
| - name: always-uv | |
| args: "--features always-uv" | |
| - name: always-uv-pqc | |
| args: "--features always-uv,advertise-pqc" | |
| - name: strict-up | |
| args: "--features strict-up" | |
| - name: strict-up-pqc | |
| args: "--features strict-up,advertise-pqc" | |
| - name: no-touch | |
| args: "--features no-touch" | |
| - name: no-touch-pqc | |
| args: "--features no-touch,advertise-pqc" | |
| - name: no-touch-fips | |
| args: "--features no-touch,fips-profile" | |
| - name: no-touch-fips-pqc | |
| args: "--features no-touch,fips-profile,advertise-pqc" | |
| # The trusted-display flavor: the panel takes the addressable-LED pin | |
| # (LED_KIND=none, compile-time-guarded) and the UI assets want the | |
| # larger flash (docs/guides/display.md). `env` is empty for every other | |
| # row, so only this one carries the extra knobs. | |
| - name: display | |
| args: "--features display" | |
| env: "LED_KIND=none FLASH_SIZE=16M" | |
| # Flash-geometry siblings of the default image (default features, RS-Key | |
| # identity). 2mb shrinks KVMAIN so the image still fits ≥1 MiB of code; | |
| # smoke-building it here catches a link/fit regression in PR CI rather | |
| # than at tag time. | |
| - name: 2mb | |
| args: "" | |
| env: "FLASH_SIZE=2M KVMAIN=896K" | |
| - name: 16mb | |
| args: "" | |
| env: "FLASH_SIZE=16M" | |
| # One row per shipped board file. Nothing else in the tree builds a | |
| # `BOARD=`, so without these a wrong pin or flash size in a TOML — the | |
| # values a user gets by following the CHANGELOG — reaches them uncaught. | |
| - name: board-waveshare-one | |
| args: "" | |
| env: "BOARD=waveshare-one" | |
| - name: board-tenstar-usb | |
| args: "" | |
| env: "BOARD=tenstar-usb" | |
| - name: board-seeed-xiao | |
| args: "" | |
| env: "BOARD=seeed-xiao" | |
| - name: board-waveshare-touch-lcd | |
| args: "--features display" | |
| env: "BOARD=waveshare-touch-lcd" | |
| - name: board-abrobot-4m | |
| args: "" | |
| env: "BOARD=abrobot-4m" | |
| - name: board-abrobot-16m | |
| args: "" | |
| env: "BOARD=abrobot-16m" | |
| # The strict admin-write posture (historical default). The permissive | |
| # default is what every other row builds; smoke-build this so the | |
| # strict-gated code keeps compiling. | |
| - name: strict-config | |
| args: "--features strict-config" | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 60 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - uses: DeterminateSystems/nix-installer-action@ef8a148080ab6020fd15196c2084a2eea5ff2d25 # v22 | |
| - uses: nix-community/cache-nix-action@7df957e333c1e5da7721f60227dbba6d06080569 # v7.0.2 | |
| with: | |
| primary-key: nix-${{ runner.os }}-${{ hashFiles('flake.lock') }} | |
| restore-prefixes-first-match: nix-${{ runner.os }}- | |
| - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 | |
| with: | |
| path: | | |
| ~/.cargo/registry | |
| ~/.cargo/git | |
| target | |
| # One key per flavour. Sharing it meant 24 jobs racing to write the | |
| # same entry while each restored a `target/` another feature set had | |
| # built — so a matrix whose whole point is that the rows differ was | |
| # rebuilding from cold, and thrashing the cache doing it. | |
| key: cargo-flavor-${{ matrix.name }}-${{ runner.os }}-${{ hashFiles('Cargo.lock', 'flake.lock') }} | |
| restore-keys: cargo-flavor-${{ matrix.name }}-${{ runner.os }}- | |
| - name: build (${{ matrix.name }}) | |
| run: nix develop -c env ${{ matrix.env }} cargo build --release -p firmware ${{ matrix.args }} | |
| - name: package firmware-${{ matrix.name }}.uf2 | |
| run: | | |
| nix develop -c ./scripts/pt.sh \ | |
| target/thumbv8m.main-none-eabihf/release/firmware \ | |
| firmware-${{ matrix.name }}.elf | |
| nix develop -c picotool uf2 convert \ | |
| firmware-${{ matrix.name }}.elf -t elf \ | |
| firmware-${{ matrix.name }}.uf2 | |
| sha256sum firmware-${{ matrix.name }}.uf2 | |
| - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: firmware-${{ matrix.name }}-${{ github.sha }} | |
| path: firmware-${{ matrix.name }}.uf2 | |
| knob-builds: | |
| needs: changes | |
| if: needs.changes.outputs.firmware == 'true' | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| # `scripts/ci-knobs.sh --groups` is the list; keep the two in step, since | |
| # a group nobody names is a smoke nobody runs. Grouped rather than one row | |
| # per preset: a public repo gets 20 concurrent runners and `flavors` | |
| # already wants 24, so past that more rows buy queue time, not wall-clock. | |
| group: [vidpid-1, vidpid-2, vidpid-3, identity, misc] | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 40 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - uses: DeterminateSystems/nix-installer-action@ef8a148080ab6020fd15196c2084a2eea5ff2d25 # v22 | |
| - uses: nix-community/cache-nix-action@7df957e333c1e5da7721f60227dbba6d06080569 # v7.0.2 | |
| with: | |
| primary-key: nix-${{ runner.os }}-${{ hashFiles('flake.lock') }} | |
| restore-prefixes-first-match: nix-${{ runner.os }}- | |
| - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 | |
| with: | |
| path: | | |
| ~/.cargo/registry | |
| ~/.cargo/git | |
| target | |
| key: cargo-knobs-${{ matrix.group }}-${{ runner.os }}-${{ hashFiles('Cargo.lock', 'flake.lock') }} | |
| restore-keys: cargo-knobs-${{ matrix.group }}-${{ runner.os }}- | |
| - name: knobs (${{ matrix.group }}) | |
| run: nix develop -c ./scripts/ci-knobs.sh ${{ matrix.group }} | |
| # The name `knobs` is a required status check on `main`, so it has to be | |
| # reported by *something* whatever the matrix above looks like. Splitting the | |
| # job renamed its checks to `knobs (<group>)` and left the required context | |
| # waiting for a report that would never come — a merge blocked on a check that | |
| # no longer exists. An aggregate keeps the contract stable: the matrix can grow, | |
| # shrink or be renamed without touching the repository's rules. | |
| # | |
| # `always()` so it still reports when the matrix was skipped — a skipped job is | |
| # a satisfied required check, an absent one is not. | |
| knobs: | |
| needs: [changes, knob-builds] | |
| if: always() | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - name: the knob builds all passed (or had nothing to build) | |
| env: | |
| RESULT: ${{ needs.knob-builds.result }} | |
| run: | | |
| case "$RESULT" in | |
| success | skipped) echo "knob builds: $RESULT" ;; | |
| *) echo "knob builds: $RESULT"; exit 1 ;; | |
| esac |