Skip to content

A controls-free pill for push-to-talk #80

A controls-free pill for push-to-talk

A controls-free pill for push-to-talk #80

Workflow file for this run

# The gate that runs before anything is called a version.
#
# `release.yml` runs the suite too, but only on a `v*` tag — so between tags nothing was
# checked, and the first thing that could find a broken push was a release. Release stays
# tag-only and deliberate; this is the one that runs on the way in.
name: CI
on:
pull_request:
push:
branches: [main]
# A second push to the same ref makes the first run's answer worthless before it lands.
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
suite:
# Two platforms, and the second one is the whole point: the suite's law is that the
# platform decides what imports and `lite` decides what happens, which had never been
# run anywhere but Windows. macOS now says it holds — **1128 of 1168 tests pass**, and
# the 40 that do not are six named Win32 mechanisms, each carrying a `skipUnless` that
# says which.
#
# **Ubuntu was here and was dropped on evidence, 2026-08-03.** uv's managed CPython
# ships tkinter on macOS and not on Linux, and the suite reaches `flow.ui` in six
# modules — 139 errors that say nothing about Flow and everything about that build.
# macOS gives the same "not Windows" signal *with* the UI included, so the Linux leg
# was buying a second opinion at the cost of a permanently red badge, and a CI nobody
# believes is worse than no CI. Worth re-adding the day it can run the UI: the leg was
# `ubuntu-latest` plus `sudo apt-get install -y libportaudio2` before the sync, which
# did work — sounddevice bundles PortAudio for Windows and macOS only.
#
# The third leg re-runs Windows on CPython 3.14, because the `.python-version` pin
# cuts both ways: every dev venv is 3.12 now, so the first 3.13+ divergence
# (`ntpath.isabs`, 2026-08-15, seven tests red) was found by an *unpinned* venv and
# nothing else could have found it. One leg past the pin finds the next one here,
# on purpose. Windows only — path semantics are where the interpreter moved.
name: suite (${{ matrix.os }}, py${{ matrix.python }})
runs-on: ${{ matrix.os }}
strategy:
# One platform failing is a fact about that platform; the other answer is what says
# whether it is specific to it.
fail-fast: false
matrix:
os: [windows-latest, macos-latest]
python: ["3.12"]
include:
- os: windows-latest
python: "3.14"
# Both actions below are pinned to a commit SHA, not a tag. A tag is a mutable
# pointer the action's owner can repoint at any time, which is how the trivy-action
# and kics-github-action compromises reached everyone using them at once. The
# trailing comment is the human-readable half: bump the SHA and the version together.
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
# Pinned, and the pin is the point. The first run of this workflow used whatever
# each runner happened to have: **CPython 3.12.3 from `/usr/bin/python3` on Ubuntu
# and 3.14.6 from Homebrew on macOS** — two minor versions past what this project
# targets, and three legs running three unrelated experiments rather than one.
# It also cost 137 of the 153 errors: Debian splits `tkinter` into `python3-tk`,
# and the suite reaches `flow.ui` in six modules. uv's own build is the same
# interpreter everywhere, which is the only way the matrix answers a question.
# The matrix supplies the version now — still pinned per leg, never the runner's
# choice, and this input outranks `.python-version`, which is what lets the 3.14
# leg exist beside the pin at all.
- uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
with:
enable-cache: true
python-version: ${{ matrix.python }}
# `--frozen` and not a plain sync: a run that silently resolves a newer dependency
# is testing a tree nobody has, and reports green about it.
- run: uv sync --frozen
- name: unit suite
run: uv run python -m unittest discover -s tests
# Catches a syntax error in a module no test happens to import — the failure that
# a green suite is structurally unable to see.
- name: compile everything
run: uv run python -m compileall -q flow scripts
# And an entry point that cannot boot at all, which is the one thing every user
# does first and no unit test does.
- name: the entry point starts
run: uv run flow --help
# The one part of Flow that is not Python, on the only runner that can compile it.
#
# `native/flow_stt.swift` is the macOS on-device decoder — the answer for machines that
# cannot reach huggingface.co, where faster-whisper's weights are the only official
# copy. It is written on Windows, where there is no Swift toolchain, so without this
# leg the compiler is a person on a laptop pasting errors back. The first version was
# exactly that and shipped with a hard one in it: Swift allows top-level statements
# only in a file called `main.swift`, and this is not one.
#
# Compile only, and that is the honest limit of what CI can say here. Actually
# *running* it needs a granted Speech Recognition permission and an on-device model
# that only arrives when a human enables Dictation — neither of which a headless runner
# has, and faking them would make this leg green about something it never checked.
# `--probe` would exit 2 on the runner for exactly that reason, which is the right
# answer and a useless test.
helper:
name: swift helper compiles
runs-on: macos-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- name: swiftc version
run: xcrun swiftc --version
# `-warnings-as-errors` because this file is edited by people who cannot run it.
# A warning here is the only signal that reaches them before a user does.
- name: compile the macOS decoder
run: xcrun swiftc -O -parse-as-library -warnings-as-errors -o /tmp/flow-stt native/flow_stt.swift
# It links against AVFoundation and Speech, so a binary that builds but cannot
# resolve a symbol is still a broken helper. Asking it for its usage line proves
# the dynamic linker is satisfied without needing a microphone or a permission.
- name: it links and runs far enough to refuse
run: |
set +e
out=$(/tmp/flow-stt --nonsense 2>&1)
code=$?
echo "$out"
# 1 is `die("usage: ...")`. Anything else — a link failure, a crash, or a
# silent success on an argument that is not valid — is a broken build.
if [ "$code" -ne 1 ]; then echo "expected exit 1, got $code"; exit 1; fi
case "$out" in *usage*) ;; *) echo "expected a usage line"; exit 1 ;; esac