Commit 9201ef0008
Verified · cmc
Layout: unified · split
.github/workflows/build.yml deleted −127
| @@ -1,127 +0,0 @@ | ||
| 1 | name: 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. | |
| 47 | on: | |
| 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. | |
| 53 | permissions: | |
| 54 | contents: read | |
| 55 | ||
| 56 | concurrency: | |
| 57 | group: build-${{ github.ref }} | |
| 58 | cancel-in-progress: true | |
| 59 | ||
| 60 | jobs: | |
| 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. | ||
| 115 | 115 | With phases 1–5 landed, `AccessibilityAuditHarness.enforcedAuditTypes` enforces |
| 116 | 116 | **`.textClipped`, `.dynamicType`, `.hitRegion`, `.elementDetection`, |
| 117 | 117 | `.sufficientElementDescription`, `.trait`** on the empty-state test suite. A |
| 118 | named finding in any of these fails CI — regressions in five phases of work are | |
| 119 | now gated, not merely reported. | |
| 118 | named finding in any of these fails the test run — regressions in five phases of | |
| 119 | work are still gated locally and at the pre-push hook, not merely reported. | |
| 120 | 120 | |
| 121 | 121 | Three 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 | ||
| 143 | 143 | silently did nothing. And a committed value makes "when did clipping become |
| 144 | 144 | enforced?" 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 | 148 | **Audit coverage is not nested across OS versions.** Each runtime reports |
| 149 | 149 | findings 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. | ||
| 158 | 158 | This also rules out committing per-screen baseline counts as a regression guard: |
| 159 | 159 | no single number is correct on both. |
| 160 | 160 | |
| 161 | The catch is that **GitHub's `macos-26` image ships only iOS 26.x simulator | |
| 162 | runtimes.** It cannot test the 17.6 floor at all. A two-job CI matrix was tried | |
| 163 | and produced two near-identical 26.x runs at double the macOS minutes. | |
| 164 | ||
| 165 | So the work is split by what each side can uniquely do: | |
| 166 | ||
| 167 | | | Runtime | Uniquely provides | | |
| 168 | | --- | --- | --- | | |
| 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. | | |
| 170 | | **Local** (`Scripts/audit-a11y.sh`) | oldest supported + newest | Real floor coverage, on a machine that actually has an 18.x runtime installed. | | |
| 171 | ||
| 172 | Together they cover both ends; neither duplicates the other. | |
| 161 | The audit therefore runs locally, across both ends, via | |
| 162 | `Scripts/audit-a11y.sh` — on a machine that actually has an 18.x runtime | |
| 163 | installed. It is wired to the pre-push hook in `.githooks/`. | |
| 164 | ||
| 165 | There is no CI job. The GitHub Actions workflow that used to run this suite on | |
| 166 | the newest runtime has been removed, and with it the one check that no local run | |
| 167 | can reproduce: **a clean checkout of the merge result.** That mattered here | |
| 168 | because `DomainDig.xcodeproj` is hand-edited and uses file-system-synchronized | |
| 169 | groups, where a whole missing folder still builds fine locally and breaks only | |
| 170 | for someone else. A local hook runs against the working tree, so it cannot catch | |
| 171 | a file that was never committed. Nothing covers that now — verify a fresh clone | |
| 172 | by hand before a release. | |
| 173 | 173 | |
| 174 | 174 | ## Running it |
| 175 | 175 | |