Skip to content

ci: give check:affected real Apple ownership rules and route ios.yml on them (#1781 A9-2) - #1857

Merged
thymikee merged 5 commits into
mainfrom
ci/1781-a9-2-apple-ownership
Aug 19, 2026
Merged

ci: give check:affected real Apple ownership rules and route ios.yml on them (#1781 A9-2)#1857
thymikee merged 5 commits into
mainfrom
ci/1781-a9-2-apple-ownership

Conversation

@thymikee

@thymikee thymikee commented Aug 18, 2026

Copy link
Copy Markdown
Member

Summary

pnpm check:affected gains device-lane ownership by platform family, and ios.yml is routed on it at pull_request time via a paths-ignore list that the gate manifest holds to the selector over every tracked path, both ways. Part of #1781 (A9-2).

Before: a TypeScript-only Apple change (src/platforms/apple/**, packages/platform-apple/**) produced a plan with no iOS check at allswift-runner-ios fired only for .swift/apple/runner/**, and replay-ios was reachable only through a full fail-open. ios.yml ran on every non-docs PR, including Android-only and unit-test-only ones.

After:

$ pnpm check:affected   # diff touching src/platforms/apple/core/apps.ts
  - replay-ios: iOS simulator replay suite (GitHub-authoritative; not run locally)
      · src/platforms/apple/core/apps.ts [own:device-lane:apple] — apple-owned tree: only the apple device lanes execute it
  - replay-ios-device … - replay-macos …
$ pnpm check:affected   # diff touching src/platforms/android/perf.ts
  - replay-android …      (no iOS check; ios.yml does not start)
$ pnpm check:affected   # diff touching src/platforms/android/perf.ts + package.json
Fail-open: selecting the full check set.   (ios.yml starts)

push to main is unfiltered: main runs every lane unconditionally.

The four lines

  • Catches: nothing new — this is a cost item. It removes iOS-lane runs the lane cannot use (unit-test-only and other-family-only PRs) and, as a side effect, makes check:affected tell the truth about Apple changes (TS-only Apple PRs such as fix(ios): preserve final-probe xcodebuild diagnostics #1776/refactor: narrow perf API to actionable evidence #1731/fix(ios): use current devicectl capture-screenshot syntax for physical devices #1769 now carry replay-ios instead of a plan that omits iOS entirely).
  • Evidence: 90-day retrospective replay below, plus an independent adversarial re-derivation (692 merged PRs / 60 days, 31 incremental would-skips cross-referenced against all 118 red ios.yml PR runs of the last 30 days → 0 hits) — 871 merged PRs, 3,286 pull_request ios.yml runs; would-skip-and-really-broke-the-runner = 0. Planted-red proofs: scripts/check-affected/device-lanes.test.ts (Apple file → replay-ios; Android-only + any tooling file → full fail-open) and scripts/gate/routing.test.ts (an ignore hiding src/platforms/apple/** or .github/actions/** fails the manifest; dropping src/platforms/android/** or the unit-test ignore fails it the other way).
  • Cost: wall-clock +0 (GitHub evaluates paths-ignore before it allocates a runner; no plan job on the critical path, no if: on the 12 smoke steps); macOS runner occupancy −61 runs / −691 min per 90 days over what A9-1 already saved (1.9% of the lane; last 30 days: −31 runs / −422 min of 16,490); red-lane attention 0 (no new lane); harness ≈ 165 LOC selector + 100 LOC manifest assertion + 23 YAML lines, all under existing gates (check:affected:test, check:gate-manifest:test).
  • Kill-criterion: was "one would-have-been-skipped PR that broke the runner for a real reason" — not met (0/35). Going forward: if main goes red on ios.yml for a change a routed PR skipped (a cross-family lifecycle call, see residual risk), drop the routing entries from ios.yml and the ROUTED_LANES record in the same commit; the ownership rules stay.

Design note

What enumerates the Apple surface? Not the import graph. The daemon value-imports every family: src/platform-runtime.ts composes all six platform packages, src/core/interactors/* import each family's actions, and src/daemon/session-teardown.ts / src/daemon/server/daemon-runtime.ts call Android and web lifecycle helpers on every session close and daemon start. Reachability from src/bin.ts is therefore the whole tree, and the layering graph additionally does not resolve relative imports inside packages/ (intra-package edges are invisible; packages/contracts/src/facades/* fan out to nothing). Transitive ownership derived from that graph would either classify everything as Apple or miss what matters.

What the repo does enforce is the platform partition: CANONICAL_PLATFORM_FAMILIES (scripts/layering/platform-package-policy.ts) names the families, layering R13 pins each family's runtime to packages/platform-<family>/ and src/platforms/<family>/, and the remaining family-owned trees are named by a family or Apple-leaf directory segment (android/, linux/, test/integration/replays/<leaf>/, src/daemon/snapshot-presentation/ios/) or, under test/integration/, by the lane prefix of the smoke file. So scripts/check-affected/device-lanes.ts — the single declaration site — says:

path leaf lanes
tagged by exactly one family / Apple leaf that leaf apple → replay-ios, replay-ios-device, replay-macos · ios → replay-ios, replay-ios-device · macos → replay-macos · android → replay-android · linux → replay-linux · web → web-smoke · harmonyos/vega → none
untagged, or tagged by two families shared every device lane
examples/test-app/** fixture-app replay-ios, replay-ios-device, replay-android
unit test under src/, packages/*/src/ none (no device lane runs Vitest; the build excludes them)
non-TS the selector cannot place (src/*.json, a .png under test/integration/) unchanged: still fails open through the ambiguous-path guard

Filename tags in shared directories are deliberately not used (src/daemon/android-system-dialog.ts stays shared) — a naming convention is not a boundary. contracts/fixtures/*.json (TS/Swift golden tables) get their own build rule: unit + swift-runner-ios + swift-runner-macos instead of fail-open.

Decide before allocating macOS. Three mechanisms were compared: (a) an ubuntu plan job the smoke job needs — adds ~1 min serial latency to all 3,286 iOS runs to skip 61, on the lane that is usually a PR's critical path; (b) computing the plan inside the macOS job and skipping the rest — the runner is still allocated for ~2 min, and 12 steps need an if:; (c) paths-ignore — GitHub evaluates it before creating the job, so it costs nothing and adds no latency, but it is a hand-written glob list next to a derived selector. (c) is shipped, made honest by scripts/gate/routing.ts: for every tracked path, the lane must start when the selector fails open on it or routes it to one of the lane's declared gates (swift-runner-ios) or sampled checks (replay-ios, replay-ios-device, declared once in ROUTED_LANES), and must not start on a path the selector classifies as another family's or as a unit test. Docs paths are exempt; a .github/** path is exempt only when ignored by its exact name (deploy/docs-preview workflows the lane does not use — a glob there would fail).

Fail-open on tooling. Unchanged and now asserted: .github/**, scripts/**, package.json, pnpm-lock.yaml, tsconfig*.json, vitest*.ts, packages/*/package.json|tsconfig.json, and the selector's own sources fail open to the full set, so any PR touching one of them starts ios.yml regardless of what else it touches (routing.test.ts plants .github/actions/** and package.json into the ignore list and expects the manifest to fail).

Residual risk — large PRs. GitHub's path filters examine only the first 300 changed files. A PR whose first 300 paths all match the ignore list skips the lane even if file 301 is Apple code (in-window examples of PRs past the limit: #1499 at 511 files, #1494 at 865). Inherent to paths-ignore and shared with A9-1, but this list matches many more files than A9-1's did, so the exposure is larger. Backstop is the unfiltered push trigger on main. Recorded in docs/agents/testing.md.

Residual risk (stated, not hidden). A family-tagged production file can execute on another family's lane through a cross-family lifecycle call — 50 files under other-family roots are directly value/dynamic-imported by shared code today (pnpm depgraph), e.g. session-teardown.ts → src/platforms/android/perf.ts, daemon-runtime.ts → src/platforms/web/agent-browser-lifecycle.ts. Backstops: ci.yml's node-integration lanes start a real daemon (import-time and startup breakage), the unit/provider suites, and the unfiltered push trigger on main. The 90-day replay found no instance. A seam-based refinement (treat those 50 as shared, derived from the layering graph) is available if this ever bites; it was not shipped because it would need paths: with 50 negations and churn on every new cross-import.

Also fixed on the way: check:gate-manifest's path-coverage assertion now exempts declared manual-only checks the way its owned assertion already did (the parked replay-ios/replay-android lanes are printed by name on every run; repeating the gap once per path that selects them buried the real findings).

Retrospective replay (90 days, ran before routing)

Method: every PR merged since 2026-05-20 (GraphQL, files + commit oids), every pull_request ios.yml run since 2026-05-15 (REST, 4,455 runs), matched by head SHA with a branch-name fallback inside the PR's lifetime; each PR's file list run through the new selector in-process, and separately through the new paths-ignore list.

PRs ios.yml PR runs macOS min
merged PRs in window (27 with >100 files counted as "would run") 871 3,286 35,541
plans that fail open (tooling/workflow/scripts) 345 (40%)
skipped by paths-ignore as of main (A9-1, docs-only) 39 49 437
skipped by the new list 74 (8.5%) 110 1,128
incremental: routing adds 35 (4.0%) 61 (1.9%) 691 (1.9%)
incremental, last 30 days 18 31 of 1,267 422 of 16,490
incremental would-skip with a red ios.yml run 1 1
incremental would-skip that broke the runner 0

Incremental would-skip by class: Android-only 14 (e.g. #1808 #1737 #1734 #1561 #1513 #1378 #1314 #1285 #1090 #903 #796 #788 #671 #1531), unit-tests-only 20 (e.g. #1790 #1642 #1552 #1113 #595), web-only 1 (#835). Their 61 runs: 53 green, 7 cancelled, 1 red.

The one red: #796 (src/platforms/android/snapshot-helper-session.ts + its test), run 27434816426, step "Prepare iOS runner" — Daemon request timed out at the then-300 s prepare ios-runner budget (a cold xcodebuild). The same step failed the same day on two unrelated PRs (runs 27420294661, 27407004126); the PR's follow-up commit changed two Android lines plus a test and went green; the file set cannot reach xcodebuild. Classified infra flake. (Under the selector-only view a second red appears, #1347, docs-only CONTEXT.md + docs/adr — "Boot iOS test simulator" failed; docs cannot break the runner and A9-1 already skips it.)

Selector vs YAML agreement over the 871 PRs: the YAML would-skip set equals the selector's except four PRs the YAML skips and the selector fails open on — #1836/#1547 (docs/agents/testing.md, selector-owning prose) and #607/#603 (docs-preview workflows ignored by exact name), the two documented exemptions.

Review round (independent adversarial review — routing approved)

Findings addressed in the two commits at the head of this branch (ci: bound the routed-lane exemption to sibling workflows and ci: close the routed-lane exemption over composite-action support files):

  1. Real hole, fixed in code. The exact-name .github exemption was unbounded: the reviewer planted setup-apple-runner-build/action.yml and boot-ios-test-simulator/action.yml — the lane's own build and boot actions — into paths-ignore and check:gate-manifest stayed green. Lane now carries uses: the transitive local composite-action closure plus the workflow's own file, from the same walk declaredGates performs; the exemption refuses anything in it. Both planted cases (plus run-gate/action.yml, reached only through a composite action, and ios.yml itself) are pinned in routing.test.ts and proven red against the pre-fix code.

    1b. The same hole one level deeper, also fixed. A follow-up review found the closure stopped at the YAML boundary: it recorded each action's action.yml but not the files that descriptor executes, so .github/actions/setup-fixture-app/fetch-artifact.sh — genuine iOS-lane machinery, run as bash "$GITHUB_ACTION_PATH/fetch-artifact.sh" — was still exemptible. The closure's unit is the action's directory now. That is deliberate over parsing $GITHUB_ACTION_PATH/... out of the YAML: fetch-artifact.sh itself runs resolve-artifact-name.sh and trusted-artifact.mjs, references that exist only inside shell, so a YAML-parsing fix would have repeated the same one-level-short failure. A directory needs no shell model and cannot miss a file however deep the chain runs; the coarseness is harmless because a file in an action's own directory belongs to that action. All three files are pinned and red against the descriptor-only closure.

  2. Wrong remedy in a correct failure, fixed. A tracked non-TS file under an ignored family root (src/platforms/android/probe-fixture.json) correctly failed, but told the reader to remove the ignore entry — which would un-route every sibling .ts in that tree. Fail-open causes are now split: unknown-path/ambiguous-path ask for a selector owner, everything else still asks for the entry's removal. Pinned, also red against the pre-fix code.

  3. 300-file limit, documented. See the residual-risk note above and docs/agents/testing.md.

Merge condition (ratchet-class — do not skip)

The routing assertion runs over every tracked path and reads every workflow, so it behaves like the layering/coverage ratchets: an in-flight PR that adds a family-tagged path, a non-TS file under an ignored root, or a new composite-action support file is green on its own base and turns check:gate-manifest red on main after both land (the #1825 × #1779 failure mode). Re-run pnpm check:gate-manifest against whatever main is at merge time — not against the base this PR was last rebased onto, which goes stale within hours — and merge after any in-flight PR that adds paths under src/platforms/*, packages/platform-*, android/, linux/, test/integration/replays/*, or .github/actions/*.

Validation

  • check:affected:test (47), check:gate-manifest:test (45, incl. the 8 new routing witnesses and the path-coverage exemption witness), depgraph:test (22) green; pnpm check:gate-manifest green (46 checks wired across 33 lanes, manual-only: replay-android, replay-ios, replay-ios-device).
  • Planted red, locally (all pinned as tests): adding src/platforms/apple/** to ios.yml paths-ignorerouting: ios.yml ignores src/platforms/apple/core/apps.ts, but the selector routes it to "replay-ios", "replay-ios-device", "replay-macos"; adding .github/actions/**… fails open on it (workflow-tooling); removing src/platforms/android/**ios.yml starts on src/platforms/android/adb-executor.ts, which the selector classifies as android-owned (lanes: replay-android). Plus the two review plants above. All are pinned in scripts/gate/routing.test.ts; the two review cases were re-run against the pre-fix code and fail there.
  • This PR itself fails open (.github/**, scripts/check-affected/**, package.json, docs/agents/testing.md), so ios.yml runs on it — CI on this PR is the live proof that the fail-open direction still starts the lane.
  • Not verified live: a PR whose files are all under the new ignore list not starting ios.yml. That is GitHub's paths-ignore semantics ("skip when every changed file matches"), the same mechanism A9-1 relies on; the first Android-only or test-only PR after merge will show it.

Docs: docs/agents/testing.md (selector section + gate-manifest section) updated; skills untouched.

Touched: 14 files (4 new). Scope stayed in scripts/check-affected, scripts/gate, .github/workflows/ios.yml, testing docs.

@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown

Size Report

Metric Base Current Diff
JS raw 2.32 MB 2.32 MB 0 B
JS gzip 762.2 kB 762.2 kB 0 B
npm tarball 885.6 kB 885.7 kB +8 B
npm unpacked 3.09 MB 3.09 MB +44 B

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 28.9 ms 29.7 ms +0.8 ms
CLI --help 69.5 ms 71.8 ms +2.3 ms

Top changed chunks: no changes in the largest emitted chunks.

@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-08-19 15:36 UTC

@thymikee

Copy link
Copy Markdown
Member Author

Not ready at exact 4992259e.

The body claims the dependency-closure fix is in 7398ded24, but that commit is not pushed or reachable: both the source branch and refs/pull/1857/head still point to 4992259e. The claimed fix is therefore not reviewable or checked.

At the actual head, the routing P1 remains: any ignored .github/** file named exactly is exempted without proving that ios.yml does not directly or transitively use it, and the current test explicitly blesses an arbitrary workflow exemption. A lane-owned composite-action change can still bypass the manifest.

The Coverage failure is inherited main-side ratchet skew tracked by #1860, not caused by this patch. The body is also stale: it describes the unpushed fix and says 14 touched files while the exact PR contains 13.

thymikee added a commit that referenced this pull request Aug 19, 2026
…1857)

The exact-name .github exemption was unbounded: naming the lane's own
setup-apple-runner-build or boot-ios-test-simulator action skipped the lane
that runs them and the manifest stayed green. Lane now carries the transitive
composite-action closure plus its own workflow file (Lane.uses, same walk
declaredGates does), and the exemption refuses anything in it.

Also: an unowned path under an ignored root (a non-TS fixture under a family
root) asked for the ignore entry to be removed, which would un-route every
sibling in that tree; it now asks for a selector owner. Both cases pinned,
both proven red against the pre-fix code. Documents GitHub's 300-changed-file
path-filter limit in docs/agents/testing.md.
@thymikee
thymikee force-pushed the ci/1781-a9-2-apple-ownership branch from 4992259 to 1f4ed03 Compare August 19, 2026 08:39
@thymikee

Copy link
Copy Markdown
Member Author

Re-reviewed GitHub exact head 4992259e: unchanged and still not ready. The claimed dependency-closure fix 7398ded24 is still not pushed or reachable from any fetched remote branch, so it is neither reviewable nor covered by authoritative checks. At the actual head, exact-name .github/** exemptions are still not bounded by ios.yml’s direct/transitive local-action dependency closure, and the test still blesses an arbitrary workflow exemption; a lane-owned composite-action change can bypass the routing manifest.

The body is materially stale: it describes the unpushed fix and says 14 touched files, while GitHub reports 13. The PR base b12a3e3c is also three commits behind current main fda81c512, so the old checks are not current-base readiness evidence. The old-head Coverage failure is inherited pre-#1860 ratchet skew; other old-head lanes were green. Push the claimed fix, rebase, and rerun exact-head gates before substantive re-review.

thymikee added a commit that referenced this pull request Aug 19, 2026
…1857)

The exact-name .github exemption was unbounded: naming the lane's own
setup-apple-runner-build or boot-ios-test-simulator action skipped the lane
that runs them and the manifest stayed green. Lane now carries the transitive
composite-action closure plus its own workflow file (Lane.uses, same walk
declaredGates does), and the exemption refuses anything in it.

Also: an unowned path under an ignored root (a non-TS fixture under a family
root) asked for the ignore entry to be removed, which would un-route every
sibling in that tree; it now asks for a selector owner. Both cases pinned,
both proven red against the pre-fix code. Documents GitHub's 300-changed-file
path-filter limit in docs/agents/testing.md.
@thymikee
thymikee force-pushed the ci/1781-a9-2-apple-ownership branch from 1f4ed03 to 354126c Compare August 19, 2026 09:44
@thymikee

Copy link
Copy Markdown
Member Author

The re-review above read head 4992259e, which was correct at the time and is now three pushes stale. Current head is 354126c35, rebased onto current main (6984a1e09, #1852 included) — base is no longer behind.

What is now at the head, all of it reviewable:

  • Dependency-closure fix pushed (354126c35, was 7398ded24 pre-rebase). Lane carries uses: the transitive local composite-action closure plus the workflow's own file, built by the same walk declaredGates already performs (scripts/gate/workflows.ts). exemptSiblingWorkflow refuses any exact-name ignore that names a file in it, so a lane-owned composite-action change can no longer bypass the routing manifest. The reviewer's exact plants — .github/actions/setup-apple-runner-build/action.yml and .github/actions/boot-ios-test-simulator/action.yml — are pinned in scripts/gate/routing.test.ts, together with run-gate/action.yml (reachable only through a composite action) and ios.yml itself. The earlier test that "blessed an arbitrary workflow exemption" now only blesses a sibling workflow (size.yml), which is the claim the exemption actually makes.
  • Both new cases proven red against the pre-fix code: reverting the two guards fails exactly those two tests (naming .github/actions/setup-apple-runner-build/action.yml exactly must still fail the routing assertion), so they are not vacuous.
  • Second finding fixed: an unowned non-TS path under an ignored family root (src/platforms/android/probe-fixture.json) no longer says "Remove the ignore entry" — removing src/platforms/android/** would un-route every sibling .ts — it asks for a selector owner instead. Also pinned, also red pre-fix.
  • Third finding documented: GitHub's path filters examine only the first 300 changed files, so a large PR can skip the lane on its first 300 paths alone. In docs/agents/testing.md and the PR body's residual-risk section; push to main stays unfiltered as the backstop.
  • Touched-file count corrected and verified against the pushed tree: git diff --name-status origin/main..HEAD reports 14 files, 4 new (device-lanes.ts, device-lanes.test.ts, routing.ts, routing.test.ts), which is what the body now says. The earlier "13" was GitHub counting the pre-fix head.

Local evidence on the rebased tree: pnpm check:affected --run green end-to-end, pnpm check:gate-manifest green (46 checks wired across 33 lanes, manual-only: replay-android, replay-ios, replay-ios-device), routing + device-lane suites 21/21.

Note for whoever merges — this is ratchet-class, as the body says: the routing assertion runs over every tracked path, so a path added on main after this base can turn check:gate-manifest red on main post-merge (#1825 × #1779). Re-run pnpm check:gate-manifest against current main immediately before merging.

@thymikee

Copy link
Copy Markdown
Member Author

Re-reviewed exact 354126c35e9b6bd2cf94108f6b719257efebdf2d: the prior .github exemption bypass is fixed. Lane ownership now includes the workflow plus transitive local composite-action descriptors, and focused tests non-vacuously pin ios.yml, direct actions, and the transitive run-gate action. No code finding remains.

Before readiness: update the stale body reference from unreachable 7398ded24 to the current fix/head, await all exact-head CI green, and perform the promised immediate pre-merge pnpm check:gate-manifest rerun against then-current main. PR remains draft.

@thymikee
thymikee marked this pull request as ready for review August 19, 2026 10:02
@thymikee

Copy link
Copy Markdown
Member Author

Out of draft. Head 354126c35 on base 6984a1e09 (current main, #1852 included): 30/30 checks pass, 0 failures — CI, iOS, macOS, Android, Linux, Size, Test App Build Cache, Deploy PR previews all green on this exact head (CodeQL skipped as usual for this path set).

The iOS lane started on this PR rather than being skipped — that is the fail-open direction proving itself in production: the diff touches .github/workflows/ios.yml, scripts/check-affected/**, scripts/gate/**, package.json and docs/agents/testing.md, every one of which the selector fails open on, so routing must not skip it. Linux was cancelled twice earlier at the job's 30-minute timeout inside sudo apt-get install — a repo-wide condition that also cancelled main's own runs at 08:38, 08:46, 09:06 and 09:08 today — and passed on the rebased head.

Local evidence on this tree: pnpm check:affected --run green end to end, pnpm check:gate-manifest green (46 checks wired across 33 lanes, manual-only: replay-android, replay-ios, replay-ios-device), routing + device-lane suites 21/21, both review-round guards proven red against their pre-fix code.

Merge note (ratchet-class, repeated from the body): the routing assertion runs over every tracked path, so a path landing on main after this base can turn check:gate-manifest red post-merge (#1825 × #1779). Re-run pnpm check:gate-manifest against current main immediately before merging.

thymikee added a commit that referenced this pull request Aug 19, 2026
…1857)

The exact-name .github exemption was unbounded: naming the lane's own
setup-apple-runner-build or boot-ios-test-simulator action skipped the lane
that runs them and the manifest stayed green. Lane now carries the transitive
composite-action closure plus its own workflow file (Lane.uses, same walk
declaredGates does), and the exemption refuses anything in it.

Also: an unowned path under an ignored root (a non-TS fixture under a family
root) asked for the ignore entry to be removed, which would un-route every
sibling in that tree; it now asks for a selector owner. Both cases pinned,
both proven red against the pre-fix code. Documents GitHub's 300-changed-file
path-filter limit in docs/agents/testing.md.
@thymikee
thymikee force-pushed the ci/1781-a9-2-apple-ownership branch from 354126c to a600673 Compare August 19, 2026 12:11
@thymikee

Copy link
Copy Markdown
Member Author

Pre-merge rebase done — the ratchet condition this PR's body warns about arrived for real.

Head is now a60067383, rebased onto d07b83762 (current main). Five commits landed under the old base: #1861 (d07b83762, renamed ci.yml's swift-runner-unit-compile / "Swift Runner Unit Compile" → swift-runner-host-tests / "Swift Runner Host XCTests"), #1854, #1853, #1872 (layering: relative imports inside workspace packages), #1867.

Did the rename require a change here? No — and confirmed rather than assumed. The rebase was clean because this branch never touches ci.yml (git diff --name-only origin/main...HEAD has zero ci.yml entries), so main's new job name came in verbatim. The routing rules carry no job names at all: device-lanes.ts routes by directory tag, and routing.ts resolves lanes by the label the loader derives from the YAML, so a renamed job is picked up automatically. owningLanes on the rebased tree now reports swift-runner-macos -> Swift Runner Host XCTests | macOS / Smoke Tests — the manifest tracked the rename with no edit.

Does the renamed host-XCTest job need an ownership rule? No. It declares exactly one gate, swift-runner-macos, through setup-apple-runner-build; its XCTest step is raw run: shell, which by design declares no ownership, and #1861 added no new CheckId (the universe is unchanged at 46 wired). It lives in ci.yml, whose paths-ignore is docs-only and which this PR does not touch or route — so every non-docs PR, including a TypeScript-only Apple change, already starts it. Adding a device-lane rule would claim ownership of a check the lane already runs unconditionally.

Re-run against main at d07b83762: pnpm check:gate-manifest green (46 checks wired across 33 lanes, manual-only: replay-android, replay-ios, replay-ios-device) — it had nothing to say about the renamed job or the new lane. pnpm check:affected --run green end to end on the rebased head; routing + device-lane suites 21/21.

The merge condition in the body still stands for whoever merges: re-run pnpm check:gate-manifest against main immediately before merging, since the assertion reads every tracked path and every workflow.

@thymikee

Copy link
Copy Markdown
Member Author

Re-reviewed exact a60067383. One routing bypass remains: Lane.uses records only transitive action.yml descriptors, not files those composite actions execute. ios.yml uses setup-fixture-app, which runs $GITHUB_ACTION_PATH/fetch-artifact.sh and sibling scripts, but those files are absent from lane.uses; adding that exact path to paths-ignore is accepted as a sibling exemption and the manifest stays green even though the file is iOS-lane machinery. Extend the closure to action support files or narrow the exemption, and add a non-vacuous planted witness. Also replace stale 7398ded24 in the body with current a60067383. Exact-head CI is green; retain the promised immediate pre-merge gate-manifest rerun. No ready-for-human yet.

…on them (#1781 A9-2)

Device-lane ownership by platform family in the affected selector
(scripts/check-affected/device-lanes.ts): a TypeScript-only Apple change now
carries replay-ios/replay-ios-device/replay-macos in a narrow plan, other
families own only their own lanes, shared runtime surface owns every lane,
unit tests own none. Golden tables (contracts/fixtures) own the parity unit
test and both runner builds instead of failing open.

ios.yml pull_request paths-ignore is routed on that ownership; the gate
manifest asserts the list against the selector over every tracked path both
ways (scripts/gate/routing.ts, ROUTED_LANES). push to main is unfiltered.
Path coverage exempts declared manual-only checks the way owned does.
…1857)

The exact-name .github exemption was unbounded: naming the lane's own
setup-apple-runner-build or boot-ios-test-simulator action skipped the lane
that runs them and the manifest stayed green. Lane now carries the transitive
composite-action closure plus its own workflow file (Lane.uses, same walk
declaredGates does), and the exemption refuses anything in it.

Also: an unowned path under an ignored root (a non-TS fixture under a family
root) asked for the ignore entry to be removed, which would un-route every
sibling in that tree; it now asks for a selector owner. Both cases pinned,
both proven red against the pre-fix code. Documents GitHub's 300-changed-file
path-filter limit in docs/agents/testing.md.
Lane.uses recorded only each composite action's action.yml, so a support file
the descriptor executes was exemptible as if it were an unrelated sibling
workflow: ios.yml uses setup-fixture-app, whose action.yml runs
"$GITHUB_ACTION_PATH/fetch-artifact.sh", and that script runs its siblings
resolve-artifact-name.sh and trusted-artifact.mjs — references that exist only
inside shell, one level past anything YAML parsing sees.

The closure unit is the action's directory now. It needs no shell model and
cannot miss a file however deep the reference chain runs; the coarseness is
harmless because a file in an action's own directory belongs to that action.
All three files pinned, red against the descriptor-only closure.
@thymikee
thymikee force-pushed the ci/1781-a9-2-apple-ownership branch from a600673 to 17a762f Compare August 19, 2026 15:03
@thymikee

Copy link
Copy Markdown
Member Author

Second bypass closed. Head is 17a762f27 on base 67ce19b50 (current main#1880, #1879, #1842 came in on the way).

Which fix, and why that one. I extended the closure to every file under a composite action's directory, rather than narrowing the exemption or parsing $GITHUB_ACTION_PATH/... out of the YAML. The reason is in the reference chain you pointed at: setup-fixture-app/action.yml runs fetch-artifact.sh, and that script runs resolve-artifact-name.sh and trusted-artifact.mjs — references that appear nowhere in YAML. A fix that read $GITHUB_ACTION_PATH out of the descriptor would have caught fetch-artifact.sh and missed the other two: the same one-level-short failure, a third time. The directory is the smallest unit that terminates without modelling shell and cannot miss a file however deep the chain runs. Its coarseness costs nothing real — a file inside an action's own directory belongs to that action, so refusing to exempt it is the correct answer, not an approximation of one. lane.uses for iOS / Smoke Tests now carries 13 entries: the workflow, seven action.yml descriptors, and the five setup-fixture-app support files.

Planted witness (.github/actions/setup-fixture-app/fetch-artifact.sh added to paths-ignore, verbatim):

### PLANTED: fetch-artifact.sh added to ios.yml paths-ignore
Routed lanes whose paths-ignore disagrees with the selector:
  - ios.yml ignores .github/actions/setup-fixture-app/fetch-artifact.sh, but the selector fails
    open on it (workflow-tooling). Remove the ignore entry.

gate manifest: 1 failure(s).
### RESTORED
gate manifest: ok — 46 checks wired across 33 lanes, manual-only: replay-android, replay-ios, replay-ios-device.

Pinned as a test over all three files — the descriptor-named one and the two named only inside the shell script — and the test is non-vacuous: reverting the closure to descriptors-only fails exactly it (✖ the exemption cannot name a support file of an action the lane runs, 10 pass / 1 fail), and passes at 11/11 restored.

Is anything else path-referenced? I checked the three shapes you named, and the answer is no, for a structural reason worth stating: the exemption only ever applies to .github/** paths, so that is the entire risk surface. Within it — every local uses: across all workflows and actions resolves to a directory that has an action.yml (no bare-path uses:); the only non-action.yml files under .github/actions/** are the five setup-fixture-app scripts, now all in the closure; and the rest of .github/ is FUNDING.yml and renovate.json, which no lane reaches. Files referenced by path from run: blocks outside .github/ (e.g. scripts/**) were never exemptible in the first place — the selector fails open on them and the assertion demands the lane start, which is what the .github/actions/** and package.json plants in routing.test.ts already pin.

Merge condition, corrected. The body no longer names a fixed base: it now says to re-run pnpm check:gate-manifest against whatever main is at merge time, since the assertion reads every tracked path and every workflow, and this branch has already been overtaken three times today. The stale 7398ded24 reference is gone — the body cites the two commits by title instead, which survive rebases.

Local on this head: pnpm check:affected --run exit 0, check:gate-manifest green, gate-manifest suite 49/49, affected-selector suite 47/47, fallow clean.

@thymikee

Copy link
Copy Markdown
Member Author

Independent re-review of 17a762f: clean. The action-directory closure now covers the descriptor plus every support file of direct and transitive local actions, and the routing gate refuses any exact-name ignore in that closure. The three support-file witnesses would fail if either the closure or exemption guard regressed. Exact-head CI is green, including iOS; Size is +44 B unpacked. Merge-ready. If main moves before merge, rerun pnpm check:gate-manifest against then-current main as documented.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Aug 19, 2026
@thymikee
thymikee merged commit 393eb30 into main Aug 19, 2026
30 checks passed
@thymikee
thymikee deleted the ci/1781-a9-2-apple-ownership branch August 19, 2026 15:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-human Valid work that needs human implementation, judgment, or maintainer merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant