Commit 9201ef0008

9201ef0008db5c45b33e75e674febe0cd4d4f0d5

parent: b5cffb442d

Verified · cmc

cmc <hello@cleberg.net> · 2026-08-22 19:25 UTC

Remove the GitHub Actions build workflow

Deletes .github/workflows/build.yml. The accessibility audit it ran was failing
on unattributed findings that do not reproduce locally on the same simulator and
OS, and the job is no longer wanted.

Worth recording what goes with it: the workflow was the only check that built a
clean checkout of the merge result. DomainDig.xcodeproj is hand-edited and uses
file-system-synchronized groups, so a folder missing from a commit still builds
locally and breaks only for someone else, and the pre-push hook runs against the
working tree where such a file is still present. Nothing covers that now.

Docs/ACCESSIBILITY.txt described the local/CI split as two halves that each
covered what the other could not, so it is updated rather than left claiming a
job that no longer exists.

Layout: unified · split

.github/workflows/build.yml deleted −127
@@ -1,127 +0,0 @@
1name: Build
2
3# Compile gate and accessibility audit for the GitHub mirror. builds.sr.ht is the
4# primary remote for this project but has no macOS images, so xcodebuild cannot
5# run there; this job builds and tests on a GitHub-hosted macOS runner instead.
6#
7# This runs `xcodebuild test`, which also compiles the app, the widget, and the
8# share extension (the DomainDig scheme's build action pulls both in as
9# dependencies). The scheme has two test targets: DomainDigUITests — an
10# accessibility audit suite; see DomainDigUITests/AccessibilityAuditHarness.swift
11# — and DomainDigTests — unit coverage of the deterministic core (report builder,
12# exporter, diff, portability dedup).
13#
14# WHAT THIS JOB IS FOR, given the audit also runs locally:
15# a clean checkout of the merge result. A local hook runs against the working
16# tree and therefore cannot catch a file that was never committed — the failure
17# mode that matters most here, since DomainDig.xcodeproj is hand-edited and uses
18# file-system-synchronized groups where a whole missing folder still builds fine
19# locally. This job is the only place that check exists; sr.ht cannot run it.
20#
21# DELIBERATELY ONE JOB, NEWEST RUNTIME ONLY. Audit coverage is not nested across
22# OS versions, so the oldest supported OS genuinely needs its own run — but the
23# macos-26 image ships only iOS 26.x runtimes, so CI *cannot* provide it. Asking
24# for two jobs here bought two near-identical 26.x runs at double the macOS
25# minutes. Floor coverage lives in Scripts/audit-a11y.sh, run from a machine that
26# actually has an 18.x runtime installed, and is wired to the pre-push hook in
27# .githooks/. See Docs/ACCESSIBILITY.md for the split.
28#
29# The audit REPORTS but does not FAIL by default. It surfaces violations that
30# exist today, so gating on it would block every unrelated PR until the
31# accessibility pass in issue #21 completes. Findings land in the job log and in
32# the uploaded .xcresult bundle, tagged [report] or [FAIL]. Enforcement is a
33# committed constant: widen `AccessibilityAuditHarness.enforcedAuditTypes` as
34# each phase clears a category. (Env vars were tried first — neither a plain
35# xcodebuild env var nor a TEST_RUNNER_-prefixed build setting reaches the UI
36# test process.)
37#
38# pull_request only, plus manual dispatch. GitHub builds the merge result (PR
39# merged into main), so a green PR validates exactly what will land on main.
40# Note: this repo currently also pushes directly to main for releases, and those
41# pushes are NOT gated here — add a `push: { branches: [main] }` trigger below if
42# you want direct-to-main commits covered too.
43#
44# paths-ignore skips prose-only changes. Both globs are single-star, so they
45# match the repo root and Docs/ but nothing deeper — a .md that ever lands inside
46# a source directory still builds.
47on:
48 pull_request:
49 paths-ignore: ['*.md', 'Docs/*.md']
50 workflow_dispatch:
51
52# The job only reads code; drop the default read-write GITHUB_TOKEN scope.
53permissions:
54 contents: read
55
56concurrency:
57 group: build-${{ github.ref }}
58 cancel-in-progress: true
59
60jobs:
61 test:
62 name: xcodebuild test
63 # macos-latest still points at macOS 15, which lacks the iOS 26+ SDK this app
64 # is built against.
65 runs-on: macos-26
66
67 steps:
68 - uses: actions/checkout@v7
69
70 - name: Show toolchain
71 run: |
72 xcodebuild -version
73 swift --version
74
75 - name: Select simulator
76 id: sim
77 run: |
78 set -euo pipefail
79
80 # Newest available iPhone runtime. No deployment-target filtering is
81 # needed for "newest" — it is always at or above the floor. The
82 # previous selector took the first iPhone from ANY runtime, which on a
83 # machine with an older runtime installed could pick a simulator below
84 # the deployment target, where the app cannot install.
85 selected=$(xcrun simctl list devices available --json \
86 | jq -c '
87 [ .devices | to_entries[]
88 | (.key | capture("SimRuntime\\.iOS-(?<maj>[0-9]+)-(?<min>[0-9]+)$")) as $v
89 | (($v.maj | tonumber) * 1000 + ($v.min | tonumber)) as $rank
90 | .value[]
91 | select(.name | startswith("iPhone"))
92 | { rank: $rank, udid: .udid, name: .name, os: "\($v.maj).\($v.min)" }
93 ]
94 | sort_by(.rank, .name)
95 | last
96 ')
97
98 if [ -z "$selected" ] || [ "$selected" = "null" ]; then
99 echo "::error::No iPhone simulator available on this image"
100 xcrun simctl list devices available >&2
101 exit 1
102 fi
103
104 label=$(echo "$selected" | jq -r '"\(.name) (iOS \(.os))"')
105 echo "Selected $label"
106 echo "udid=$(echo "$selected" | jq -r .udid)" >> "$GITHUB_OUTPUT"
107 echo "label=$label" >> "$GITHUB_OUTPUT"
108
109 - name: Test on ${{ steps.sim.outputs.label }}
110 run: |
111 set -o pipefail
112 xcodebuild test \
113 -project DomainDig.xcodeproj \
114 -scheme DomainDig \
115 -destination "id=${{ steps.sim.outputs.udid }}" \
116 -resultBundlePath TestResults.xcresult \
117 CODE_SIGNING_ALLOWED=NO
118
119 - name: Upload results
120 # Always upload: on success the bundle carries the accessibility burndown
121 # list, which is the reason this suite exists.
122 if: always()
123 uses: actions/upload-artifact@v4
124 with:
125 name: test-results
126 path: TestResults.xcresult
127 retention-days: 7
Docs/ACCESSIBILITY.txt +15 −15
@@ -115,8 +115,8 @@ headers above. Everything the app actually controls passes in both schemes.
115With phases 1–5 landed, `AccessibilityAuditHarness.enforcedAuditTypes` enforces 115With phases 1–5 landed, `AccessibilityAuditHarness.enforcedAuditTypes` enforces
116**`.textClipped`, `.dynamicType`, `.hitRegion`, `.elementDetection`, 116**`.textClipped`, `.dynamicType`, `.hitRegion`, `.elementDetection`,
117`.sufficientElementDescription`, `.trait`** on the empty-state test suite. A 117`.sufficientElementDescription`, `.trait`** on the empty-state test suite. A
118named finding in any of these fails CI — regressions in five phases of work are 118named finding in any of these fails the test run — regressions in five phases of
119now gated, not merely reported. 119work are still gated locally and at the pre-push hook, not merely reported.
120 120
121Three deliberate carve-outs, each with its evidence: 121Three deliberate carve-outs, each with its evidence:
122 122
@@ -143,7 +143,7 @@ Environment variables do not work: neither a plain `xcodebuild` env var nor a
143silently did nothing. And a committed value makes "when did clipping become 143silently did nothing. And a committed value makes "when did clipping become
144enforced?" answerable with `git blame` instead of CI tribal knowledge. 144enforced?" answerable with `git blame` instead of CI tribal knowledge.
145 145
146## Why coverage is split between local and CI 146## Why the floor runtime needs its own run
147 147
148**Audit coverage is not nested across OS versions.** Each runtime reports 148**Audit coverage is not nested across OS versions.** Each runtime reports
149findings the others miss, in *both* directions. Measured on this project: 149findings the others miss, in *both* directions. Measured on this project:
@@ -158,18 +158,18 @@ Neither runtime is a superset, so the oldest supported OS needs its own run.
158This also rules out committing per-screen baseline counts as a regression guard: 158This also rules out committing per-screen baseline counts as a regression guard:
159no single number is correct on both. 159no single number is correct on both.
160 160
161The catch is that **GitHub's `macos-26` image ships only iOS 26.x simulator 161The audit therefore runs locally, across both ends, via
162runtimes.** It cannot test the 17.6 floor at all. A two-job CI matrix was tried 162`Scripts/audit-a11y.sh` — on a machine that actually has an 18.x runtime
163and produced two near-identical 26.x runs at double the macOS minutes. 163installed. It is wired to the pre-push hook in `.githooks/`.
164 164
165So the work is split by what each side can uniquely do: 165There is no CI job. The GitHub Actions workflow that used to run this suite on
166 166the newest runtime has been removed, and with it the one check that no local run
167| | Runtime | Uniquely provides | 167can reproduce: **a clean checkout of the merge result.** That mattered here
168| --- | --- | --- | 168because `DomainDig.xcodeproj` is hand-edited and uses file-system-synchronized
169| **CI** (`.github/workflows/build.yml`) | newest available | A clean checkout of the merge result — catches a file that was never committed, which a local run cannot. Matters here because `DomainDig.xcodeproj` is hand-edited and uses file-system-synchronized groups, where a whole missing folder still builds locally. | 169groups, where a whole missing folder still builds fine locally and breaks only
170| **Local** (`Scripts/audit-a11y.sh`) | oldest supported + newest | Real floor coverage, on a machine that actually has an 18.x runtime installed. | 170for someone else. A local hook runs against the working tree, so it cannot catch
171 171a file that was never committed. Nothing covers that now — verify a fresh clone
172Together they cover both ends; neither duplicates the other. 172by hand before a release.
173 173
174## Running it 174## Running it
175 175