@patronage/factory-ci 1.0.0-alpha.26 → 1.0.0-alpha.28
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 +7 -8
- package/dist/.build-fingerprint.json +1 -1
- package/dist/alchemy/index.d.ts +14 -2
- package/dist/alchemy/index.js +37 -8
- package/dist/index.d.ts +73 -81
- package/dist/index.js +98 -172
- package/package.json +1 -1
- package/src/alchemy/credential-preflight.ts +8 -2
- package/src/alchemy/secret-name-policy.ts +6 -0
- package/src/alchemy/stage-policy.ts +7 -6
- package/src/alchemy/url-implies-auth.ts +20 -1
- package/src/alchemy-baseline.ts +19 -6
- package/src/candidate-impact-workflow.ts +2 -0
- package/src/cursor-oidc.ts +33 -0
- package/src/github-app-token.ts +40 -2
- package/src/index.ts +10 -2
- package/src/pr-status-hud-workflow.ts +2 -0
- package/src/production-impact-workflow.ts +2 -0
- package/src/proof-reuse-gate.ts +23 -213
- package/src/proof-reuse-presentation.ts +4 -8
- package/src/runnable-cli.ts +27 -0
package/README.md
CHANGED
|
@@ -365,9 +365,8 @@ const coreCommands = profile.verification.commands.filter(
|
|
|
365
365
|
);
|
|
366
366
|
|
|
367
367
|
const core = job("core", {
|
|
368
|
-
// `checks: read` is all the
|
|
369
|
-
//
|
|
370
|
-
// where the merge fallback (#611) looks up the producing pull request.
|
|
368
|
+
// `checks: read` is all the PR-only gate needs. It reads one Checks API
|
|
369
|
+
// endpoint and nothing else.
|
|
371
370
|
permissions: { checks: "read", contents: "read" },
|
|
372
371
|
steps: [
|
|
373
372
|
step(factoryProofGateStep({ commands: coreCommands, surface: "core" })),
|
|
@@ -380,11 +379,9 @@ const core = job("core", {
|
|
|
380
379
|
|
|
381
380
|
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.
|
|
382
381
|
|
|
383
|
-
**The gate runs on pull requests only
|
|
382
|
+
**The gate runs on pull requests only** (#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 answers: a `main` push reused an impact-scoped candidate proof, executed no command, and left a red package unnoticed for three days.
|
|
384
383
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
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.
|
|
384
|
+
There is no mode to choose. The emitted condition is a trust predicate, so the package owns it: the merge-target opt-out and its push-event merge fallback (#611) are deleted, and no consumer option can reuse proof outside a pull request.
|
|
388
385
|
|
|
389
386
|
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.
|
|
390
387
|
|
|
@@ -489,6 +486,8 @@ Nothing is cached. The token is returned to the caller, which owns its lifetime
|
|
|
489
486
|
|
|
490
487
|
`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.
|
|
491
488
|
|
|
489
|
+
Callers that must not receive the App's full installation grant pass `permissions` and `repositories` on the mint input. GitHub then issues a token covering only that subset. The Factory App broker uses `FACTORY_APP_BROKER_TOKEN_PERMISSIONS` (`checks: write`) and the one repository the Cursor OIDC claim authenticated (#949). Omitted, those fields keep the historical full-grant mint that operator PEM and Actions already use.
|
|
490
|
+
|
|
492
491
|
### Vitest suite profiling
|
|
493
492
|
|
|
494
493
|
```ts
|
|
@@ -679,7 +678,7 @@ import { ALCHEMY_BASELINE, assertAlchemyBaseline } from "@patronage/factory-ci";
|
|
|
679
678
|
|
|
680
679
|
`ALCHEMY_BASELINE` is the exact `alchemy` and `effect` pair the fleet moves together on: `{ alchemy: "2.0.0-beta.76", effect: "4.0.0-rc.112" }`. It is a plain constant on the root entry — it imports neither package — so any consumer can read it without installing the `./alchemy` subpath's peers.
|
|
681
680
|
|
|
682
|
-
`assertAlchemyBaseline({ packageJson })` is a consumer contract helper: it fails when the consumer's own `dependencies` or `devDependencies` pin `alchemy` or `effect` to anything other than the baseline
|
|
681
|
+
`assertAlchemyBaseline({ packageJson })` is a consumer contract helper: it fails when the consumer's own `dependencies` or `devDependencies` pin `alchemy` or `effect` to anything other than the baseline, and it fails when only one of the pair is present. Alchemy peers on Effect, so an unpinned auto-installed Effect can drift outside the baseline while a lone `alchemy` pin would otherwise pass. Both packages remaining absent is not drift — they are optional peers of the subpath, so a consumer that never imports it, such as this package's own CLI, carries neither and passes. The pin is exact, not a range: the fleet is pre-1.0 and moves together, so a range would let one project drift silently ahead of or behind the rest. `assertAlchemyBaseline` reads only the object it is handed; it never walks the filesystem or reads a lockfile. Call it from a consumer's own contract test, passing that consumer's parsed `package.json`.
|
|
683
682
|
|
|
684
683
|
The baseline bumps only in a lockstep release: the same commit that bumps `@patronage/factory-ci`'s own `alchemy`/`effect` devDependencies and the `./alchemy` subpath's peer ranges (ADR 0031) also bumps `ALCHEMY_BASELINE`, so the constant a consumer's contract test checks against can never point at a version the subpath itself has moved past.
|
|
685
684
|
|
package/dist/alchemy/index.d.ts
CHANGED
|
@@ -342,8 +342,8 @@ declare class CloudflareCredentialUnavailableError extends Error {
|
|
|
342
342
|
constructor(message?: string);
|
|
343
343
|
}
|
|
344
344
|
/**
|
|
345
|
-
* Resolves the deploy's Cloudflare credential posture and
|
|
346
|
-
*
|
|
345
|
+
* Resolves the deploy's Cloudflare credential posture and checks the token
|
|
346
|
+
* against the reviewed policy document.
|
|
347
347
|
*
|
|
348
348
|
* The check is: the environment matches the declared source, the credential
|
|
349
349
|
* resolves, Cloudflare reports the token active, the reviewed policy names
|
|
@@ -351,6 +351,12 @@ declare class CloudflareCredentialUnavailableError extends Error {
|
|
|
351
351
|
* the project expects, and the permission groups match the required list with
|
|
352
352
|
* no group missing, extra, or repeated.
|
|
353
353
|
*
|
|
354
|
+
* The permission groups come from the reviewed policy document, not from
|
|
355
|
+
* Cloudflare. This function never reads live permissions from the Cloudflare
|
|
356
|
+
* API. A token whose permissions changed after the review still passes, as
|
|
357
|
+
* long as Cloudflare reports it active and the document still matches its id.
|
|
358
|
+
* See issue #1010.
|
|
359
|
+
*
|
|
354
360
|
* Throws on any failure. {@link CloudflareCredentialUnavailableError} means
|
|
355
361
|
* Cloudflare could not be asked; every other error means the credential is
|
|
356
362
|
* wrong. Nothing it throws, and nothing it returns, carries a secret value.
|
|
@@ -550,6 +556,12 @@ interface SecretNamePolicyOptions {
|
|
|
550
556
|
* Longer values are replaced first, so a secret that contains another secret
|
|
551
557
|
* leaves no fragment behind.
|
|
552
558
|
*
|
|
559
|
+
* The policy replaces verbatim substrings of each named value only. A value
|
|
560
|
+
* that the output re-encodes passes through unredacted. URL-encoded,
|
|
561
|
+
* base64, and JSON-escaped forms are all such re-encodings. A project that
|
|
562
|
+
* prints a secret in one of those forms must not rely on this policy for it.
|
|
563
|
+
* See issue #1011.
|
|
564
|
+
*
|
|
553
565
|
* An empty `names` list yields a pass-through function only when `values` is
|
|
554
566
|
* also empty: a project with no secrets in its Alchemy output declares none,
|
|
555
567
|
* and gets no redaction. Empty names beside a populated `values` map is
|
package/dist/alchemy/index.js
CHANGED
|
@@ -180,15 +180,18 @@ const LOCAL_PREVIEW_STAGES = (stage) => isLocalPreviewStage(stage);
|
|
|
180
180
|
*
|
|
181
181
|
* A `RegExp` with `g` or `y` advances `lastIndex` on every match, so the same
|
|
182
182
|
* pattern answers "yes" and then "no" for the same stage. A policy that
|
|
183
|
-
* alternates is worse than one that always refuses, so
|
|
184
|
-
*
|
|
185
|
-
*
|
|
183
|
+
* alternates is worse than one that always refuses, so a flagged pattern is
|
|
184
|
+
* tested through a fresh copy whose `lastIndex` starts at zero. The copy
|
|
185
|
+
* carries the complete flags: `y` anchors the match at `lastIndex`, so
|
|
186
|
+
* dropping it would let `/prod/y` accept a stage named `pre-prod` that the
|
|
187
|
+
* project's own pattern rejects. The caller's own `RegExp` is never written
|
|
188
|
+
* to, so a frozen one still works.
|
|
186
189
|
*/
|
|
187
|
-
const statelessTest = (pattern, stage) => pattern.global || pattern.sticky ? new RegExp(pattern.source, pattern.flags
|
|
190
|
+
const statelessTest$1 = (pattern, stage) => pattern.global || pattern.sticky ? new RegExp(pattern.source, pattern.flags).test(stage) : pattern.test(stage);
|
|
188
191
|
const stageMatches = (matcher, stage) => {
|
|
189
192
|
if (typeof matcher === "function") return matcher(stage);
|
|
190
193
|
if (Array.isArray(matcher)) return matcher.includes(stage);
|
|
191
|
-
return statelessTest(matcher, stage);
|
|
194
|
+
return statelessTest$1(matcher, stage);
|
|
192
195
|
};
|
|
193
196
|
/** A matcher that accepts a stage any of `matchers` accepts. */
|
|
194
197
|
const anyStage = (...matchers) => (stage) => matchers.some((matcher) => stageMatches(matcher, stage));
|
|
@@ -387,8 +390,8 @@ const resolveResolvedCredential = async (resolveCredential, environment) => {
|
|
|
387
390
|
}
|
|
388
391
|
};
|
|
389
392
|
/**
|
|
390
|
-
* Resolves the deploy's Cloudflare credential posture and
|
|
391
|
-
*
|
|
393
|
+
* Resolves the deploy's Cloudflare credential posture and checks the token
|
|
394
|
+
* against the reviewed policy document.
|
|
392
395
|
*
|
|
393
396
|
* The check is: the environment matches the declared source, the credential
|
|
394
397
|
* resolves, Cloudflare reports the token active, the reviewed policy names
|
|
@@ -396,6 +399,12 @@ const resolveResolvedCredential = async (resolveCredential, environment) => {
|
|
|
396
399
|
* the project expects, and the permission groups match the required list with
|
|
397
400
|
* no group missing, extra, or repeated.
|
|
398
401
|
*
|
|
402
|
+
* The permission groups come from the reviewed policy document, not from
|
|
403
|
+
* Cloudflare. This function never reads live permissions from the Cloudflare
|
|
404
|
+
* API. A token whose permissions changed after the review still passes, as
|
|
405
|
+
* long as Cloudflare reports it active and the document still matches its id.
|
|
406
|
+
* See issue #1010.
|
|
407
|
+
*
|
|
399
408
|
* Throws on any failure. {@link CloudflareCredentialUnavailableError} means
|
|
400
409
|
* Cloudflare could not be asked; every other error means the credential is
|
|
401
410
|
* wrong. Nothing it throws, and nothing it returns, carries a secret value.
|
|
@@ -561,7 +570,21 @@ const routesReachable = (routes) => Array.isArray(routes) && routes.length > 0;
|
|
|
561
570
|
const enrollsInAccess = (access) => access !== void 0 && access !== null;
|
|
562
571
|
/** Is this Worker reachable from the internet by what the stack declares? */
|
|
563
572
|
const publiclyReachable = ({ props }) => workersDevReachable(props.workersDev) || domainReachable(props.domain) || routesReachable(props.routes);
|
|
564
|
-
|
|
573
|
+
/**
|
|
574
|
+
* Test a pattern without carrying state between Workers.
|
|
575
|
+
*
|
|
576
|
+
* A `RegExp` with `g` or `y` advances `lastIndex` on every match, so the same
|
|
577
|
+
* pattern answers "yes" for the first Worker and "no" for the second Worker
|
|
578
|
+
* with the same binding name. A guard that alternates is worse than one that
|
|
579
|
+
* always refuses, so a flagged pattern is tested through a fresh copy whose
|
|
580
|
+
* `lastIndex` starts at zero. The copy carries the complete flags: `y`
|
|
581
|
+
* anchors the match at `lastIndex`, so dropping it would let `/TOKEN/y`
|
|
582
|
+
* credit a binding named `X_TOKEN` that the caller's pattern rejects. The
|
|
583
|
+
* caller's own `RegExp` is never written to, so a frozen one still works.
|
|
584
|
+
* Same isolation `stage-policy` already applies to stage names.
|
|
585
|
+
*/
|
|
586
|
+
const statelessTest = (pattern, name) => pattern.global || pattern.sticky ? new RegExp(pattern.source, pattern.flags).test(name) : pattern.test(name);
|
|
587
|
+
const matchesAuthBindingName = (matcher, name) => matcher instanceof RegExp ? statelessTest(matcher, name) : matcher(name);
|
|
565
588
|
/**
|
|
566
589
|
* The Workers a Cloudflare Access application gates in production.
|
|
567
590
|
*
|
|
@@ -661,6 +684,12 @@ const MINIMUM_REDACTABLE_SECRET_LENGTH = 8;
|
|
|
661
684
|
* Longer values are replaced first, so a secret that contains another secret
|
|
662
685
|
* leaves no fragment behind.
|
|
663
686
|
*
|
|
687
|
+
* The policy replaces verbatim substrings of each named value only. A value
|
|
688
|
+
* that the output re-encodes passes through unredacted. URL-encoded,
|
|
689
|
+
* base64, and JSON-escaped forms are all such re-encodings. A project that
|
|
690
|
+
* prints a secret in one of those forms must not rely on this policy for it.
|
|
691
|
+
* See issue #1011.
|
|
692
|
+
*
|
|
664
693
|
* An empty `names` list yields a pass-through function only when `values` is
|
|
665
694
|
* also empty: a project with no secrets in its Alchemy output declares none,
|
|
666
695
|
* and gets no redaction. Empty names beside a populated `values` map is
|
package/dist/index.d.ts
CHANGED
|
@@ -29,9 +29,12 @@ interface AssertAlchemyBaselineInput {
|
|
|
29
29
|
}
|
|
30
30
|
/**
|
|
31
31
|
* Fails when the consumer's `alchemy` or `effect` pin differs from
|
|
32
|
-
* {@link ALCHEMY_BASELINE}
|
|
33
|
-
*
|
|
34
|
-
*
|
|
32
|
+
* {@link ALCHEMY_BASELINE}, or when only one of the pair is present.
|
|
33
|
+
* Alchemy peers on Effect, so an unpinned auto-installed Effect can drift
|
|
34
|
+
* outside the baseline while a lone `alchemy` pin would otherwise pass.
|
|
35
|
+
* Both packages remaining absent is not drift — they are optional peers of
|
|
36
|
+
* the `./alchemy` subpath, so a consumer that never touches it carries
|
|
37
|
+
* neither.
|
|
35
38
|
*
|
|
36
39
|
* Reads only the `packageJson` object it is handed; it never walks the
|
|
37
40
|
* filesystem.
|
|
@@ -511,6 +514,35 @@ interface FactoryWorkflowArtifact<Additional extends Readonly<Record<string, Pin
|
|
|
511
514
|
*/
|
|
512
515
|
declare const factoryWorkflow: <const Additional extends Readonly<Record<string, PinnedAction>> = Record<never, never>>(options: FactoryWorkflowOptions<Additional>) => FactoryWorkflowArtifact<Additional>;
|
|
513
516
|
//#endregion
|
|
517
|
+
//#region src/cursor-oidc.d.ts
|
|
518
|
+
/**
|
|
519
|
+
* Pinned Cursor OIDC trust predicates for the Factory App broker (#949).
|
|
520
|
+
*
|
|
521
|
+
* The broker verifies a Cursor-managed Cloud Agent JWT and mints a
|
|
522
|
+
* repo-scoped Factory App installation token. Audience, issuer, and JWKS are
|
|
523
|
+
* not consumer settings: a knob that pointed them elsewhere would be the
|
|
524
|
+
* whole mechanism undone, the same reason {@link FACTORY_PROOF_GATE_APP_ID}
|
|
525
|
+
* is pinned.
|
|
526
|
+
*/
|
|
527
|
+
/** Cursor's OIDC issuer. Tokens minted on the agent VM carry this `iss`. */
|
|
528
|
+
declare const FACTORY_CURSOR_OIDC_ISSUER = "https://api.cursor.com";
|
|
529
|
+
/** JWKS the broker uses to verify RS256 signatures. */
|
|
530
|
+
declare const FACTORY_CURSOR_OIDC_JWKS_URL = "https://api.cursor.com/keys";
|
|
531
|
+
/**
|
|
532
|
+
* Audience a Cursor-managed VM must mint with. The CLI and the broker share
|
|
533
|
+
* this string; Cursor does not allowlist audiences.
|
|
534
|
+
*/
|
|
535
|
+
declare const FACTORY_CURSOR_OIDC_AUDIENCE = "Factory";
|
|
536
|
+
/**
|
|
537
|
+
* Runtime claim a Cursor-managed Cloud Agent VM always presents. Self-hosted
|
|
538
|
+
* workers are not this broker's callers.
|
|
539
|
+
*/
|
|
540
|
+
declare const FACTORY_CURSOR_OIDC_MANAGED_RUNTIME = "managed";
|
|
541
|
+
/** HQ route that exchanges a verified Cursor OIDC JWT for an App token. */
|
|
542
|
+
declare const FACTORY_APP_BROKER_PATH = "/api/factory-app/installation-token";
|
|
543
|
+
/** Production broker origin. The CLI pins this; it is not profile-declared. */
|
|
544
|
+
declare const FACTORY_APP_BROKER_ORIGIN = "https://hq.patronage.com";
|
|
545
|
+
//#endregion
|
|
514
546
|
//#region src/github-transport.d.ts
|
|
515
547
|
/**
|
|
516
548
|
* Bounded transport retries could not reach the GitHub App endpoint (#923).
|
|
@@ -545,6 +577,18 @@ declare class GitHubApiError extends Error {
|
|
|
545
577
|
readonly status: number;
|
|
546
578
|
constructor(status: number, statusText: string);
|
|
547
579
|
}
|
|
580
|
+
/**
|
|
581
|
+
* Installation-token permission subset. GitHub only grants permissions the
|
|
582
|
+
* App already has; a caller that wants least privilege names the subset here
|
|
583
|
+
* rather than taking the App's full installation grant (#949).
|
|
584
|
+
*/
|
|
585
|
+
interface GithubAppInstallationPermissions {
|
|
586
|
+
checks?: "read" | "write";
|
|
587
|
+
}
|
|
588
|
+
/** The Factory App broker's token: repo-scoped Checks write, nothing else. */
|
|
589
|
+
declare const FACTORY_APP_BROKER_TOKEN_PERMISSIONS: {
|
|
590
|
+
readonly checks: "write";
|
|
591
|
+
};
|
|
548
592
|
interface GithubAppTokenOptions {
|
|
549
593
|
/** Injectable `fetch` (tests, or a caller with its own instrumented one). */
|
|
550
594
|
fetch?: typeof fetch;
|
|
@@ -584,6 +628,18 @@ declare const githubAppJwt: (credentials: GithubAppCredentials, options?: Pick<G
|
|
|
584
628
|
declare const mintInstallationToken: (input: {
|
|
585
629
|
credentials: GithubAppCredentials;
|
|
586
630
|
owner: string;
|
|
631
|
+
/**
|
|
632
|
+
* Installation permission subset. Omitted, GitHub grants the App's full
|
|
633
|
+
* installation permissions. The Factory App broker passes
|
|
634
|
+
* {@link FACTORY_APP_BROKER_TOKEN_PERMISSIONS}.
|
|
635
|
+
*/
|
|
636
|
+
permissions?: GithubAppInstallationPermissions;
|
|
637
|
+
/**
|
|
638
|
+
* Repository names (not `owner/repo`) to bind the token to. Omitted, the
|
|
639
|
+
* token covers every repository the installation can reach. The broker
|
|
640
|
+
* always names the one repository the Cursor OIDC claim authenticated.
|
|
641
|
+
*/
|
|
642
|
+
repositories?: readonly string[];
|
|
587
643
|
repo: string;
|
|
588
644
|
}, options?: GithubAppTokenOptions) => Promise<string>;
|
|
589
645
|
//#endregion
|
|
@@ -658,13 +714,9 @@ declare const executeAlchemyEntry: (options: ExecuteAlchemyEntryOptions) => Prom
|
|
|
658
714
|
*
|
|
659
715
|
* It answers one question, before checkout and before install: may this job
|
|
660
716
|
* reuse the local verification the factory already published for *this exact
|
|
661
|
-
* head*?
|
|
662
|
-
*
|
|
663
|
-
*
|
|
664
|
-
* extends "this exact head" to "this exact tree": a squash merge whose result
|
|
665
|
-
* tree is byte-identical to the producing pull request's proven head tree
|
|
666
|
-
* reuses that head's proof (#611). Refusing runs hosted CI; it never fails
|
|
667
|
-
* the candidate. Three
|
|
717
|
+
* head*? It asks that only on a pull request, so every default-branch push
|
|
718
|
+
* executes the full hosted suite (#863). Refusing runs hosted CI; it never
|
|
719
|
+
* fails the candidate. Three
|
|
668
720
|
* repositories had independently grown their own answer to that question and
|
|
669
721
|
* had already drifted apart, so the answer lives here once, with the App
|
|
670
722
|
* identity, check name, step identity, output names, guard, and every trust
|
|
@@ -742,70 +794,24 @@ declare const FACTORY_PROOF_GATE_SOURCE_URL_OUTPUT = "source-check-url";
|
|
|
742
794
|
*/
|
|
743
795
|
declare const FACTORY_PROOF_GATE_GUARD = "steps.factory-proof.outputs.reuse-proof != 'true'";
|
|
744
796
|
/**
|
|
745
|
-
* Pull requests
|
|
746
|
-
*
|
|
747
|
-
*
|
|
748
|
-
* It buys runner time on the merge target and pays for it in coverage. No
|
|
749
|
-
* local proof is ever written for a merged SHA, so the direct read finds
|
|
750
|
-
* nothing after a squash merge. The push path therefore exists only together
|
|
751
|
-
* with the merge fallback below: when no generation exists at the merged SHA,
|
|
752
|
-
* the gate may look up the producing pull request and reuse its head proof,
|
|
753
|
-
* but only when the merge result's tree is exactly the proven head's tree.
|
|
754
|
-
* Deploy gates keep waiting for a green required check at the merged SHA; a
|
|
755
|
-
* reused proof turns that check green through the same guarded steps a pull
|
|
756
|
-
* request uses.
|
|
757
|
-
*
|
|
758
|
-
* The trade this mode accepts: a candidate's proof may be impact-scoped, and
|
|
759
|
-
* an impact-scoped proof is a sound gate for the candidate and an unsound
|
|
760
|
-
* gate for the merge target. #931 is that defect observed — a `main` push
|
|
761
|
-
* reused a candidate proof, executed no command, and left a red package
|
|
762
|
-
* unnoticed for three days. Choose this mode only when the consumer accepts
|
|
763
|
-
* that risk for speed.
|
|
764
|
-
*
|
|
765
|
-
* The push side is deliberately the merge target only — the repository's
|
|
766
|
-
* default branch — not every pushed branch. Which branches trigger a
|
|
767
|
-
* consumer's workflow at all stays repository-owned; this condition only
|
|
768
|
-
* keeps the gate from consulting proof on pushes that are not merges into
|
|
769
|
-
* the default branch.
|
|
770
|
-
*/
|
|
771
|
-
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))";
|
|
772
|
-
/**
|
|
773
|
-
* Pull requests only — the **default** condition, and the condition a
|
|
774
|
-
* consumer gets with `reuse: "pull-request"` (#873, default since #863).
|
|
797
|
+
* Pull requests only — the gate's single condition (#873, default since #863,
|
|
798
|
+
* the only mode since the merge-target opt-out was deleted).
|
|
775
799
|
*
|
|
776
800
|
* A merge target's guarantee is about the whole tree, not the diff. A
|
|
777
801
|
* candidate proof only ever binds the diff the candidate was scoped to, so
|
|
778
802
|
* reusing it on the merged commit proves less than the merge target needs.
|
|
779
|
-
*
|
|
780
|
-
*
|
|
781
|
-
*
|
|
782
|
-
*
|
|
803
|
+
* #931 is that defect observed — a `main` push reused a candidate proof,
|
|
804
|
+
* executed no command, and left a red package unnoticed for three days. Every
|
|
805
|
+
* default-branch push therefore executes the full hosted suite, whatever
|
|
806
|
+
* proof exists.
|
|
783
807
|
*
|
|
784
808
|
* Which branches trigger the workflow at all stays repository-owned. This
|
|
785
809
|
* condition only keeps the gate from consulting proof outside a pull request.
|
|
786
810
|
*/
|
|
787
|
-
declare const
|
|
788
|
-
/**
|
|
789
|
-
* Which events may reuse proof.
|
|
790
|
-
*
|
|
791
|
-
* A two-value enum, not a free-text condition: a string lets a consumer write
|
|
792
|
-
* a condition this package cannot reason about — one that reuses proof on an
|
|
793
|
-
* unproven ref — and the emitted condition is a trust predicate. The consumer
|
|
794
|
-
* chooses the mode; `factory-ci` owns what each mode emits.
|
|
795
|
-
*
|
|
796
|
-
* - `pull-request` (default) pull requests only. Default-branch pushes
|
|
797
|
-
* execute the full hosted suite, and the merge fallback is not emitted.
|
|
798
|
-
* - `pull-request-and-default-branch` the opt-out: pull requests plus pushes
|
|
799
|
-
* to the merge target, with the merge fallback. It is faster on the merge
|
|
800
|
-
* target and it lets an impact-scoped candidate proof gate a whole tree
|
|
801
|
-
* (#931), so a consumer must ask for it.
|
|
802
|
-
*/
|
|
803
|
-
type FactoryProofGateReuse = "pull-request" | "pull-request-and-default-branch";
|
|
811
|
+
declare const FACTORY_PROOF_GATE_IF = "github.event_name == 'pull_request'";
|
|
804
812
|
/**
|
|
805
813
|
* The complete refusal vocabulary. Deliberately few, because these are the
|
|
806
814
|
* only distinctions the gate can honestly make from its Checks API reads.
|
|
807
|
-
* The merge fallback adds no words: a fallback that establishes nothing
|
|
808
|
-
* leaves the direct read's refusal standing, with the specifics in `detail`.
|
|
809
815
|
*
|
|
810
816
|
* - `proven` a covering, passing, unambiguous proof for this exact head
|
|
811
817
|
* - `none` no App-verified factory check run at this commit
|
|
@@ -884,13 +890,6 @@ interface FactoryProofGateOptions {
|
|
|
884
890
|
* therefore requires a different coverage.
|
|
885
891
|
*/
|
|
886
892
|
readonly commands: readonly ProofReuseCommand[];
|
|
887
|
-
/**
|
|
888
|
-
* Which events may reuse proof. Defaults to `"pull-request"`: every
|
|
889
|
-
* default-branch push executes the full hosted suite (#873, #863).
|
|
890
|
-
* `"pull-request-and-default-branch"` is the opt-out a consumer chooses for
|
|
891
|
-
* speed on the merge target, and it accepts #931's risk.
|
|
892
|
-
*/
|
|
893
|
-
readonly reuse?: FactoryProofGateReuse;
|
|
894
893
|
/** Names the suite in the job summary. Changes no trust decision. */
|
|
895
894
|
readonly surface: string;
|
|
896
895
|
}
|
|
@@ -908,7 +907,6 @@ interface FactoryProofGateOptions {
|
|
|
908
907
|
*/
|
|
909
908
|
declare const factoryProofGateScript: ({
|
|
910
909
|
commands,
|
|
911
|
-
reuse,
|
|
912
910
|
surface
|
|
913
911
|
}: FactoryProofGateOptions) => string;
|
|
914
912
|
interface FactoryProofGateStep {
|
|
@@ -918,7 +916,7 @@ interface FactoryProofGateStep {
|
|
|
918
916
|
HEAD_SHA: string;
|
|
919
917
|
}>;
|
|
920
918
|
readonly id: typeof FACTORY_PROOF_GATE_STEP_ID;
|
|
921
|
-
readonly if: typeof FACTORY_PROOF_GATE_IF
|
|
919
|
+
readonly if: typeof FACTORY_PROOF_GATE_IF;
|
|
922
920
|
readonly name: string;
|
|
923
921
|
readonly run: string;
|
|
924
922
|
readonly shell: typeof FACTORY_PROOF_GATE_SHELL;
|
|
@@ -927,14 +925,8 @@ interface FactoryProofGateStep {
|
|
|
927
925
|
* The step itself, structurally accepted by gagen's `step()` without adding a
|
|
928
926
|
* gagen runtime dependency. It belongs first in the job it guards: a few API
|
|
929
927
|
* reads with the default `GITHUB_TOKEN`, no checkout, no install, so a proven
|
|
930
|
-
* head costs a runner nothing beyond job startup. The
|
|
931
|
-
*
|
|
932
|
-
* needs only `checks: read` (#873, #863). A consumer that opts into
|
|
933
|
-
* `reuse: "pull-request-and-default-branch"` also runs the push-event merge
|
|
934
|
-
* fallback, which reads the producing pull request (`pull-requests: read`)
|
|
935
|
-
* and the two commit objects whose tree ids it compares (`contents: read`).
|
|
936
|
-
* A job that grants less loses only the fallback — the failed read degrades
|
|
937
|
-
* to the full suite.
|
|
928
|
+
* head costs a runner nothing beyond job startup. The step runs on pull
|
|
929
|
+
* requests only, so it needs only `checks: read` (#873, #863).
|
|
938
930
|
*
|
|
939
931
|
* A **step, not a job**, and that is not a style preference. A separate gate
|
|
940
932
|
* job that errored would leave the guarded job `skipped`, and a summary job
|
|
@@ -1715,4 +1707,4 @@ declare const assertWorkflowShellParses: (yaml: string, options: {
|
|
|
1715
1707
|
readonly source: string;
|
|
1716
1708
|
}) => void;
|
|
1717
1709
|
//#endregion
|
|
1718
|
-
export { ALCHEMY_BASELINE, type AlchemyBaselinePackageJson, type AssertAlchemyBaselineInput, 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,
|
|
1710
|
+
export { ALCHEMY_BASELINE, type AlchemyBaselinePackageJson, type AssertAlchemyBaselineInput, type BundleAlchemyEntryOptions, type CheckoutStepOptions, EXECUTE_ALCHEMY_ENTRY_MAX_BUFFER, type ExecuteAlchemyEntryOptions, type ExecuteAlchemyEntryResult, FACTORY_APP_BROKER_ORIGIN, FACTORY_APP_BROKER_PATH, FACTORY_APP_BROKER_TOKEN_PERMISSIONS, 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_CURSOR_OIDC_AUDIENCE, FACTORY_CURSOR_OIDC_ISSUER, FACTORY_CURSOR_OIDC_JWKS_URL, FACTORY_CURSOR_OIDC_MANAGED_RUNTIME, 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_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 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 GithubAppInstallationPermissions, 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, assertAlchemyBaseline, 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 };
|