Files
callstack__agent-device/scripts/gate/declarations.ts
Michał Pierzchała 393eb30a28 ci: give check:affected real Apple ownership rules and route ios.yml on them (#1781 A9-2) (#1857)
* ci: give check:affected real Apple ownership rules and route ios.yml 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.

* ci: tighten routing assertion shape (fallow: unused exports, complexity)

* ci: name parked checks in check:affected --run skips

* ci: bound the routed-lane exemption to sibling workflows (review of #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.

* ci: close the routed-lane exemption over composite-action support files

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.
2026-08-19 17:35:23 +02:00

95 lines
4.8 KiB
TypeScript

// The five small facts the manifest cannot derive from package scripts and workflow YAML.
// A script whose Vitest/node-test invocation the loader cannot read, mapped to the units it
// really runs. Empty right now: the one entry was a coverage wrapper, and `test:coverage:ci`
// is a plain `vitest run --coverage` again, which the loader reads directly.
export const OPAQUE_RUNNERS: Readonly<Record<string, readonly string[]>> = {};
export const REPORTING_SCRIPTS: Readonly<Record<string, string>> = {
'test:integration:progress': [
'Prints the provider-backed integration status table and exits 0. The assertion lives in',
'its `--check` sibling, `test:integration:progress:check`, which IS the registered',
'`integration-progress` gate. Running the reporter in CI would gate nothing.',
].join(' '),
};
// A check whose lane runs, but through a surface the loader cannot read. Empty right now:
// the one entry moved to MANUAL_ONLY_OWNERS when its lane stopped running automatically.
export const UNPROVABLE_OWNERS: Readonly<Record<string, string>> = {};
/**
* A check whose only lane is a `workflow_dispatch` workflow.
*
* `lane` is the load-bearing field, not documentation: the audit resolves it against the
* derived model and fails when the named lane is gone, has become a pull_request/schedule
* lane again, or has stopped declaring the gate. Without it the record would be a plain
* allowlist, and deleting the parked job would read as "parked" forever — parked coverage
* quietly becoming deleted coverage.
*/
export type ManualOnlyOwner = {
/** Lane label, exactly as the loader builds it: `<workflow name> / <job name>`. */
readonly lane: string;
/**
* Set when the lane's own steps cannot show the gate, so the lane's existence is the whole
* attestation the model can make. Every opaque entry needs the surface named in `reason`.
*/
readonly opaque?: true;
readonly reason: string;
};
// Checks nothing runs on the way in or on a schedule — *unowned by design and temporarily*,
// the opposite of the claim UNPROVABLE_OWNERS makes. `check.ts` prints them by name on every
// run rather than folding them into the wired count, because a check that quietly loses its
// owner reads exactly like a green build. Delete the entry when the lane goes back on
// `pull_request` or `schedule`; the audit fails if an entry outlives its parking, and fails
// the other way too if a declaration is deleted while its lane is still dispatch-only.
export const MANUAL_ONLY_OWNERS: Readonly<Record<string, ManualOnlyOwner>> = {
'replay-android': {
lane: 'Replay Manual / Android Full Emulator Suite',
opaque: true,
reason: [
'Parked on `workflow_dispatch` by #1781 A1. The lane runs `pnpm gate replay-android`',
'inside the `script:` input of `reactivecircus/android-emulator-runner` — shell handed',
'to a third-party action, which this loader does not read — so the job existing is all',
'the model can attest, and the gate would be invisible even back on a schedule. Routing',
'the emulator lane through steps the loader opens is the open item named in #1429.',
].join(' '),
},
'replay-ios': {
lane: 'Replay Manual / iOS Replay Suite',
reason: [
'Parked on `workflow_dispatch` by #1781 A1 after the suite failed every scheduled run',
'from 2026-07-24 on.',
].join(' '),
},
'replay-ios-device': {
lane: 'Replay Manual / iOS Replay Suite',
reason: [
'Parked on `workflow_dispatch` by #1781 A1 with the simulator suite it shares a job',
'with; the step is additionally skipped unless the `IOS_UDID` repository variable is set.',
].join(' '),
},
};
/**
* A `pull_request` lane whose `paths-ignore` list is routing, not hygiene (#1781 A9-2): the
* list is asserted against the affected selector over every tracked path, both ways. A path
* the selector fails open on, or routes to one of the lane's declared gates or to `sampled`
* (checks the lane runs a slice of through raw shell, invisible to the loader), must start the
* lane; a path the selector places on another family's device-lane surface, or classifies as
* a unit test, must not. GitHub evaluates the list before a runner is allocated, so the
* decision costs no macOS time and adds no job to the critical path.
*/
export type RoutedLane = {
/** Lane label, exactly as the loader builds it: `<workflow name> / <job name>`. */
readonly lane: string;
/** Checks the lane samples without declaring, in addition to its run-gate declarations. */
readonly sampled: readonly string[];
};
export const ROUTED_LANES: readonly RoutedLane[] = [
// ios.yml declares swift-runner-ios; its raw-shell steps run one iOS simulator replay and, when
// IOS_UDID is set, one physical-device replay — a slice of the parked replay-ios lanes.
{ lane: 'iOS / Smoke Tests', sampled: ['replay-ios', 'replay-ios-device'] },
];