@patronage/factory-ci 1.0.0-alpha.23 → 1.0.0-alpha.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -61,22 +61,50 @@ workflow({/* caller-owned jobs and topology */}).writeOrLint({
61
61
  });
62
62
  ```
63
63
 
64
- The **runner is not this package's business**. Jobs, runners, permissions, workflow topology, and deploy policy remain with the caller. The returned values are plain structural objects; this package does not depend on gagen.
65
-
66
- Candidate lifecycle support follows the same boundary. Use `FACTORY_CANDIDATE_PULL_REQUEST_TYPES` for the pull-request trigger matrix and `factoryCandidateOrPushCondition()` on each substantive job. With an optional caller-owned path condition, the helper emits the GitHub expression that runs merge-target pushes unconditionally and runs pull-request work only when GitHub's draft boolean says the pull request is a Candidate:
64
+ `factoryWorkflow()` still does not set `runsOn`. This package does not depend on gagen. Job ids, permissions, topology, and deploy policy stay with the caller. Verify runner kinds are this package's business. Name each job's kind (or an override with a non-empty why), pass `.label` into gagen `job()`, and hand the same table to `assertVerifyRunnerPolicy` next to `assertWorkflowShellParses`:
67
65
 
68
66
  ```ts
67
+ import {
68
+ assertVerifyRunnerPolicy,
69
+ assertWorkflowShellParses,
70
+ factoryVerifyRunner,
71
+ type FactoryVerifyRunner,
72
+ } from "@patronage/factory-ci";
73
+
74
+ type VerifyJobId = "changes" | "core";
75
+
76
+ const verifyRunners: Record<VerifyJobId, FactoryVerifyRunner> = {
77
+ changes: factoryVerifyRunner("rounding-tax"),
78
+ core: factoryVerifyRunner("sustained-compute"),
79
+ };
80
+
81
+ const runsOnFor = (id: VerifyJobId) => verifyRunners[id].label;
82
+
69
83
  const changes = job("changes", {
70
84
  if: factoryCandidateOrPushCondition(),
71
- // caller-owned runner, permissions, and steps
85
+ runsOn: runsOnFor("changes"),
72
86
  });
73
87
 
74
88
  const core = job("core", {
75
89
  if: factoryCandidateOrPushCondition("needs.changes.outputs.core == 'true'"),
76
- // caller-owned topology
90
+ runsOn: runsOnFor("core"),
91
+ });
92
+
93
+ const generated = workflow({ jobs: [changes, core] });
94
+
95
+ assertWorkflowShellParses(generated.toYamlString(), {
96
+ source: ".github/workflows/verify.ts",
97
+ });
98
+ assertVerifyRunnerPolicy(generated.toYamlString(), {
99
+ source: ".github/workflows/verify.ts",
100
+ jobs: verifyRunners,
77
101
  });
78
102
  ```
79
103
 
104
+ `sustained-compute` and `rounding-tax` both resolve to `depot-ubuntu-24.04`. They stay distinct so a short billed-rounding job cannot be confused with a long compute job in the table. `app-token` resolves to `ubuntu-latest`. `deploy-credentials` also resolves to `ubuntu-latest` and is refused inside `assertVerifyRunnerPolicy`, because a Verify workflow has no Cloudflare production secret. HQ deploy keeps a literal `ubuntu-latest` and never calls this assert.
105
+
106
+ Candidate lifecycle support follows the same boundary. Use `FACTORY_CANDIDATE_PULL_REQUEST_TYPES` for the pull-request trigger matrix and `factoryCandidateOrPushCondition()` on each substantive job. With an optional caller-owned path condition, the helper emits the GitHub expression that runs merge-target pushes unconditionally and runs pull-request work only when GitHub's draft boolean says the pull request is a Candidate. The runner label still comes from the kind table above.
107
+
80
108
  Migration: upgrade `@patronage/factory-ci`, apply the shared trigger types and job condition in the TypeScript workflow source, then regenerate and commit the emitted YAML. Draft `opened` and `synchronize` events will stop running substantive Verify work. Promotion through `ready_for_review`, later non-draft Candidate events, and merge-target pushes continue to run their applicable battery. A skipped draft run is presentation, not proof.
81
109
 
82
110
  Production impact support follows the same ownership line. A generated deploy workflow declares only its target names and consumes the returned decision step and fail-open `demandedIf(target, decisionJob)` expressions:
@@ -257,7 +285,10 @@ The audit job is `always()` so a failed destroy still runs the stage-wide check.
257
285
  ### Merge-freeze writer job
258
286
 
259
287
  ```ts
260
- import { factoryMergeFreezeJob } from "@patronage/factory-ci";
288
+ import {
289
+ factoryMergeFreezeJob,
290
+ factoryVerifyRunner,
291
+ } from "@patronage/factory-ci";
261
292
 
