feat: add support for custom volume curves and GitHub build attestation #148
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
| name: ci | |
| # Builds, tests and lints everything that CAN be checked on a hosted runner. | |
| # | |
| # WHY THIS EXISTS SEPARATELY FROM release.yml. The release workflow only runs on a tag, so until | |
| # now the first time the installer was compiled for Windows was during the release itself — which | |
| # is the worst possible moment to discover it does not compile. This runs the same build on every | |
| # change, so a tag is a formality rather than a gamble. | |
| # | |
| # The ARM LINK is still not done here: it needs a glibc-2.23 + libc++-3.9.0 cross toolchain matched | |
| # to the player's runtime, which is real machinery to maintain on a runner for little gain. The | |
| # device binaries are built by a maintainer and committed under cinder-home/dist/, and | |
| # `tools/release.sh` verifies that committed payload byte-for-byte against a fresh build before it | |
| # will tag. | |
| # | |
| # But "we cannot LINK for ARM here" was quietly being read as "we cannot check the C++ at all", and | |
| # that left ~19,400 lines of C and C++ — the code that runs as root, owns the boot path and drives | |
| # the closed Sony services — with no automated gate of any kind (docs/SHORTCOMINGS.md §A1). The | |
| # `native` job below closes most of that with a stock toolchain and about a minute of runner time. | |
| # | |
| # ── TRIGGERS: ONE RUN PER CHANGE, NOT TWO ─────────────────────────────────────────────────────── | |
| # This used to be `push: ["**"]` PLUS `pull_request`, and for a branch with a PR open — which is | |
| # every branch that matters — BOTH fired, so every push ran the whole matrix TWICE. Eight jobs | |
| # where four were wanted, for the life of the repo. | |
| # | |
| # Now `pull_request` covers branches and `push` covers main. A branch with no PR gets no CI; that | |
| # is the deliberate trade, and `workflow_dispatch` is the escape hatch when you want a run anyway. | |
| on: | |
| push: | |
| branches: [main] | |
| tags-ignore: ["v*"] # tags are release.yml's job, no need to run twice | |
| pull_request: | |
| workflow_dispatch: | |
| schedule: | |
| # Weekly, so a newly published advisory against unchanged code is found by the calendar rather | |
| # than by the next unrelated commit. Monday 07:00 UTC. | |
| - cron: "0 7 * * 1" | |
| # A second push supersedes the first, so cancel the run it replaced rather than paying for both to | |
| # finish. Never on main: a green tick there is a record of what shipped, not a preview. | |
| concurrency: | |
| group: ci-${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} | |
| jobs: | |
| rust: | |
| name: ${{ matrix.what }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - what: player (host tests) | |
| os: ubuntu-latest | |
| dir: player | |
| - what: installer (linux) | |
| os: ubuntu-latest | |
| dir: installer | |
| - what: installer (windows) | |
| os: windows-latest | |
| dir: installer | |
| runs-on: ${{ matrix.os }} | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: dtolnay/rust-toolchain@stable | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| workspaces: ${{ matrix.dir }} | |
| - name: Test | |
| working-directory: ${{ matrix.dir }} | |
| run: cargo test --release | |
| - name: Build | |
| working-directory: ${{ matrix.dir }} | |
| run: cargo build --release | |
| # Clippy, scoped to the lint groups that describe BUGS rather than taste. | |
| # | |
| # Not `-D warnings`: the tree has 121 style/pedantic warnings today, so that gate would be | |
| # red on arrival and would only teach everyone to ignore it. `correctness` and `suspicious` | |
| # are the groups that catch things that are actually wrong, both pass clean right now, and a | |
| # new one appearing is a real regression worth stopping on. Widen as the backlog is worked | |
| # off. | |
| - name: Clippy (correctness + suspicious) | |
| working-directory: ${{ matrix.dir }} | |
| run: cargo clippy --all-targets -- -D clippy::correctness -D clippy::suspicious | |
| # ── the half of the tree nothing used to check ───────────────────────────────────────────────── | |
| # Linux only, no cross toolchain, no matrix — about a minute. Every step here is run locally and | |
| # passing before being wired. (The first version of this job was NOT: the shellcheck step went | |
| # red on its first run because the runner's shellcheck was older than the local one. Hence the | |
| # pinning below, and hence "run it locally first" being written down rather than assumed.) | |
| native: | |
| name: C/C++ syntax + self-tests + shell + harness + launcher | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| # ~19,400 lines of C and C++ that nothing compiled before this. It is a SYNTAX check, not a | |
| # build: it cannot see the ABI, the glibc ceiling, or anything needing the device's headers. | |
| # It answers "does this parse", which is the question nothing was asking — and on its first | |
| # run it found probe.cpp using uintptr_t in seven places with no <cstdint>, building only | |
| # because the device toolchain happened to pull it in transitively. | |
| - name: C/C++ syntax check | |
| run: tools/host_syntax_check.sh | |
| # The six pure-logic rules that decide whether the device pauses, reconnects, reboots or | |
| # notices new music. They live in build.sh, which needs the cross toolchain and which only | |
| # tools/release.sh ever calls — so until now they ran when somebody remembered. They compile | |
| # with plain cc in milliseconds and have no business being opt-in. | |
| - name: C++ self-tests | |
| run: | | |
| fail=0 | |
| for t in guard volramp btedge jackedge btswitch dbsig btpoll framebudget eqrange; do | |
| src="cinder-home/tools/${t}_selftest.cpp" | |
| if [ ! -f "$src" ]; then echo "::error::missing self-test $src"; fail=1; continue; fi | |
| if ! cc -O2 -o "/tmp/$t" "$src" -lstdc++; then | |
| echo "::error::$t failed to build"; fail=1; continue | |
| fi | |
| if ! "/tmp/$t"; then echo "::error::$t FAILED"; fail=1; fi | |
| done | |
| exit $fail | |
| # 5,288 lines of shell across 33 scripts — the launcher, the crash supervisor, the bad-boot | |
| # counter, the USB-MSC ordering. All root-privileged, all on the boot path, and none of it | |
| # had so much as a syntax check. | |
| # | |
| # PINNED, AND VIA A SCRIPT, both for the same reason. The first version of this job inlined | |
| # the commands and used whatever shellcheck the runner image shipped — and it went red on its | |
| # first run, because the local shellcheck used to verify it was 0.11.0 and the runner's was | |
| # older, and the two disagree about `#!/system/xbin/busybox sh`. A lint gate whose version | |
| # floats is a gate that can turn red with no source change. tools/shell_check.sh is what a | |
| # contributor runs locally, so local and CI cannot drift into checking different things. | |
| - name: Install pinned shellcheck | |
| run: pipx install shellcheck-py==0.11.0.1 || pip install --user shellcheck-py==0.11.0.1 | |
| - name: Shell syntax + lint | |
| run: tools/shell_check.sh | |
| # The release payload must be exempt from Git's EOL rewriting — and this has to be checked | |
| # HERE, on every push, not at release time. | |
| # | |
| # v0.1.7 is why. release.yml verifies the payload against a sha256 manifest, and that gate ran | |
| # for the FIRST TIME on the v0.1.7 tag (v0.1.6 was prepared but never tagged). It failed | |
| # immediately: `windows-latest` checks out with core.autocrlf=true, which rewrote the two text | |
| # members of the payload, so their hashes no longer matched. Nothing was stale — the bytes were | |
| # changed in transit — but the release was already cut before anything noticed. | |
| # | |
| # That is the same shape as the shellcheck lesson recorded in tools/shell_check.sh: a check | |
| # that only ever runs during a release is a check that fails during a release. This one is | |
| # cheap, deterministic, and runs on every push. | |
| - name: Payload is safe from line-ending rewrites | |
| run: tools/check_payload_attrs.sh | |
| # The release body is rendered from .github/release-notes.md at tag time. If the template | |
| # loses its {{SHA256SUMS}} marker, the renderer fails — and it would fail for the first time | |
| # DURING a release, which is the failure mode this repo keeps hitting. Render it here, on | |
| # every push, against a stand-in checksum file. | |
| - name: Release notes still render | |
| run: | | |
| printf '%s cinder-installer-linux-x64\n' \ | |
| "$(printf 0%.0s $(seq 64) | tr 0 a)" > /tmp/fake-sums | |
| tools/render_release_notes.sh /tmp/fake-sums /tmp/body.md | |
| # The app itself, BOOTED. Everything above checks pieces in isolation; this links the real | |
| # main.cpp against fake Sony services and a virtual clock, runs the easel lifecycle, and | |
| # asserts on the call trace — that the paired list and the notification listener are set up | |
| # during bring-up, that a service which is not there yet degrades a feature instead of the | |
| # Home app, that the idle Bluetooth poll still backs off. Those are the defects this project | |
| # actually ships, and nothing else in CI can see them. | |
| # | |
| # Costs about two seconds, build included: the harness replaces sleeping with advancing a | |
| # counter, so two virtual minutes of device time is a few milliseconds of CPU. | |
| # See cinder-home/harness/README.md for what it does and does not prove. | |
| - name: Off-device harness (boots main.cpp against fake services) | |
| run: cinder-home/harness/run.sh | |
| # The escape ladder: bad-boot counter, auto-revert, crash supervisor, kill switch, the cable | |
| # escape. 45 cases over the launcher generated by install_cinderhome.sh — every mechanism | |
| # standing between a bad build and a device that needs wbrt to recover. It has existed since | |
| # July and nothing automatic ran it (SHORTCOMINGS.md §A3), which for the one script whose | |
| # failure mode is "brick" is the wrong way round. About fifteen seconds — the cases that | |
| # exercise the crash supervisor have to actually wait for its backoff. | |
| # | |
| # Runs here as a NON-root user, which matters: one case makes /data/cinder unwritable with | |
| # chmod, and chmod does not bind uid 0 — on a root shell that case silently reports a failure | |
| # about the tester rather than the launcher. It skips itself there and a root-proof variant | |
| # covers the same rule. | |
| - name: Launcher recovery matrix | |
| run: bash cinder-home/tools/test_launcher.sh | |
| # ── the dependency tree, which nothing looked at ─────────────────────────────────────────────── | |
| # ~130 crates reach the binary that runs as the device's Home app, and until now no gate checked | |
| # any of them against the RustSec advisory database. The installer has ZERO dependencies by | |
| # design — it is audited here anyway so that stays true rather than merely being true today. | |
| # | |
| # `cargo audit` exits non-zero on a VULNERABILITY and zero on an unmaintained-crate warning, which | |
| # is the right split for this repo: a vulnerability in the tree is a stop, whereas `instant` and | |
| # `ttf-parser` (both transitive via fontdue) being unmaintained is a fact to know, not a reason to | |
| # go red. Both were clean of vulnerabilities when this job was added (2026-09-01). | |
| # | |
| # Scheduled as well as on-push: an advisory is published against code that has not changed, so a | |
| # gate that only runs on a commit finds out about it on the next unrelated commit. | |
| audit: | |
| name: dependency advisories | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: dtolnay/rust-toolchain@stable | |
| # A plain `cargo audit` rather than a marketplace action, for the same reason the shellcheck | |
| # step is a script: this is the exact command a contributor runs locally, so local and CI | |
| # cannot drift into checking different things. Pinned, and cached — the install is the slow | |
| # part, not the scan. | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| workspaces: player | |
| - name: Install cargo-audit | |
| run: cargo install cargo-audit --locked --version ^0.22 | |
| - name: player — advisory scan | |
| working-directory: player | |
| run: cargo audit | |
| - name: installer — advisory scan | |
| working-directory: installer | |
| run: cargo audit | |
| payload: | |
| name: dist payload is complete | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Every file the installer embeds must be committed | |
| shell: bash | |
| run: | | |
| # The installer's build.rs fails on a missing payload, but it fails with a build error | |
| # deep in a cross-platform matrix. Checking it here names the file directly. | |
| missing=0 | |
| while read -r f; do | |
| [ -n "$f" ] || continue | |
| if [ -f "$f" ]; then | |
| printf ' ok %8s %s\n' "$(stat -c%s "$f")" "$f" | |
| else | |
| printf ' MISSING %s\n' "$f"; missing=1 | |
| fi | |
| done <<'PAYLOAD' | |
| cinder-home/dist/stable/cinder-home | |
| cinder-home/dist/stable/cinder-probe | |
| cinder-home/dist/stable/cinder-umount | |
| cinder-home/dist/stable/cinder-power | |
| cinder-home/dist/stable/cinder-msc | |
| cinder-home/dist/stable/cinder-clock | |
| cinder-home/dist/stable/cinder-fm | |
| cinder-home/dist/stable/cinder-voltable | |
| cinder-home/dist/stable/cinder-battery | |
| cinder-home/dist/stable/cinder-signature.sh | |
| cinder-home/dist/stable/cinder_components.conf | |
| cinder-home/dist/stable/cinder_home_install.upg | |
| cinder-home/dist/stable/cinder_home_uninstall.upg | |
| PAYLOAD | |
| [ "$missing" = 0 ] || { echo "::error::dist/stable is incomplete — the installer cannot be built"; exit 1; } | |
| - name: The ARM binaries must actually be ARM | |
| shell: bash | |
| run: | | |
| # Guards against someone committing a host build by accident, which would produce an | |
| # installer that ships x86 binaries to an armv7 player and fails only on the device. | |
| for f in cinder-home/dist/stable/cinder-home cinder-home/dist/stable/cinder-probe; do | |
| t=$(file -b "$f") | |
| echo " $f: $t" | |
| case "$t" in | |
| *ARM*) ;; | |
| *) echo "::error::$f is not an ARM binary"; exit 1 ;; | |
| esac | |
| done |