* Refactor CI workflows to use reusable composite actions
Extracts duplicated step sequences from cmake-linux.yml, cmake-macos.yml,
and cmake-windows.yml into shared composite actions under .github/actions/,
and consolidates the near-identical Windows "test"/"test-sanitizer" jobs
into a single parameterized reusable workflow (windows-audio-test.yml).
New composite actions:
- install-llvm-apt-pool: apt.llvm.org pool install (was duplicated 3x)
- setup-rtkit-linux: rtkit/polkit setup for Ubuntu (was duplicated 3x)
- setup-pulseaudio-legacy: PulseAudio config for Ubuntu 22.04 (was 3x)
- setup-rade-python-env: RADE venv + torch/matplotlib install (was 5x)
- run-linux-gui-test: Xvfb/dbus/metacity display setup (was 4x)
- setup-macos-virtual-audio: VB-Cable + permission workarounds (was 3x)
- download-mingw-llvm: pinned llvm-mingw toolchain download (was 3x)
- setup-windows-audio-drivers / configure-windows-audio-devices: VAC/
VB-Cable install and Windows audio device configuration (was 3x)
- configure-windows-crash-dumps: WER crash dump registry setup (was 2x)
This reduces cmake-linux.yml, cmake-macos.yml, and cmake-windows.yml by
about 820 lines combined while preserving existing job behavior. A few
inert simplifications were made along the way (e.g. reordering some
macOS setup steps, unifying near-identical if/else branches whose bodies
differed only in already-duplicated display-setup code) and are called
out in the accompanying summary.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Route Hamlib's autotools build through the configured ccache launcher
Hamlib is built via ExternalProject_Add's own ./configure && make
invocation, which CMake just shells out to. That means it never sees
CMAKE_C_COMPILER_LAUNCHER/CMAKE_CXX_COMPILER_LAUNCHER (e.g. ccache) --
those only apply to CMake's own compile rules -- so Hamlib's compile
was previously uncached everywhere except one macOS job that happened
to have ccache-action's create-symlink PATH workaround enabled.
Bake the launcher into CC/CXX for all three configure branches (mingw
cross-compile, Apple, and Linux) instead, so Hamlib gets cached the
same way as the rest of the project on every platform, without needing
create-symlink enabled on every runner that builds it from source.
Verified empirically with a standalone CMake script that the resulting
CONFIGURE_COMMAND list element is exactly "CC=ccache <compiler>" (one
list element, not split) when a launcher is set, and plain "CC=<compiler>"
with no leading space when it isn't.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Extend ccache launcher forwarding to the other ExternalProject deps
Reviewed every ExternalProject_Add-based dependency for the same gap
found in Hamlib: FetchContent-based deps (freedv_backend, wxWidgets,
portaudio) are compiled via add_subdirectory() as part of the same
CMake generation, so they already inherit CMAKE_C_COMPILER_LAUNCHER/
CMAKE_CXX_COMPILER_LAUNCHER automatically -- no change needed there.
The remaining ExternalProject_Add deps did need it:
- Extract the CC/CXX-with-launcher logic out of BuildHamlib.cmake into
a shared cmake/ExternalProjectCCache.cmake, defining
EXTERNAL_PROJECT_CC/EXTERNAL_PROJECT_CXX for any autotools-based
ExternalProject to reuse instead of duplicating the same if/else.
- Apply it to BuildLame.cmake and BuildMpg123.cmake, which build via
./configure the same way Hamlib does and had the identical gap:
neither previously set CC/CXX at all, so both relied entirely on
autoconf's own PATH-based (or --host-triggered) compiler detection.
- BuildCodec2.cmake and BuildSndfile.cmake build CMake-based deps as a
*nested* ExternalProject cmake invocation (CMAKE_ARGS), which gets
its own fresh CMakeCache.txt and doesn't inherit the parent's
compiler-launcher settings either -- forward
CMAKE_C_COMPILER_LAUNCHER/CMAKE_CXX_COMPILER_LAUNCHER there instead,
since that's the correct mechanism for a nested CMake build (as
opposed to baking it into CC/CXX, which is only needed for autotools).
Verified against real generated build.make/CMakeCache output (not just
the CMake list/string semantics) for both the launcher-set and
launcher-unset cases, and for the mingw-cross-compile, Apple, and
native-Linux configure branches.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017BRyLXHuZHy1b9oLt4wJFi
* Fix CI: composite action steps don't support timeout-minutes
CI run on PR #1498 confirmed a real error: GitHub Actions rejected
setup-macos-virtual-audio/action.yml with "Unexpected value
'timeout-minutes'" -- that key is only valid on a normal job step, not
on a step inside a composite action's own `runs.steps` list. This broke
every macOS job that calls the action (test, dist-pgo-instrument, and
transitively test-app-folder/dist-pgo-build via needs:).
Move the timeout from the inner "Workaround macOS permission issues"
step (which can't have it) to the outer `uses:` call site in all three
callers instead, covering the whole composite action's four steps.
CI results otherwise confirmed the rest of the refactor and the ccache
changes are working: every Linux and Windows job passed, including the
new windows-audio-test.yml reusable workflow (test/test-sanitizer) and
all the other extracted composite actions.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017BRyLXHuZHy1b9oLt4wJFi
* Add workflow to delete a PR branch's Actions caches when it closes
While investigating whether the repo was at risk of hitting the 10GB
Actions cache quota (it was: 10.03 GiB across 111 entries, meaning
GitHub was almost certainly already silently evicting caches), found
that 40% of that usage (4.06 GiB / 43 entries) was superseded
duplicates -- ccache-action's default append-timestamp behavior writes
a brand-new, uniquely-keyed cache blob on every run and never removes
the previous one, so every active branch's cache footprint only grows
for as long as the branch exists. Deleted those 43 stale entries
(repo-wide, confirmed via the caches API), freeing the repo back down
to ~5.98 GiB.
That only clears the backlog once, though -- it doesn't stop the same
accumulation from happening again on every other branch. This adds the
other half: delete a PR branch's cache entries once the PR closes, so
finished work stops permanently reserving quota that active branches
need.
Loops over `gh cache list`/`gh cache delete` rather than a single
capped call, since a long-lived PR can accumulate more than one page
of cache entries. Uses refs/heads/<branch> (not refs/pull/<n>/merge,
which is what GitHub's own docs example uses) for the BRANCH filter,
since our build workflows trigger on push, not pull_request, so that's
the ref every cache entry we inspected was actually scoped to -- the
docs example wouldn't have matched anything in this repo. Runs on
pull_request_target (not pull_request) so it also covers PRs from
forks; safe here since the job never checks out or executes the PR's
code, only calls the cache API for its branch name.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017BRyLXHuZHy1b9oLt4wJFi
* Add scheduled job to prune stale Actions caches on every branch
cleanup-caches.yml only handles a branch's caches when its PR closes,
which leaves the default branch (no PR to close) and any other
long-lived branch's older cache generations unaddressed -- those keep
accumulating for as long as the branch exists, since ccache-action
writes a brand-new timestamped cache blob every run and nothing else
ever removes the previous one.
Adds a daily scheduled workflow (plus workflow_dispatch for manual
runs) that lists every cache entry repo-wide, groups them by (ref, key
with its trailing timestamp stripped), and deletes every entry in a
group except the most recently created one. It never touches the
newest cache for any (branch, job) pair, so nothing still reachable by
ccache-action's restore step is removed.
Verified the grouping/survivor logic against the repo's live cache
list (94 entries, 68 distinct branch+job groups): every group retained
exactly its newest entry with zero errors, and it correctly identified
26 already-superseded duplicates (~2.8 GiB) that have accumulated
again since the one-time manual cleanup, which is exactly the ongoing
drift this job is meant to catch automatically going forward.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017BRyLXHuZHy1b9oLt4wJFi
* Bring in VB-Cable latency tuning from ms-audio-fifo-wasapi-fixes
That branch (PR #1496) independently added CI steps to override
VB-Cable's default audio latency on both macOS and Windows, inserted at
the same points in the audio setup sequence that are now consolidated
into setup-macos-virtual-audio and setup-windows-audio-drivers here --
applying its changes on top of master directly would have conflicted
with this refactor. Ported both changes into the composite actions
instead, and the corresponding revert is going out on that branch
separately so the two don't diverge or duplicate the work:
- setup-macos-virtual-audio: adds "Override VB-Cable latency settings",
copying the new ci/com.vbaudio.vbcable.plist into
/Library/Preferences, right after "Install VB-Cable".
- setup-windows-audio-drivers: adds "Set VB-Cable Registry settings"
(VBAudioCableWDM/_Latency under HKLM\SOFTWARE\VB-Audio\Cable and the
matching CurrentControlSet\Services key), right after "Install VAC"
and before "Install VB-Cable", matching the original ordering.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017BRyLXHuZHy1b9oLt4wJFi
* Pin freedv_backend to last known-good commit -- RNNoise build regression
CI on this PR surfaced a real failure: the macOS lint job's RNNoise
ExternalProject build now fails with "C compiler cannot create
executables" during ./configure. This traces to freedv-backend#55
(merged 2026-09-18, mine), which baked the ccache launcher into
RNNoise's CC ("ccache <compiler>") -- something about that combination
breaks RNNoise's own configure script on this macOS/LLVM setup in a way
the earlier verification (checking the generated ExternalProject
command file) didn't catch, since that only confirmed the argument was
passed through correctly, not that RNNoise's ./configure would accept
it.
Ebur128 and RADE's launcher forwarding from the same PR use a different
mechanism (CMAKE_ARGS on a nested cmake invocation, not CC) and aren't
implicated by this failure -- but freedv-gui has no way to revert just
one file's change from a merged commit in another repo, so pinning
GIT_TAG back to the immediate parent commit is the only fix available
without editing freedv-backend itself, which is out of scope here.
The three application-level test failures on this same run
(rade_reporting_awgn on Linux, TestFreeDVFullDuplex on both Windows
runners) are the same pre-existing RADE audio-timing flakiness already
seen on earlier runs, unrelated to this change or the CI refactor --
not touched.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017BRyLXHuZHy1b9oLt4wJFi
* Move freedv_backend back to main -- RNNoise regression fixed upstream
freedv-backend#56 (merged) replaces the two-word CC/CXX that broke
RNNoise's ./configure on macOS lint CI with a generated wrapper script,
so the pin from the previous commit is no longer needed. main's HEAD
is now 3d605c7, which includes both the original ccache-forwarding fix
(#55: Ebur128/RADE/RNNoise) and the RNNoise follow-up fix (#56).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017BRyLXHuZHy1b9oLt4wJFi
* macOS lint: use Homebrew llvm@20 instead of install-llvm-action
Diagnostic swap for the RNNoise ccache failure investigation. Two
independent fix attempts in freedv-backend (two-word CC, then a wrapper
script) both failed identically in this job, but the same
ccache-launcher-forwarding mechanism works fine elsewhere: Linux's lint
job uses the exact same explicit -DCMAKE_C_COMPILER_LAUNCHER=ccache
forwarding (clang-21 from apt.llvm.org's llvm.sh) and passes; macOS's
dist-pgo-instrument/dist-pgo-build jobs pass too, though those rely on
ccache-action's create-symlink PATH trick rather than this job's
explicit forwarding. The one thing unique to this failing job is the
compiler binary itself: KyleMayes/install-llvm-action's LLVM 20.1.
Swap that for `brew install llvm@20 lld@20` (matching
dist-pgo-instrument/dist-pgo-build's own package list and comment about
LLVM 21 incompatibilities) and prepend its bin dir to PATH via
GITHUB_PATH, the same mechanism install-llvm-action used internally.
CC=clang/CXX=clang++ and run-clang-tidy in the later steps are unchanged
and will resolve through the new PATH entry.
Deliberately not touching create-symlink on this job's ccache step --
keeping the explicit -DCMAKE_C_COMPILER_LAUNCHER=ccache forwarding as
the only ccache-wiring mechanism in play isolates whether the compiler
binary was actually the variable that mattered, since dist-pgo-instrument
conflates "Homebrew LLVM" with "create-symlink" and doesn't answer that
on its own.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017BRyLXHuZHy1b9oLt4wJFi
* macOS lint: fix clang-tidy sysroot detection with Homebrew llvm@20
Switching to Homebrew's llvm@20 fixed the RNNoise ccache failure, but
surfaced a different problem: run-clang-tidy failed on all 106 files
with errors like "no member named 'ldiv'" and "'TargetConditionals.h'
file not found" -- Homebrew's llvm@20, unlike Apple's own clang or
install-llvm-action's redistributable, doesn't auto-detect the active
Xcode SDK. The actual build step doesn't hit this (CMake sets
CMAKE_OSX_SYSROOT itself via its own xcrun-based detection, independent
of which clang compiled it), but clang-tidy re-invoking the compiler
directly needs the sysroot spelled out.
Pass it explicitly via --extra-arg=-isysroot --extra-arg="$(xcrun
--show-sdk-path)".
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017BRyLXHuZHy1b9oLt4wJFi
* Fix run-clang-tidy flag syntax: single-dash -extra-arg, not --extra-arg
The previous commit used GNU-style --extra-arg=, but run-clang-tidy.py
(confirmed against LLVM 20's actual source) defines this as a
single-dash LLVM-style flag: parser.add_argument("-extra-arg", ...).
The existing "-p ." usage in this same command is the same convention
-- should have followed it. Python's argparse rejected the double-dash
form outright: "unrecognized arguments: --extra-arg=-isysroot
--extra-arg=...", never reaching clang-tidy at all. RNNoise's ccache
build itself succeeded in this run (confirmed in the logs), so this
was the only remaining problem.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017BRyLXHuZHy1b9oLt4wJFi
* Add PR #1498 to changelog.
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>