262
293
  const freeze = factoryMergeFreezeJob({
263
294
  createGithubAppToken: workflowArtifact.actions.createGithubAppToken,
@@ -272,7 +303,7 @@ const jobs = [
272
303
  name: freeze.jobName,
273
304
  needs: freeze.needs,
274
305
  if: freeze.if,
275
- runsOn: "ubuntu-latest",
306
+ runsOn: factoryVerifyRunner("app-token").label,
276
307
  permissions: freeze.permissions,
277
308
  timeoutMinutes: 10,
278
309
  steps: freeze.steps,
@@ -282,7 +313,7 @@ const jobs = [
282
313
 
283
314
  A consumer's generated merge-target-push Verify workflow is the only producer of `patronage-factory/merge-freeze` generations (#356, ADR 0016 as amended; #429, #872). A red merge-target Verify completes a generation active, a green one completes it inactive, and `pr:ready` reads the generation on the candidate's own base tip. No command, scheduled job, or second workflow writes that check run; the operator override (`demand:waive --demand merge-freeze`) waives the demand for one candidate and never writes here.
284
315
 
285
- `factoryMergeFreezeJob` owns everything a reader trusts: the check name (`FACTORY_MERGE_FREEZE_CHECK_NAME`), the pinned Patronage Factory App identity, the `actions/create-github-app-token` inputs, the `needs.*.result` fold (`FACTORY_MERGE_FREEZE_VERIFY_RESULT_EXPRESSION`), the merge-target condition (`FACTORY_MERGE_FREEZE_IF`, built from `FACTORY_MERGE_TARGET_REF_CONDITION`), and the job's least-privilege `permissions` (an empty block: the App installation token carries the check-run write, the job uses no `GITHUB_TOKEN` scope, and it never checks out the repository). The caller owns the runner, the timeout, and the `needs` list.
316
+ `factoryMergeFreezeJob` owns everything a reader trusts: the check name (`FACTORY_MERGE_FREEZE_CHECK_NAME`), the pinned Patronage Factory App identity, the `actions/create-github-app-token` inputs, the `needs.*.result` fold (`FACTORY_MERGE_FREEZE_VERIFY_RESULT_EXPRESSION`), the merge-target condition (`FACTORY_MERGE_FREEZE_IF`, built from `FACTORY_MERGE_TARGET_REF_CONDITION`), and the job's least-privilege `permissions` (an empty block: the App installation token carries the check-run write, the job uses no `GITHUB_TOKEN` scope, and it never checks out the repository). The caller owns the runner kind (`app-token` for the Factory App token), the timeout, and the `needs` list.
286
317
 
287
318
  That `needs` list is the whole trust input: it must name **every authoritative verification leaf**, because the fold is what decides active or inactive. An empty list is refused — a fold over no needed job always reports success, which would report a merge target nothing verified as green.
288
319
 
@@ -332,10 +363,10 @@ const coreCommands = profile.verification.commands.filter(
332
363
  );
333
364
 
334
365
  const core = job("core", {
335
- // `pull-requests: read` serves the gate's default-branch merge fallback
336
- // (#611). Without it the fallback's PR lookup fails and every merge push
337
- // silently pays the full suite.
338
- permissions: { checks: "read", contents: "read", "pull-requests": "read" },
366
+ // `checks: read` is all the default PR-only gate needs. Add
367
+ // `pull-requests: read` only with `reuse: "pull-request-and-default-branch"`,
368
+ // where the merge fallback (#611) looks up the producing pull request.
369
+ permissions: { checks: "read", contents: "read" },
339
370
  steps: [
340
371
  step(factoryProofGateStep({ commands: coreCommands, surface: "core" })),
341
372
  step(factoryProofTimingStartStep()),
@@ -347,9 +378,11 @@ const core = job("core", {
347
378
 
348
379
  The **proof-reuse gate** decides whether a hosted job may reuse the local verification the factory already published for this exact head (ADR 0022). Refusing runs hosted CI; it never fails the candidate. The App identity, check name, step id, output names, guard, and every trust predicate are fixed here rather than consumer-configurable — three repositories had grown three answers to the same question and had already drifted.
349
380
 
350
- The gate runs on pull requests and on pushes to the default branch (#611). A push run first reads proof at the pushed head. A squash merge mints a new commit, so when that direct read finds no generation at all, a **merge fallback** looks up the single merged pull request whose merge commit is the pushed head and reuses that PR head's proven check run only when the merge commit's tree id equals the proven head's tree id, which makes the pushed content byte-identical to what was verified (a clean squash of an unchanged tip). Patch identity was considered and rejected as the comparator: `git patch-id` normalizes whitespace and ignores base motion, so an identical patch can still integrate into a tree nothing verified. No unique producing PR, an unreadable commit, a proof that is anything but proven, or tree drift from a dirty or stale merge all leave the direct refusal standing and run the full suite. The fallback needs `pull-requests: read` and `contents: read` on the job in addition to `checks: read`; a job that grants less loses only the fallback.
381
+ **The gate runs on pull requests only by default** (#863). A merge target's guarantee is about the whole tree, not the diff, and a candidate proof binds only the diff that candidate was scoped to. Every default-branch push therefore executes the full hosted suite, and the job needs only `checks: read`. #931 is the defect this default answers: a `main` push reused an impact-scoped candidate proof, executed no command, and left a red package unnoticed for three days.
382
+
383
+ `reuse` chooses which events may reuse proof (#873). It takes two values and nothing else, because the emitted condition is a trust predicate: a free-text override lets a consumer reuse proof on a ref nothing proved. `"pull-request"` is the default described above. `"pull-request-and-default-branch"` is the opt-out: it adds pushes to the default branch, and the consumer accepts that an impact-scoped candidate proof may gate a whole tree. Choose it when merge-target runner time matters more than that guarantee.
351
384
 
352
- `reuse` chooses which events may reuse proof (#873). It takes two values and nothing else, because the emitted condition is a trust predicate: a free-text override lets a consumer reuse proof on a ref nothing proved. `"pull-request-and-default-branch"` is the default and the behaviour above. `"pull-request"` emits `github.event_name == 'pull_request'` alone and omits the merge fallback, so every default-branch push executes the full hosted suite and the job needs only `checks: read`. Choose it when the merged commit must be verified in its own right; the consumer then owns that unconditional default-branch cost.
385
+ Under the opt-out, a push run first reads proof at the pushed head. A squash merge mints a new commit, so when that direct read finds no generation at all, a **merge fallback** looks up the single merged pull request whose merge commit is the pushed head and reuses that PR head's proven check run — only when the merge commit's tree id equals the proven head's tree id, which makes the pushed content byte-identical to what was verified (a clean squash of an unchanged tip). Patch identity was considered and rejected as the comparator: `git patch-id` normalizes whitespace and ignores base motion, so an identical patch can still integrate into a tree nothing verified. No unique producing PR, an unreadable commit, a proof that is anything but proven, or tree drift from a dirty or stale merge all leave the direct refusal standing and run the full suite. The fallback needs `pull-requests: read` and `contents: read` on the job in addition to `checks: read`; a job that grants less loses only the fallback.
353
386
 
354
387
  A proof is reusable only when the complete Checks API result (`filter=all`, every page) establishes one unambiguous newest generation by greatest `started_at`, produced by the pinned App for the exact repository and head, completed successfully with `outcome: passed`, and covering every command identity the guarded surface requires. Coverage is the union of `executedCommands` and commands released by `notRequiredCommands` only after the gate validates each release against that same proof binding's command-to-target map and identity-bound impact stamp. Missing, malformed, duplicated, affected, unknown, or differently bound release data refuses reuse and runs the hosted surface. `mode` is reported as diagnostic metadata, never authorized on: a reduced-mode proof whose executed and stamp-authorized released commands cover a surface is reusable.
355
388
 
@@ -450,9 +483,9 @@ The factory publishes its check runs, and paitronage its proof comments, under t
450
483
 
451
484
  `mintInstallationToken({ credentials, owner, repo }, options?)` is that mechanism and nothing more. `credentials` is `{ appId, installationId?, privateKeyPath }`: the app id GitHub issued, the installation when a consumer has recorded one, and the path to the private key. The key is a _path_, not key material, so no caller has to hold a secret in memory to make this call — and **where that path comes from stays the consumer's**. Operator config, a secret manager, an environment variable: this package neither reads a config file nor knows a key-path convention. `githubAppJwt(credentials, options?)` is the signing step alone, for a caller that needs the app JWT rather than an installation token.
452
485
 
453
- Nothing is cached. The token is returned to the caller, which owns its lifetime — this module keeps no copy of the token or the key, and never writes either to any output. A failing GitHub response throws `GitHubApiError`, which carries `status` so a caller can tell a retryable failure (422, 429, 5xx) from a wrong-credentials one.
486
+ Nothing is cached. The token is returned to the caller, which owns its lifetime — this module keeps no copy of the token or the key, and never writes either to any output. Mint requests send `Connection: close` and retry transport failures — fetch rejection, timeout, a body that dies mid-read, or 408/429/5xx — with doubling backoff for three attempts by default. Exhaustion throws `GitHubAppTokenTransportError` with the attempt count and a bounded credential-free reason. Any other 4xx is GitHub's verdict and throws `GitHubApiError` on the first response without retrying. The budget is the caller's: `transportAttempts` sets the total attempts, and a caller whose mint is best-effort and already has a fallback passes `1` so the fallback stays prompt. The backoff is fixed and does not scale with `timeoutMs`.
454
487
 
455
- `options` are all injectable seams: `fetch`, `now`, `readPrivateKey`, and a `timeoutMs` per request (five seconds by default). Tests substitute the first three; production passes at most a timeout.
488
+ `options` are all injectable seams: `fetch`, `now`, `readPrivateKey`, `transportAttempts`, and a `timeoutMs` per request (five seconds by default). Tests substitute the seams; production passes at most a timeout and a budget.
456
489
 
457
490
  ### Vitest suite profiling
458
491
 
@@ -0,0 +1,3 @@
1
+ {
2
+ "fingerprint": "43a41eecd333f5075b92a78d7d29e9d5e6b723acea5c6bb063e5bb952bf5bf9d"
3
+ }
package/dist/index.d.ts CHANGED
@@ -471,6 +471,20 @@ interface FactoryWorkflowArtifact<Additional extends Readonly<Record<string, Pin
471
471
  */
472
472
  declare const factoryWorkflow: <const Additional extends Readonly<Record<string, PinnedAction>> = Record<never, never>>(options: FactoryWorkflowOptions<Additional>) => FactoryWorkflowArtifact<Additional>;
473
473
  //#endregion
474
+ //#region src/github-transport.d.ts
475
+ /**
476
+ * Bounded transport retries could not reach the GitHub App endpoint (#923).
477
+ * The reason is credential-free, so it is safe to log. Re-exported from
478
+ * `github-app-token.ts`, which owns the mint surface.
479
+ */
480
+ declare class GitHubAppTokenTransportError extends Error {
481
+ /** How many transport attempts were made before giving up. */
482
+ readonly attempts: number;
483
+ /** The last transient HTTP status, when the server answered at all. */
484
+ readonly status?: number;
485
+ constructor(reason: string, attempts: number, status?: number);
486
+ }
487
+ //#endregion
474
488
  //#region src/github-app-token.d.ts
475
489
  /**
476
490
  * What a consumer must know to mint: the app id, where the private key is, and
@@ -500,6 +514,15 @@ interface GithubAppTokenOptions {
500
514
  readPrivateKey?: (privateKeyPath: string) => Buffer | string;
501
515
  /** Per-request timeout; defaults to five seconds. */
502
516
  timeoutMs?: number;
517
+ /**
518
+ * Total transport attempts per request, the first one included. Defaults to
519
+ * three: a mint that must survive a stale keep-alive socket (#923). A caller
520
+ * whose mint is best-effort and already has a fallback passes 1, because
521
+ * retrying against a deliberately short timeout only delays that fallback
522
+ * (#938). The backoff between attempts is fixed and does not scale with
523
+ * `timeoutMs`; the two dials are unrelated.
524
+ */
525
+ transportAttempts?: number;
503
526
  }
504
527
  /**
505
528
  * The signed app JWT GitHub accepts as `Authorization: Bearer` for the App
@@ -595,10 +618,13 @@ declare const executeAlchemyEntry: (options: ExecuteAlchemyEntryOptions) => Prom
595
618
  *
596
619
  * It answers one question, before checkout and before install: may this job
597
620
  * reuse the local verification the factory already published for *this exact
598
- * head*? On a push to the merge target one fallback extends "this exact head"
599
- * to "this exact tree": a squash merge whose result tree is byte-identical to
600
- * the producing pull request's proven head tree reuses that head's proof
601
- * (#611). Refusing runs hosted CI; it never fails the candidate. Three
621
+ * head*? By default it asks that only on a pull request, so every
622
+ * default-branch push executes the full hosted suite (#863). A consumer may
623
+ * opt into `reuse: "pull-request-and-default-branch"`, where one fallback
624
+ * extends "this exact head" to "this exact tree": a squash merge whose result
625
+ * tree is byte-identical to the producing pull request's proven head tree
626
+ * reuses that head's proof (#611). Refusing runs hosted CI; it never fails
627
+ * the candidate. Three
602
628
  * repositories had independently grown their own answer to that question and
603
629
  * had already drifted apart, so the answer lives here once, with the App
604
630
  * identity, check name, step identity, output names, guard, and every trust
@@ -676,18 +702,25 @@ declare const FACTORY_PROOF_GATE_SOURCE_URL_OUTPUT = "source-check-url";
676
702
  */
677
703
  declare const FACTORY_PROOF_GATE_GUARD = "steps.factory-proof.outputs.reuse-proof != 'true'";
678
704
  /**
679
- * Pull requests, plus pushes to the merge target (#611).
705
+ * Pull requests, plus pushes to the merge target (#611) — the **opt-out**
706
+ * condition a consumer gets with `reuse: "pull-request-and-default-branch"`.
707
+ *
708
+ * It buys runner time on the merge target and pays for it in coverage. No
709
+ * local proof is ever written for a merged SHA, so the direct read finds
710
+ * nothing after a squash merge. The push path therefore exists only together
711
+ * with the merge fallback below: when no generation exists at the merged SHA,
712
+ * the gate may look up the producing pull request and reuse its head proof,
713
+ * but only when the merge result's tree is exactly the proven head's tree.
714
+ * Deploy gates keep waiting for a green required check at the merged SHA; a
715
+ * reused proof turns that check green through the same guarded steps a pull
716
+ * request uses.
680
717
  *
681
- * The gate was pull-request-only through 1.0.0-alpha.15: a push to a
682
- * protected branch always ran the full suite, because no local proof is ever
683
- * written for the merged SHA. That still holds for the *direct* read a
684
- * squash merge mints a new commit which is why the push path exists only
685
- * together with the merge fallback below: when no generation exists at the
686
- * merged SHA, the gate may look up the producing pull request and reuse its
687
- * head proof, but only when the merge result's tree is exactly the proven
688
- * head's tree. Deploy gates keep
689
- * waiting for a green required check at the merged SHA; a reused proof turns
690
- * that check green through the same guarded steps a pull request uses.
718
+ * The trade this mode accepts: a candidate's proof may be impact-scoped, and
719
+ * an impact-scoped proof is a sound gate for the candidate and an unsound
720
+ * gate for the merge target. #931 is that defect observed a `main` push
721
+ * reused a candidate proof, executed no command, and left a red package
722
+ * unnoticed for three days. Choose this mode only when the consumer accepts
723
+ * that risk for speed.
691
724
  *
692
725
  * The push side is deliberately the merge target only — the repository's
693
726
  * default branch — not every pushed branch. Which branches trigger a
@@ -697,16 +730,16 @@ declare const FACTORY_PROOF_GATE_GUARD = "steps.factory-proof.outputs.reuse-proo
697
730
  */
698
731
  declare const FACTORY_PROOF_GATE_IF = "github.event_name == 'pull_request' || (github.event_name == 'push' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch))";
699
732
  /**
700
- * Pull requests only — the condition a consumer gets with
701
- * `reuse: "pull-request"` (#873).
733
+ * Pull requests only — the **default** condition, and the condition a
734
+ * consumer gets with `reuse: "pull-request"` (#873, default since #863).
702
735
  *
703
- * A repository may want its default-branch pushes to always execute the full
704
- * hosted suite, whatever proof exists: the merged commit is what the fleet
705
- * deploys, so a periodic unconditional run of every command is a deliberate
706
- * cost some consumers choose to pay. This condition is that choice, expressed
707
- * once here rather than as a consumer-written override string. The mode also
708
- * skips the push-event merge fallback, so no push path can reuse proof even if
709
- * the workflow reaches the step through some other trigger.
736
+ * A merge target's guarantee is about the whole tree, not the diff. A
737
+ * candidate proof only ever binds the diff the candidate was scoped to, so
738
+ * reusing it on the merged commit proves less than the merge target needs.
739
+ * Every default-branch push therefore executes the full hosted suite,
740
+ * whatever proof exists. The mode also skips the push-event merge fallback,
741
+ * so no push path can reuse proof even if the workflow reaches the step
742
+ * through some other trigger.
710
743
  *
711
744
  * Which branches trigger the workflow at all stays repository-owned. This
712
745
  * condition only keeps the gate from consulting proof outside a pull request.
@@ -720,10 +753,12 @@ declare const FACTORY_PROOF_GATE_PULL_REQUEST_IF = "github.event_name == 'pull_r
720
753
  * unproven ref — and the emitted condition is a trust predicate. The consumer
721
754
  * chooses the mode; `factory-ci` owns what each mode emits.
722
755
  *
723
- * - `pull-request-and-default-branch` (default) pull requests plus pushes to
724
- * the merge target, with the merge fallback. Today's behaviour.
725
- * - `pull-request` pull requests only. Default-branch pushes execute the full
726
- * hosted suite, and the merge fallback is not emitted.
756
+ * - `pull-request` (default) pull requests only. Default-branch pushes
757
+ * execute the full hosted suite, and the merge fallback is not emitted.
758
+ * - `pull-request-and-default-branch` the opt-out: pull requests plus pushes
759
+ * to the merge target, with the merge fallback. It is faster on the merge
760
+ * target and it lets an impact-scoped candidate proof gate a whole tree
761
+ * (#931), so a consumer must ask for it.
727
762
  */
728
763
  type FactoryProofGateReuse = "pull-request" | "pull-request-and-default-branch";
729
764
  /**
@@ -810,10 +845,10 @@ interface FactoryProofGateOptions {
810
845
  */
811
846
  readonly commands: readonly ProofReuseCommand[];
812
847
  /**
813
- * Which events may reuse proof. Defaults to
814
- * `"pull-request-and-default-branch"`, the behaviour every consumer has
815
- * today. `"pull-request"` makes default-branch pushes execute the full
816
- * hosted suite (#873).
848
+ * Which events may reuse proof. Defaults to `"pull-request"`: every
849
+ * default-branch push executes the full hosted suite (#873, #863).
850
+ * `"pull-request-and-default-branch"` is the opt-out a consumer chooses for
851
+ * speed on the merge target, and it accepts #931's risk.
817
852
  */
818
853
  readonly reuse?: FactoryProofGateReuse;
819
854
  /** Names the suite in the job summary. Changes no trust decision. */
@@ -852,13 +887,14 @@ interface FactoryProofGateStep {
852
887
  * The step itself, structurally accepted by gagen's `step()` without adding a
853
888
  * gagen runtime dependency. It belongs first in the job it guards: a few API
854
889
  * reads with the default `GITHUB_TOKEN`, no checkout, no install, so a proven
855
- * head costs a runner nothing beyond job startup. A pull request needs
856
- * `checks: read`; the push-event merge fallback additionally reads the
857
- * producing pull request (`pull-requests: read`) and the two commit objects
858
- * whose tree ids it compares (`contents: read`). A job that grants less
859
- * loses only the fallback the failed read degrades to the full suite. A
860
- * consumer that passes `reuse: "pull-request"` emits neither the push clause
861
- * nor the fallback, and needs only `checks: read` (#873).
890
+ * head costs a runner nothing beyond job startup. The default
891
+ * `reuse: "pull-request"` emits neither the push clause nor the fallback, and
892
+ * needs only `checks: read` (#873, #863). A consumer that opts into
893
+ * `reuse: "pull-request-and-default-branch"` also runs the push-event merge
894
+ * fallback, which reads the producing pull request (`pull-requests: read`)
895
+ * and the two commit objects whose tree ids it compares (`contents: read`).
896
+ * A job that grants less loses only the fallback the failed read degrades
897
+ * to the full suite.
862
898
  *
863
899
  * A **step, not a job**, and that is not a style preference. A separate gate
864
900
  * job that errored would leave the guarded job `skipped`, and a summary job
@@ -1541,6 +1577,53 @@ type VitestProfileReadResult = {
1541
1577
  */
1542
1578
  declare const readVitestProfileDocument: (value: unknown) => VitestProfileReadResult;
1543
1579
  //#endregion
1580
+ //#region src/verify-runner-policy.d.ts
1581
+ declare const DEPOT_LABEL = "depot-ubuntu-24.04";
1582
+ declare const HOSTED_LABEL = "ubuntu-latest";
1583
+ /**
1584
+ * Why a job runs where it does. The two Depot kinds share a label. They stay
1585
+ * distinct so billed-rounding jobs and long compute jobs cannot be confused
1586
+ * in the declaration table.
1587
+ *
1588
+ * `deploy-credentials` is legal to construct (HQ deploy).
1589
+ * `assertVerifyRunnerPolicy` refuses it, because a Verify workflow has no
1590
+ * Cloudflare production secret.
1591
+ */
1592
+ type FactoryRunnerKind = "sustained-compute" | "rounding-tax" | "app-token" | "deploy-credentials";
1593
+ type FactoryRunnerHome = "depot" | "hosted";
1594
+ interface FactoryRunnerOverride {
1595
+ readonly kind: "override";
1596
+ readonly home: FactoryRunnerHome;
1597
+ readonly why: string;
1598
+ }
1599
+ type FactoryRunnerDecision = FactoryRunnerKind | FactoryRunnerOverride;
1600
+ interface FactoryVerifyRunner {
1601
+ readonly decision: FactoryRunnerDecision;
1602
+ readonly home: FactoryRunnerHome;
1603
+ readonly label: typeof DEPOT_LABEL | typeof HOSTED_LABEL;
1604
+ readonly why?: string;
1605
+ }
1606
+ type FactoryVerifyRunnerTable = Readonly<Record<string, FactoryVerifyRunner>>;
1607
+ interface FactoryVerifyRunnerPolicyInput {
1608
+ readonly source: string;
1609
+ readonly jobs: FactoryVerifyRunnerTable;
1610
+ }
1611
+ interface FactoryVerifyRunnerMismatch {
1612
+ readonly jobId: string;
1613
+ readonly expected: string;
1614
+ readonly actual: string;
1615
+ }
1616
+ interface FactoryVerifyRunnerPolicyReport {
1617
+ readonly ok: boolean;
1618
+ readonly undeclared: readonly string[];
1619
+ readonly missing: readonly string[];
1620
+ readonly mismatches: readonly FactoryVerifyRunnerMismatch[];
1621
+ readonly forbiddenKinds: readonly string[];
1622
+ }
1623
+ declare const factoryVerifyRunner: (decision: FactoryRunnerDecision) => FactoryVerifyRunner;
1624
+ declare const verifyRunnerPolicy: (yaml: string, input: FactoryVerifyRunnerPolicyInput) => FactoryVerifyRunnerPolicyReport;
1625
+ declare const assertVerifyRunnerPolicy: (yaml: string, input: FactoryVerifyRunnerPolicyInput) => FactoryVerifyRunnerPolicyReport;
1626
+ //#endregion
1544
1627
  //#region src/workflow-shell-lint.d.ts
1545
1628
  /**
1546
1629
  * Parse-check the shell embedded in generated workflow YAML (#376).
@@ -1592,4 +1675,4 @@ declare const assertWorkflowShellParses: (yaml: string, options: {
1592
1675
  readonly source: string;
1593
1676
  }) => void;
1594
1677
  //#endregion
1595
- export { type BundleAlchemyEntryOptions, type CheckoutStepOptions, EXECUTE_ALCHEMY_ENTRY_MAX_BUFFER, type ExecuteAlchemyEntryOptions, type ExecuteAlchemyEntryResult, FACTORY_CANDIDATE_IMPACT_BASIS_OUTPUT, FACTORY_CANDIDATE_IMPACT_DECISION_OUTPUT, FACTORY_CANDIDATE_IMPACT_INERT_OUTPUT, FACTORY_CANDIDATE_IMPACT_STEP_ID, FACTORY_CANDIDATE_IMPACT_UNSUBSCRIBED_OUTPUT, FACTORY_CANDIDATE_PULL_REQUEST_TYPES, FACTORY_LIFECYCLE_CONTRACT_JOB_ID, FACTORY_LIFECYCLE_CONTRACT_JOB_NAME, FACTORY_MERGE_FREEZE_APP_TOKEN_STEP_ID, FACTORY_MERGE_FREEZE_CHECK_NAME, FACTORY_MERGE_FREEZE_IF, FACTORY_MERGE_FREEZE_JOB_ID, FACTORY_MERGE_FREEZE_JOB_NAME, FACTORY_MERGE_FREEZE_PERMISSIONS, FACTORY_MERGE_FREEZE_VERIFY_RESULT_EXPRESSION, FACTORY_MERGE_TARGET_REF_CONDITION, FACTORY_PREVIEW_CLEANUP_AUDIT_JOB_ID, FACTORY_PREVIEW_CLEANUP_AUDIT_JOB_NAME, FACTORY_PRODUCTION_IMPACT_BASIS_OUTPUT, FACTORY_PRODUCTION_IMPACT_DECISION_OUTPUT, FACTORY_PRODUCTION_IMPACT_STEP_ID, FACTORY_PRODUCTION_IMPACT_UNSUBSCRIBED_OUTPUT, FACTORY_PROOF_GATE_APP_ID, FACTORY_PROOF_GATE_CHECK_NAME, FACTORY_PROOF_GATE_GUARD, FACTORY_PROOF_GATE_IF, FACTORY_PROOF_GATE_MODE_OUTPUT, FACTORY_PROOF_GATE_OUTPUT, FACTORY_PROOF_GATE_PULL_REQUEST_IF, FACTORY_PROOF_GATE_REASONS, FACTORY_PROOF_GATE_REASON_OUTPUT, FACTORY_PROOF_GATE_SHELL, FACTORY_PROOF_GATE_SOURCE_URL_OUTPUT, FACTORY_PROOF_GATE_STEP_ID, FACTORY_PROOF_GATE_STEP_NAME, FACTORY_PR_STATUS_HUD_APP_TOKEN_STEP_ID, FACTORY_PR_STATUS_HUD_CONCURRENCY, FACTORY_PR_STATUS_HUD_IF, FACTORY_PR_STATUS_HUD_JOB_ID, FACTORY_PR_STATUS_HUD_JOB_NAME, FACTORY_PR_STATUS_HUD_PERMISSIONS, FACTORY_PR_STATUS_HUD_PLAN_PATH, FACTORY_PUSH_IDENTITY_SCHEMA_VERSION, type FactoryCandidateImpactWorkflow, type FactoryCandidateImpactWorkflowOptions, type FactoryLifecycleContractLane, type FactoryLifecycleContractLaneOptions, type FactoryMergeFreezeJob, type FactoryMergeFreezeJobOptions, type FactoryPrStatusHudWorkflow, type FactoryPrStatusHudWorkflowOptions, type FactoryPreviewCleanupAuditJob, type FactoryPreviewCleanupDestroyJob, type FactoryPreviewCleanupTopology, type FactoryPreviewCleanupTopologyOptions, type FactoryProductionImpactWorkflow, type FactoryProductionImpactWorkflowOptions, type FactoryProofGateOptions, type FactoryProofGateReason, type FactoryProofGateReuse, type FactoryProofGateStep, type FactoryProofReusePresentationOptions, type FactoryProofReuseSummaryStep, type FactoryProofTimingStartStep, type FactoryPushIdentityConsumer, type FactoryPushIdentityConsumerOptions, type FactoryPushIdentityDisposition, type FactoryPushIdentityEnvelope, type FactoryPushIdentityProducer, type FactoryPushIdentityProducerOptions, type FactoryWorkflowArtifact, type FactoryWorkflowOptions, type FactoryWorkflowSetupOptions, GitHubApiError, type GithubAppCredentials, type GithubAppTokenOptions, type InstallStepOptions, type LocalPreviewStage, type LocalPreviewStageOptions, NODE_PNPM_ACTION_FAMILY_NODE24, type NodePnpmActionFamily, PREVIEW_PROOF_INVENTORY_CHECK_NAME, PREVIEW_PROOF_INVENTORY_LIST_PERMISSIONS, type ParseLocalPreviewStageExpected, type ParsedLocalPreviewStage, type PinnedAction, type PreviewProofCandidate, type PreviewProofCleanupOutcome, type PreviewProofCleanupStatus, type PreviewProofEnvelopeEvidence, type PreviewProofInventoryAccess, type PreviewProofInventoryCleanupInput, type PreviewProofInventoryListInput, type PreviewProofInventoryPersistInput, type PreviewProofInventoryStore, type PreviewProofInventoryTransport, type PreviewProofLifecycleEvent, type PreviewProofLifecycleState, type PreviewProofRegistration, type PreviewProofResolveQuery, PreviewProofTransportError, type ProofReuseCommand, type ProofReuseCoverageInput, type ProofReuseCoverageReport, type SetupNodeStepOptions, UPLOAD_ARTIFACT, VITEST_PROFILE_SCHEMA_VERSION, VITEST_PROFILE_TOOL, type VitestJsonReport, type VitestProfile, type VitestProfileDependencies, type VitestProfileDurationSummary, type VitestProfileEnvironment, VitestProfileError, type VitestProfileOptions, type VitestProfileReadResult, type VitestProfileSample, type VitestProfileSampleExecution, type VitestTestStatus, type WorkflowShellParseFailure, type WorkflowStep, assertProofReuseCoverage, assertWorkflowShellParses, bundleAlchemyEntry, captureVitestProfileEnvironment, executeAlchemyEntry, factoryCandidateImpactWorkflow, factoryCandidateOrPushCondition, factoryLifecycleContractLane, factoryMergeFreezeJob, factoryMergeFreezeScript, factoryPrStatusHudWorkflow, factoryPreviewCleanupTopology, factoryProductionImpactWorkflow, factoryProofGateScript, factoryProofGateStep, factoryProofReuseSummaryStep, factoryProofTimingStartStep, factoryPushIdentityConsumer, factoryPushIdentityProducer, factoryWorkflow, githubAppJwt, isLocalPreviewStage, localPreviewStage, mintInstallationToken, normalizeVitestProfileSample, parseLocalPreviewStage, previewCleanupDestroyJobId, previewProofInventory, previewProofLifecycle, productionImpactTargetOutput, proofReuseCoverage, proofReuseRequiredCommands, readVitestProfileDocument, resolveProofReuseCommands, runVitestProfile, workflowRunBlocks, workflowShellParseFailures, writeVitestProfile };
1678
+ export { type BundleAlchemyEntryOptions, type CheckoutStepOptions, EXECUTE_ALCHEMY_ENTRY_MAX_BUFFER, type ExecuteAlchemyEntryOptions, type ExecuteAlchemyEntryResult, FACTORY_CANDIDATE_IMPACT_BASIS_OUTPUT, FACTORY_CANDIDATE_IMPACT_DECISION_OUTPUT, FACTORY_CANDIDATE_IMPACT_INERT_OUTPUT, FACTORY_CANDIDATE_IMPACT_STEP_ID, FACTORY_CANDIDATE_IMPACT_UNSUBSCRIBED_OUTPUT, FACTORY_CANDIDATE_PULL_REQUEST_TYPES, FACTORY_LIFECYCLE_CONTRACT_JOB_ID, FACTORY_LIFECYCLE_CONTRACT_JOB_NAME, FACTORY_MERGE_FREEZE_APP_TOKEN_STEP_ID, FACTORY_MERGE_FREEZE_CHECK_NAME, FACTORY_MERGE_FREEZE_IF, FACTORY_MERGE_FREEZE_JOB_ID, FACTORY_MERGE_FREEZE_JOB_NAME, FACTORY_MERGE_FREEZE_PERMISSIONS, FACTORY_MERGE_FREEZE_VERIFY_RESULT_EXPRESSION, FACTORY_MERGE_TARGET_REF_CONDITION, FACTORY_PREVIEW_CLEANUP_AUDIT_JOB_ID, FACTORY_PREVIEW_CLEANUP_AUDIT_JOB_NAME, FACTORY_PRODUCTION_IMPACT_BASIS_OUTPUT, FACTORY_PRODUCTION_IMPACT_DECISION_OUTPUT, FACTORY_PRODUCTION_IMPACT_STEP_ID, FACTORY_PRODUCTION_IMPACT_UNSUBSCRIBED_OUTPUT, FACTORY_PROOF_GATE_APP_ID, FACTORY_PROOF_GATE_CHECK_NAME, FACTORY_PROOF_GATE_GUARD, FACTORY_PROOF_GATE_IF, FACTORY_PROOF_GATE_MODE_OUTPUT, FACTORY_PROOF_GATE_OUTPUT, FACTORY_PROOF_GATE_PULL_REQUEST_IF, FACTORY_PROOF_GATE_REASONS, FACTORY_PROOF_GATE_REASON_OUTPUT, FACTORY_PROOF_GATE_SHELL, FACTORY_PROOF_GATE_SOURCE_URL_OUTPUT, FACTORY_PROOF_GATE_STEP_ID, FACTORY_PROOF_GATE_STEP_NAME, FACTORY_PR_STATUS_HUD_APP_TOKEN_STEP_ID, FACTORY_PR_STATUS_HUD_CONCURRENCY, FACTORY_PR_STATUS_HUD_IF, FACTORY_PR_STATUS_HUD_JOB_ID, FACTORY_PR_STATUS_HUD_JOB_NAME, FACTORY_PR_STATUS_HUD_PERMISSIONS, FACTORY_PR_STATUS_HUD_PLAN_PATH, FACTORY_PUSH_IDENTITY_SCHEMA_VERSION, type FactoryCandidateImpactWorkflow, type FactoryCandidateImpactWorkflowOptions, type FactoryLifecycleContractLane, type FactoryLifecycleContractLaneOptions, type FactoryMergeFreezeJob, type FactoryMergeFreezeJobOptions, type FactoryPrStatusHudWorkflow, type FactoryPrStatusHudWorkflowOptions, type FactoryPreviewCleanupAuditJob, type FactoryPreviewCleanupDestroyJob, type FactoryPreviewCleanupTopology, type FactoryPreviewCleanupTopologyOptions, type FactoryProductionImpactWorkflow, type FactoryProductionImpactWorkflowOptions, type FactoryProofGateOptions, type FactoryProofGateReason, type FactoryProofGateReuse, type FactoryProofGateStep, type FactoryProofReusePresentationOptions, type FactoryProofReuseSummaryStep, type FactoryProofTimingStartStep, type FactoryPushIdentityConsumer, type FactoryPushIdentityConsumerOptions, type FactoryPushIdentityDisposition, type FactoryPushIdentityEnvelope, type FactoryPushIdentityProducer, type FactoryPushIdentityProducerOptions, type FactoryRunnerDecision, type FactoryRunnerHome, type FactoryRunnerKind, type FactoryRunnerOverride, type FactoryVerifyRunner, type FactoryVerifyRunnerMismatch, type FactoryVerifyRunnerPolicyInput, type FactoryVerifyRunnerPolicyReport, type FactoryVerifyRunnerTable, type FactoryWorkflowArtifact, type FactoryWorkflowOptions, type FactoryWorkflowSetupOptions, GitHubApiError, GitHubAppTokenTransportError, type GithubAppCredentials, type GithubAppTokenOptions, type InstallStepOptions, type LocalPreviewStage, type LocalPreviewStageOptions, NODE_PNPM_ACTION_FAMILY_NODE24, type NodePnpmActionFamily, PREVIEW_PROOF_INVENTORY_CHECK_NAME, PREVIEW_PROOF_INVENTORY_LIST_PERMISSIONS, type ParseLocalPreviewStageExpected, type ParsedLocalPreviewStage, type PinnedAction, type PreviewProofCandidate, type PreviewProofCleanupOutcome, type PreviewProofCleanupStatus, type PreviewProofEnvelopeEvidence, type PreviewProofInventoryAccess, type PreviewProofInventoryCleanupInput, type PreviewProofInventoryListInput, type PreviewProofInventoryPersistInput, type PreviewProofInventoryStore, type PreviewProofInventoryTransport, type PreviewProofLifecycleEvent, type PreviewProofLifecycleState, type PreviewProofRegistration, type PreviewProofResolveQuery, PreviewProofTransportError, type ProofReuseCommand, type ProofReuseCoverageInput, type ProofReuseCoverageReport, type SetupNodeStepOptions, UPLOAD_ARTIFACT, VITEST_PROFILE_SCHEMA_VERSION, VITEST_PROFILE_TOOL, type VitestJsonReport, type VitestProfile, type VitestProfileDependencies, type VitestProfileDurationSummary, type VitestProfileEnvironment, VitestProfileError, type VitestProfileOptions, type VitestProfileReadResult, type VitestProfileSample, type VitestProfileSampleExecution, type VitestTestStatus, type WorkflowShellParseFailure, type WorkflowStep, assertProofReuseCoverage, assertVerifyRunnerPolicy, assertWorkflowShellParses, bundleAlchemyEntry, captureVitestProfileEnvironment, executeAlchemyEntry, factoryCandidateImpactWorkflow, factoryCandidateOrPushCondition, factoryLifecycleContractLane, factoryMergeFreezeJob, factoryMergeFreezeScript, factoryPrStatusHudWorkflow, factoryPreviewCleanupTopology, factoryProductionImpactWorkflow, factoryProofGateScript, factoryProofGateStep, factoryProofReuseSummaryStep, factoryProofTimingStartStep, factoryPushIdentityConsumer, factoryPushIdentityProducer, factoryVerifyRunner, factoryWorkflow, githubAppJwt, isLocalPreviewStage, localPreviewStage, mintInstallationToken, normalizeVitestProfileSample, parseLocalPreviewStage, previewCleanupDestroyJobId, previewProofInventory, previewProofLifecycle, productionImpactTargetOutput, proofReuseCoverage, proofReuseRequiredCommands, readVitestProfileDocument, resolveProofReuseCommands, runVitestProfile, verifyRunnerPolicy, workflowRunBlocks, workflowShellParseFailures, writeVitestProfile };