Skip to content

Add PNG encoding and screenshot rendering script #158

Add PNG encoding and screenshot rendering script

Add PNG encoding and screenshot rendering script #158

Workflow file for this run

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
# Every preview, rendered from this commit, downloadable from the run. This replaces the
# PNGs that used to be committed: golden.txt already fails the job on a pixel change, so the
# images are for LOOKING at a change, and a run artifact is always current where a committed
# folder was two months stale.
- name: Render UI previews
if: matrix.dir == 'player'
working-directory: player
run: cargo run --release -p cinder-host >/dev/null
- uses: actions/upload-artifact@v7
if: matrix.dir == 'player'
with:
name: ui-previews
path: player/out/
retention-days: 14
if-no-files-found: error
# 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; }
# Architecture, glibc floor, allowed libraries, static helpers. A script, so it runs locally
# exactly as here. It is NOT a link check: the link needs Sony's libraries, so that gate is
# tools/release.sh (docs/SHORTCOMINGS.md A1).
- name: The ARM binaries fit the player's runtime
shell: bash
# Through bash, not by path: this clone has core.filemode=false, so a new script can land in
# git without its executable bit — which is exactly how this step first failed (rc 126).
run: bash tools/check_arm_payload.sh