@patronage/factory-ci 1.0.0-alpha.22 → 1.0.0-alpha.23
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 -9
- package/dist/index.d.ts +58 -22
- package/dist/index.js +72 -35
- package/package.json +2 -2
- package/src/actions.ts +13 -0
- package/src/candidate-impact-workflow.ts +17 -14
- package/src/execute-alchemy-entry.ts +27 -22
- package/src/impact-demand-condition.ts +67 -0
- package/src/index.ts +1 -0
- package/src/merge-freeze-job.ts +7 -0
- package/src/preview-proof-inventory.ts +2 -7
- package/src/preview-proof-lifecycle.ts +1 -8
- package/src/production-impact-workflow.ts +17 -14
- package/src/proof-reuse-gate.ts +4 -2
- package/src/push-identity-workflow.ts +16 -2
package/README.md
CHANGED
|
@@ -79,7 +79,7 @@ const core = job("core", {
|
|
|
79
79
|
|
|
80
80
|
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
81
|
|
|
82
|
-
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)` expressions:
|
|
82
|
+
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:
|
|
83
83
|
|
|
84
84
|
```ts
|
|
85
85
|
const impact = factoryProductionImpactWorkflow({
|
|
@@ -111,6 +111,7 @@ import {
|
|
|
111
111
|
factoryPushIdentityProducer,
|
|
112
112
|
factoryWorkflow,
|
|
113
113
|
NODE_PNPM_ACTION_FAMILY_NODE24,
|
|
114
|
+
UPLOAD_ARTIFACT,
|
|
114
115
|
} from "@patronage/factory-ci";
|
|
115
116
|
|
|
116
117
|
const workflowArtifact = factoryWorkflow({
|
|
@@ -120,10 +121,7 @@ const workflowArtifact = factoryWorkflow({
|
|
|
120
121
|
tag: "v5.0.0",
|
|
121
122
|
uses: "actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0",
|
|
122
123
|
},
|
|
123
|
-
uploadArtifact:
|
|
124
|
-
tag: "v4.6.2",
|
|
125
|
-
uses: "actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02",
|
|
126
|
-
},
|
|
124
|
+
uploadArtifact: UPLOAD_ARTIFACT,
|
|
127
125
|
},
|
|
128
126
|
generated: {
|
|
129
127
|
source: ".github/workflows/verify.ts",
|
|
@@ -165,7 +163,7 @@ The helper selects no action family or action version. Callers pass the explicit
|
|
|
165
163
|
|
|
166
164
|
This is identity transport and validation only. The consumer still owns the `workflow_run` trigger and branch policy, job topology and runners, deploy credentials and environments, target declarations, commands, ordering, and convergence/no-op proof. None of those belong in the identity artifact or decision job.
|
|
167
165
|
|
|
168
|
-
Candidate pull-request routing follows the same ownership line through `factoryCandidateImpactWorkflow` (#741). A generated Verify workflow declares only its target names and consumes the returned decision step and fail-open `demandedIf(target)` expressions, so the profile's canonical impact declarations and dependency graph route the run instead of a repository-maintained path filter:
|
|
166
|
+
Candidate pull-request routing follows the same ownership line through `factoryCandidateImpactWorkflow` (#741). A generated Verify workflow declares only its target names and consumes the returned decision step and fail-open `demandedIf(target, decisionJob)` expressions, so the profile's canonical impact declarations and dependency graph route the run instead of a repository-maintained path filter:
|
|
169
167
|
|
|
170
168
|
```ts
|
|
171
169
|
const impact = factoryCandidateImpactWorkflow({
|
|
@@ -191,7 +189,7 @@ const core = job("core", {
|
|
|
191
189
|
});
|
|
192
190
|
```
|
|
193
191
|
|
|
194
|
-
`demandedIf(target, decisionJob)` emits `always()` so a failed decision job still demands work, and it checks only the decision job's result. Declare the decision job as the routed job's only `needs`. A routed job with more prerequisites must add its own `needs.<job>.result == 'success'` clauses, because `always()` suppresses GitHub's implicit success gating for every dependency. The production decision seam has the same contract.
|
|
192
|
+
`demandedIf(target, decisionJob)` builds the cross-job condition. The decision job id is required, so a routed job cannot receive a same-job condition by accident. It emits `always()` so a failed decision job still demands work, and it checks only the decision job's result. Declare the decision job as the routed job's only `needs`. A routed job with more prerequisites must add its own `needs.<job>.result == 'success'` clauses, because `always()` suppresses GitHub's implicit success gating for every dependency. `demandedIfAtStep(target)` builds the same fail-open condition for a step inside the decision job, where the decision is read from `steps.<stepId>`. The production decision seam has the same contract.
|
|
195
193
|
|
|
196
194
|
Trust boundary: candidate routing is a cost control inside the pull request's own CI, not a merge-security control. A `pull_request` run executes the PR's workflow file and install scripts, so a hostile PR can already shape any of its own job outputs — with or without this seam, exactly as with a repository path filter. Merge admission never trusts these outputs: merge-target push verification stays full and unrouted, and the factory proof chain binds to commits, not to job conditions.
|
|
197
195
|
|
|
@@ -379,7 +377,7 @@ import { bundleAlchemyEntry, executeAlchemyEntry } from "@patronage/factory-ci";
|
|
|
379
377
|
|
|
380
378
|
`alias` is a specifier-to-target map handed to esbuild. Substitution runs before the `packages` and `external` decisions, so an aliased bare specifier is inlined even under `packages: "external"` — that is how a repo points a workspace-only or duplicated package at one file. Give absolute paths or package names; which aliases a repo needs is consumer policy and this package ships no defaults. `executeAlchemyEntry` passes its whole `bundle` option through, so the map is available there too. Because substitution runs first, an alias on `alchemy`, `effect`, or any of their subpaths would silently defeat the externals and bundle a second copy, so those keys are rejected outright. Targets are not string-matched — no rule over how a path is spelled can survive `..` segments or symlinks — so instead the build's metafile is checked afterwards and the bundle is rejected if it carries any input from a reserved package, however that file was reached. A consumer shim that re-exports `effect` by bare specifier stays external naturally and is allowed.
|
|
381
379
|
|
|
382
|
-
`executeAlchemyEntry({ from, bundle, args, ... })` owns the repeated choreography: resolve the consumer's Alchemy CLI from `from`, bundle the entry, run that CLI under the current Node binary with the absolute bundled entry appended, and throw on spawn errors, signals, missing statuses, or non-zero exits. It returns only after status 0. Pass `bundledEntry` (absolute or cwd-relative) to
|
|
380
|
+
`executeAlchemyEntry({ from, bundle, args, ... })` owns the repeated choreography: resolve the consumer's Alchemy CLI from `from`, bundle the entry, run that CLI under the current Node binary with the absolute bundled entry appended, and throw on spawn errors, signals, missing statuses, or non-zero exits. It returns only after status 0. Options carry exactly one entry source. Pass `bundle` to bundle now, or `bundledEntry` (absolute or cwd-relative) to reuse a path from a prior `bundleAlchemyEntry` call. Passing both, or neither, does not type-check. Captured output (`stdio: "pipe"`) honors `maxBuffer` only when the caller supplies it (Node's `spawnSync` default is 1 MiB; pass `EXECUTE_ALCHEMY_ENTRY_MAX_BUFFER` for 64 MiB) and, when supplied, a `redact: (text: string) => string` hook applied to stdout and stderr before they are returned. Default `stdio: "inherit"` still streams the child unredacted. This package does not own a secret-name policy.
|
|
383
381
|
|
|
384
382
|
The caller still owns deploy/destroy/plan arguments, stage admission, credentials, environment shaping, destructive-plan guards, and convergence. Failure messages never repeat arguments or environment values.
|
|
385
383
|
|
|
@@ -406,7 +404,7 @@ import { previewProofLifecycle } from "@patronage/factory-ci";
|
|
|
406
404
|
Events:
|
|
407
405
|
|
|
408
406
|
- `begin` — the pre-deploy gate. Refuses a draft, a closed PR, a dirty or mismatched worktree, and any stage the disposable grammar does not own for this PR head. Does not create a registration.
|
|
409
|
-
- `register` — the same identity gate, plus passed smoke, passed convergence, and a passing three-way-bound evidence envelope at this head. Writes a registration with `
|
|
407
|
+
- `register` — the same identity gate, plus passed smoke, passed convergence, and a passing three-way-bound evidence envelope at this head. Writes a registration with `cleanup.outcome: "pending"`. Cleanup, smoke, and convergence status live only on their nested objects, so no top-level copy can diverge from them. Machine evidence is that envelope; this module never posts a PR comment.
|
|
410
408
|
- `cleanup` — records an explicit passed or failed outcome onto an existing registration. Cleanup is never inferred from whether the stage still exists. `recover` is the failed-to-resolved re-entry of this event.
|
|
411
409
|
|
|
412
410
|
Proof runs entirely on one side of GitHub's draft/ready boundary: both `begin` and `register` refuse `isDraft: true`. Undraft first, then prove. Callers still own deploy, destroy, credentials, smoke probes, envelope emission, and stack names.
|
package/dist/index.d.ts
CHANGED
|
@@ -53,6 +53,18 @@ declare const NODE_PNPM_ACTION_FAMILY_NODE24: {
|
|
|
53
53
|
readonly uses: "pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271";
|
|
54
54
|
};
|
|
55
55
|
};
|
|
56
|
+
/**
|
|
57
|
+
* The one `actions/upload-artifact` pin every workflow generator shares
|
|
58
|
+
* (#887). Two callers pinned two different commits of the same tag family;
|
|
59
|
+
* naming the pin here makes that drift impossible the same way the Node/pnpm
|
|
60
|
+
* family does. A caller passes this through `factoryWorkflow`'s
|
|
61
|
+
* `additionalActions.uploadArtifact` explicitly — the catalog names the pin,
|
|
62
|
+
* it does not inject the upload step.
|
|
63
|
+
*/
|
|
64
|
+
declare const UPLOAD_ARTIFACT: {
|
|
65
|
+
readonly tag: "v4.6.2";
|
|
66
|
+
readonly uses: "actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02";
|
|
67
|
+
};
|
|
56
68
|
//#endregion
|
|
57
69
|
//#region src/candidate-lifecycle.d.ts
|
|
58
70
|
/**
|
|
@@ -205,12 +217,10 @@ interface PreviewProofRegistration {
|
|
|
205
217
|
readonly outcome: PreviewProofCleanupStatus;
|
|
206
218
|
readonly runUrl?: string;
|
|
207
219
|
};
|
|
208
|
-
readonly cleanupStatus: PreviewProofCleanupStatus;
|
|
209
220
|
readonly convergence: {
|
|
210
221
|
readonly detail: string;
|
|
211
222
|
readonly status: "passed";
|
|
212
223
|
};
|
|
213
|
-
readonly convergenceStatus: "passed";
|
|
214
224
|
readonly headSha: string;
|
|
215
225
|
readonly pr: number;
|
|
216
226
|
readonly proof: {
|
|
@@ -223,7 +233,6 @@ interface PreviewProofRegistration {
|
|
|
223
233
|
readonly detail: string;
|
|
224
234
|
readonly outcome: "passed";
|
|
225
235
|
};
|
|
226
|
-
readonly smokeStatus: "passed";
|
|
227
236
|
readonly source: "local-self-certified";
|
|
228
237
|
readonly stack: string;
|
|
229
238
|
readonly stage: LocalPreviewStage;
|
|
@@ -518,22 +527,12 @@ declare const mintInstallationToken: (input: {
|
|
|
518
527
|
//#region src/execute-alchemy-entry.d.ts
|
|
519
528
|
/** Node's spawnSync default is 1 MiB; Alchemy plans routinely exceed that. */
|
|
520
529
|
declare const EXECUTE_ALCHEMY_ENTRY_MAX_BUFFER: number;
|
|
521
|
-
interface
|
|
530
|
+
interface ExecuteAlchemyEntryCommon {
|
|
522
531
|
/**
|
|
523
532
|
* CLI arguments before the bundled entry. The absolute entry path is always
|
|
524
533
|
* appended as the final argument.
|
|
525
534
|
*/
|
|
526
535
|
readonly args: readonly string[];
|
|
527
|
-
/**
|
|
528
|
-
* Bundle the consumer entry before execution. Required unless `bundledEntry`
|
|
529
|
-
* is set. Ignored when `bundledEntry` is set.
|
|
530
|
-
*/
|
|
531
|
-
readonly bundle?: BundleAlchemyEntryOptions;
|
|
532
|
-
/**
|
|
533
|
-
* Already-bundled entry path. When set, skips the internal bundle step and
|
|
534
|
-
* appends this path as the CLI's final argument.
|
|
535
|
-
*/
|
|
536
|
-
readonly bundledEntry?: string;
|
|
537
536
|
/** Child working directory. Defaults to the bundle root, then process.cwd. */
|
|
538
537
|
readonly cwd?: string;
|
|
539
538
|
/** Child environment. Defaults to process.env. */
|
|
@@ -556,6 +555,21 @@ interface ExecuteAlchemyEntryOptions {
|
|
|
556
555
|
/** Child stdio. Default "inherit". */
|
|
557
556
|
readonly stdio?: "inherit" | "pipe";
|
|
558
557
|
}
|
|
558
|
+
/**
|
|
559
|
+
* Exactly one entry source. The union refuses `bundle` and `bundledEntry`
|
|
560
|
+
* together, and refuses neither, so no runtime check is needed.
|
|
561
|
+
*/
|
|
562
|
+
type ExecuteAlchemyEntryOptions = ExecuteAlchemyEntryCommon & ({
|
|
563
|
+
/** Bundle the consumer entry before execution. */readonly bundle: BundleAlchemyEntryOptions;
|
|
564
|
+
readonly bundledEntry?: never;
|
|
565
|
+
} | {
|
|
566
|
+
readonly bundle?: never;
|
|
567
|
+
/**
|
|
568
|
+
* Already-bundled entry path. Skips the internal bundle step and
|
|
569
|
+
* appends this path as the CLI's final argument.
|
|
570
|
+
*/
|
|
571
|
+
readonly bundledEntry: string;
|
|
572
|
+
});
|
|
559
573
|
interface ExecuteAlchemyEntryResult {
|
|
560
574
|
readonly bundledEntry: string;
|
|
561
575
|
readonly stderr: string | null;
|
|
@@ -565,8 +579,8 @@ interface ExecuteAlchemyEntryResult {
|
|
|
565
579
|
* Bundle a consumer-owned Alchemy entry, resolve that consumer's Alchemy CLI,
|
|
566
580
|
* run it under the current Node binary, and return only on a successful exit.
|
|
567
581
|
*
|
|
568
|
-
*
|
|
569
|
-
* path. Captured stdout and stderr (`stdio: "pipe"`) can be redacted through
|
|
582
|
+
* Options carry exactly one entry source: `bundle` to bundle now, or
|
|
583
|
+
* `bundledEntry` to reuse an already-bundled path. Captured stdout and stderr (`stdio: "pipe"`) can be redacted through
|
|
570
584
|
* `redact` before they are returned; `stdio: "inherit"` still streams the
|
|
571
585
|
* child unredacted.
|
|
572
586
|
*
|
|
@@ -939,8 +953,13 @@ interface FactoryCandidateImpactWorkflow {
|
|
|
939
953
|
/** Outputs for a caller-owned decision job that subsequent jobs may consume. */
|
|
940
954
|
readonly decisionJobOutputs: Readonly<Record<string, string>>;
|
|
941
955
|
readonly decisionStep: WorkflowStep;
|
|
942
|
-
/**
|
|
943
|
-
|
|
956
|
+
/**
|
|
957
|
+
* A fail-open cross-job condition: only an explicit usable withdrawal skips
|
|
958
|
+
* work. The decision job id is required.
|
|
959
|
+
*/
|
|
960
|
+
readonly demandedIf: (targetName: string, decisionJob: string) => string;
|
|
961
|
+
/** The same fail-open condition for a step in the decision job. */
|
|
962
|
+
readonly demandedIfAtStep: (targetName: string) => string;
|
|
944
963
|
readonly targetOutputs: Readonly<Record<string, string>>;
|
|
945
964
|
}
|
|
946
965
|
/**
|
|
@@ -1010,6 +1029,13 @@ declare const factoryLifecycleContractLane: (declaration?: FactoryLifecycleContr
|
|
|
1010
1029
|
* `gh` and parses the emitted payload with `validateMergeFreezeState`, so
|
|
1011
1030
|
* writer and reader cannot drift silently.
|
|
1012
1031
|
*
|
|
1032
|
+
* Those two API calls are what the reader's two settling phases observe
|
|
1033
|
+
* (#890). Before the POST the merge target carries no generation at all, which
|
|
1034
|
+
* a reader reports as `awaiting-generation` while the pushed hosted verify run
|
|
1035
|
+
* is still pending. Between the POST and the PATCH the generation exists with
|
|
1036
|
+
* no `output.text`, which a reader reports as `running-generation`. Only the
|
|
1037
|
+
* PATCH writes a verdict, so neither phase is one.
|
|
1038
|
+
*
|
|
1013
1039
|
* Failure posture is deliberately fail-closed and the opposite of the
|
|
1014
1040
|
* proof-reuse gate's fail-open shell: this step runs under the runner's
|
|
1015
1041
|
* default `bash -e {0}`, so any API refusal aborts the step, the generation
|
|
@@ -1213,8 +1239,13 @@ interface FactoryProductionImpactWorkflow {
|
|
|
1213
1239
|
/** Outputs for a caller-owned decision job that subsequent jobs may consume. */
|
|
1214
1240
|
readonly decisionJobOutputs: Readonly<Record<string, string>>;
|
|
1215
1241
|
readonly decisionStep: WorkflowStep;
|
|
1216
|
-
/**
|
|
1217
|
-
|
|
1242
|
+
/**
|
|
1243
|
+
* A fail-open cross-job condition: only an explicit usable withdrawal skips
|
|
1244
|
+
* work. The decision job id is required.
|
|
1245
|
+
*/
|
|
1246
|
+
readonly demandedIf: (targetName: string, decisionJob: string) => string;
|
|
1247
|
+
/** The same fail-open condition for a step in the decision job. */
|
|
1248
|
+
readonly demandedIfAtStep: (targetName: string) => string;
|
|
1218
1249
|
readonly targetOutputs: Readonly<Record<string, string>>;
|
|
1219
1250
|
}
|
|
1220
1251
|
/**
|
|
@@ -1237,7 +1268,12 @@ interface FactoryPushIdentityEnvelope {
|
|
|
1237
1268
|
}
|
|
1238
1269
|
type FactoryPushIdentityDisposition = "refused" | "usable";
|
|
1239
1270
|
interface FactoryPushIdentityProducerOptions {
|
|
1240
|
-
/**
|
|
1271
|
+
/**
|
|
1272
|
+
* Explicit caller-owned upload-artifact pin from its workflow artifact.
|
|
1273
|
+
* Pass factory-ci's `UPLOAD_ARTIFACT` catalog export (or another
|
|
1274
|
+
* `actions/upload-artifact` pin); the caller wires the step, the catalog
|
|
1275
|
+
* only names the pin.
|
|
1276
|
+
*/
|
|
1241
1277
|
readonly uploadArtifact: PinnedAction;
|
|
1242
1278
|
/** Optional consumer trigger policy combined with the required push event. */
|
|
1243
1279
|
readonly if?: string;
|
|
@@ -1556,4 +1592,4 @@ declare const assertWorkflowShellParses: (yaml: string, options: {
|
|
|
1556
1592
|
readonly source: string;
|
|
1557
1593
|
}) => void;
|
|
1558
1594
|
//#endregion
|
|
1559
|
-
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, 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 };
|
|
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 };
|
package/dist/index.js
CHANGED
|
@@ -32,6 +32,18 @@ const NODE_PNPM_ACTION_FAMILY_NODE24 = {
|
|
|
32
32
|
uses: "pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271"
|
|
33
33
|
}
|
|
34
34
|
};
|
|
35
|
+
/**
|
|
36
|
+
* The one `actions/upload-artifact` pin every workflow generator shares
|
|
37
|
+
* (#887). Two callers pinned two different commits of the same tag family;
|
|
38
|
+
* naming the pin here makes that drift impossible the same way the Node/pnpm
|
|
39
|
+
* family does. A caller passes this through `factoryWorkflow`'s
|
|
40
|
+
* `additionalActions.uploadArtifact` explicitly — the catalog names the pin,
|
|
41
|
+
* it does not inject the upload step.
|
|
42
|
+
*/
|
|
43
|
+
const UPLOAD_ARTIFACT = {
|
|
44
|
+
tag: "v4.6.2",
|
|
45
|
+
uses: "actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02"
|
|
46
|
+
};
|
|
35
47
|
//#endregion
|
|
36
48
|
//#region src/candidate-lifecycle.ts
|
|
37
49
|
/**
|
|
@@ -251,12 +263,10 @@ const transition = (state, event) => {
|
|
|
251
263
|
evidence: [],
|
|
252
264
|
outcome: "pending"
|
|
253
265
|
},
|
|
254
|
-
cleanupStatus: "pending",
|
|
255
266
|
convergence: {
|
|
256
267
|
detail: event.convergence.detail,
|
|
257
268
|
status: "passed"
|
|
258
269
|
},
|
|
259
|
-
convergenceStatus: "passed",
|
|
260
270
|
headSha: event.candidate.headSha,
|
|
261
271
|
pr: event.candidate.pr,
|
|
262
272
|
proof: {
|
|
@@ -269,7 +279,6 @@ const transition = (state, event) => {
|
|
|
269
279
|
detail: event.smoke.detail,
|
|
270
280
|
outcome: "passed"
|
|
271
281
|
},
|
|
272
|
-
smokeStatus: "passed",
|
|
273
282
|
source: "local-self-certified",
|
|
274
283
|
stack: event.stack,
|
|
275
284
|
stage,
|
|
@@ -288,12 +297,11 @@ const transition = (state, event) => {
|
|
|
288
297
|
};
|
|
289
298
|
return replaceRegistration$1(state, {
|
|
290
299
|
...existing,
|
|
291
|
-
cleanup
|
|
292
|
-
cleanupStatus: event.outcome
|
|
300
|
+
cleanup
|
|
293
301
|
});
|
|
294
302
|
};
|
|
295
303
|
const recover = (state, event) => {
|
|
296
|
-
if (state.registrations.find((row) => row.pr === event.pr && row.stack === event.stack && row.stage === event.stage)?.
|
|
304
|
+
if (state.registrations.find((row) => row.pr === event.pr && row.stack === event.stack && row.stage === event.stage)?.cleanup.outcome !== "failed") throw new Error("Preview proof recovery re-enters cleanup only from a failed outcome.");
|
|
297
305
|
return transition(state, event);
|
|
298
306
|
};
|
|
299
307
|
const previewProofLifecycle = {
|
|
@@ -586,7 +594,7 @@ const COMMAND_IDENTITY_MAX_LENGTH = 120;
|
|
|
586
594
|
* cannot subtract hosted work. This is intentionally fail-closed and moves in
|
|
587
595
|
* lockstep with the proof/check-payload producer.
|
|
588
596
|
*/
|
|
589
|
-
const TRUSTED_IMPACT_STAMP_VERSION =
|
|
597
|
+
const TRUSTED_IMPACT_STAMP_VERSION = 4;
|
|
590
598
|
const isProofReuseCommand = (value) => {
|
|
591
599
|
if (!(value && typeof value === "object")) return false;
|
|
592
600
|
const entry = value;
|
|
@@ -779,7 +787,9 @@ def releaseAuthorized($release; $commands; $stamp):
|
|
|
779
787
|
type == "object"
|
|
780
788
|
and (.name | nonemptyString)
|
|
781
789
|
and (.basis | nonemptyString)
|
|
782
|
-
and ((.impact == "affected") or (.impact == "not-affected"))
|
|
790
|
+
and ((.impact == "affected") or (.impact == "not-affected"))
|
|
791
|
+
and ((.subscribedPaths | type) == "array")
|
|
792
|
+
and (.subscribedPaths | all(.[]; type == "string"))))
|
|
783
793
|
and ($stamp.targets | distinctNames)
|
|
784
794
|
and (([$releases[].name] - $executed | length) == ($releases | length))
|
|
785
795
|
and ($releases | all(.[]; releaseAuthorized(.; $commands; $stamp)))
|
|
@@ -1280,7 +1290,7 @@ const readIdentity = (value) => {
|
|
|
1280
1290
|
const readCleanup = (value) => {
|
|
1281
1291
|
if (!isRecord$1(value.cleanup) || !isCleanupStatus(value.cleanup.outcome)) return null;
|
|
1282
1292
|
const evidence = readStringArray(value.cleanup.evidence);
|
|
1283
|
-
if (evidence === null
|
|
1293
|
+
if (evidence === null) return null;
|
|
1284
1294
|
return typeof value.cleanup.runUrl === "string" && value.cleanup.runUrl.length > 0 ? {
|
|
1285
1295
|
evidence,
|
|
1286
1296
|
outcome: value.cleanup.outcome,
|
|
@@ -1292,7 +1302,7 @@ const readCleanup = (value) => {
|
|
|
1292
1302
|
};
|
|
1293
1303
|
const readPassingChecks = (value) => {
|
|
1294
1304
|
if (!isRecord$1(value.convergence) || !isRecord$1(value.smoke)) return null;
|
|
1295
|
-
if (value.convergence.status !== "passed" ||
|
|
1305
|
+
if (value.convergence.status !== "passed" || typeof value.convergence.detail !== "string" || value.smoke.outcome !== "passed" || typeof value.smoke.detail !== "string") return null;
|
|
1296
1306
|
return {
|
|
1297
1307
|
convergence: {
|
|
1298
1308
|
detail: value.convergence.detail,
|
|
@@ -1327,14 +1337,11 @@ const readPreviewProofRegistration = (value) => {
|
|
|
1327
1337
|
if (identity === null || cleanup === null || passing === null || proof === null) return null;
|
|
1328
1338
|
return {
|
|
1329
1339
|
cleanup,
|
|
1330
|
-
cleanupStatus: cleanup.outcome,
|
|
1331
1340
|
convergence: passing.convergence,
|
|
1332
|
-
convergenceStatus: "passed",
|
|
1333
1341
|
headSha: identity.headSha,
|
|
1334
1342
|
pr: identity.pr,
|
|
1335
1343
|
proof,
|
|
1336
1344
|
smoke: passing.smoke,
|
|
1337
|
-
smokeStatus: "passed",
|
|
1338
1345
|
source: "local-self-certified",
|
|
1339
1346
|
stack: identity.stack,
|
|
1340
1347
|
stage: identity.stage,
|
|
@@ -1492,7 +1499,7 @@ const registrationFromCheckRun = (run, expectedHeadSha) => {
|
|
|
1492
1499
|
const checkRunOutput = (registration) => {
|
|
1493
1500
|
const title = `Preview proof ${registration.stack}/${registration.stage}`.slice(0, 255);
|
|
1494
1501
|
return {
|
|
1495
|
-
summary: `cleanup ${registration.
|
|
1502
|
+
summary: `cleanup ${registration.cleanup.outcome}`,
|
|
1496
1503
|
text: JSON.stringify({
|
|
1497
1504
|
kind: INVENTORY_KIND,
|
|
1498
1505
|
registration,
|
|
@@ -1852,9 +1859,8 @@ const factoryWorkflow = (options) => {
|
|
|
1852
1859
|
const EXECUTE_ALCHEMY_ENTRY_MAX_BUFFER = 64 * 1024 * 1024;
|
|
1853
1860
|
let spawnImplementation = spawnSync;
|
|
1854
1861
|
const resolveBundledEntry = async (options) => {
|
|
1855
|
-
if (options.bundledEntry
|
|
1856
|
-
|
|
1857
|
-
return await bundleAlchemyEntry(options.bundle);
|
|
1862
|
+
if (options.bundledEntry === void 0) return await bundleAlchemyEntry(options.bundle);
|
|
1863
|
+
return path.resolve(options.cwd ?? process.cwd(), options.bundledEntry);
|
|
1858
1864
|
};
|
|
1859
1865
|
const captured = (text, redact) => {
|
|
1860
1866
|
if (text === null || redact === void 0) return text;
|
|
@@ -1864,8 +1870,8 @@ const captured = (text, redact) => {
|
|
|
1864
1870
|
* Bundle a consumer-owned Alchemy entry, resolve that consumer's Alchemy CLI,
|
|
1865
1871
|
* run it under the current Node binary, and return only on a successful exit.
|
|
1866
1872
|
*
|
|
1867
|
-
*
|
|
1868
|
-
* path. Captured stdout and stderr (`stdio: "pipe"`) can be redacted through
|
|
1873
|
+
* Options carry exactly one entry source: `bundle` to bundle now, or
|
|
1874
|
+
* `bundledEntry` to reuse an already-bundled path. Captured stdout and stderr (`stdio: "pipe"`) can be redacted through
|
|
1869
1875
|
* `redact` before they are returned; `stdio: "inherit"` still streams the
|
|
1870
1876
|
* child unredacted.
|
|
1871
1877
|
*
|
|
@@ -1971,6 +1977,24 @@ const factoryProofReuseSummaryStep = (options) => Object.freeze({
|
|
|
1971
1977
|
run: factoryProofReuseSummaryScript(options)
|
|
1972
1978
|
});
|
|
1973
1979
|
//#endregion
|
|
1980
|
+
//#region src/impact-demand-condition.ts
|
|
1981
|
+
const impactDemandConditions = (options) => {
|
|
1982
|
+
const targetOutput = (targetName) => {
|
|
1983
|
+
const output = options.targetOutputs[targetName];
|
|
1984
|
+
if (output === void 0) throw new Error(`Unknown ${options.label} impact target "${targetName}".`);
|
|
1985
|
+
return output;
|
|
1986
|
+
};
|
|
1987
|
+
const condition = (source, outcomeKey, output) => `${source}.${outcomeKey} != 'success' || ${source}.outputs.${options.decisionOutput} != 'usable' || ${source}.outputs.${output} != 'withdrawn'`;
|
|
1988
|
+
return Object.freeze({
|
|
1989
|
+
demandedIf: (targetName, decisionJob) => {
|
|
1990
|
+
const output = targetOutput(targetName);
|
|
1991
|
+
if (decisionJob.trim().length === 0) throw new Error(`A cross-job ${options.label} impact condition requires the decision job id.`);
|
|
1992
|
+
return `always() && (${condition(`needs.${decisionJob}`, "result", output)})`;
|
|
1993
|
+
},
|
|
1994
|
+
demandedIfAtStep: (targetName) => condition(`steps.${options.stepId}`, "outcome", targetOutput(targetName))
|
|
1995
|
+
});
|
|
1996
|
+
};
|
|
1997
|
+
//#endregion
|
|
1974
1998
|
//#region src/production-impact-workflow.ts
|
|
1975
1999
|
const FACTORY_PRODUCTION_IMPACT_STEP_ID = "production_impact";
|
|
1976
2000
|
const FACTORY_PRODUCTION_IMPACT_DECISION_OUTPUT = "decision";
|
|
@@ -2007,6 +2031,12 @@ const factoryProductionImpactWorkflow = (options) => {
|
|
|
2007
2031
|
name: "Classify production impact",
|
|
2008
2032
|
run: `${cli} production:impact --before "$FACTORY_BEFORE_SHA" --after "$FACTORY_AFTER_SHA" --github-output "$GITHUB_OUTPUT" --github-summary "$GITHUB_STEP_SUMMARY"${profile}`
|
|
2009
2033
|
};
|
|
2034
|
+
const conditions = impactDemandConditions({
|
|
2035
|
+
decisionOutput: FACTORY_PRODUCTION_IMPACT_DECISION_OUTPUT,
|
|
2036
|
+
label: "production",
|
|
2037
|
+
stepId: FACTORY_PRODUCTION_IMPACT_STEP_ID,
|
|
2038
|
+
targetOutputs
|
|
2039
|
+
});
|
|
2010
2040
|
return Object.freeze({
|
|
2011
2041
|
decisionJobOutputs: Object.freeze({
|
|
2012
2042
|
basis: `\${{ steps.${FACTORY_PRODUCTION_IMPACT_STEP_ID}.outputs.${FACTORY_PRODUCTION_IMPACT_BASIS_OUTPUT} }}`,
|
|
@@ -2015,13 +2045,8 @@ const factoryProductionImpactWorkflow = (options) => {
|
|
|
2015
2045
|
...Object.fromEntries(Object.values(targetOutputs).map((output) => [output, `\${{ steps.${FACTORY_PRODUCTION_IMPACT_STEP_ID}.outputs.${output} }}`]))
|
|
2016
2046
|
}),
|
|
2017
2047
|
decisionStep: Object.freeze(decisionStep),
|
|
2018
|
-
demandedIf:
|
|
2019
|
-
|
|
2020
|
-
if (output === void 0) throw new Error(`Unknown production impact target "${targetName}".`);
|
|
2021
|
-
const source = decisionJob ? `needs.${decisionJob}` : `steps.${FACTORY_PRODUCTION_IMPACT_STEP_ID}`;
|
|
2022
|
-
const condition = `${source}.${decisionJob ? "result" : "outcome"} != 'success' || ${source}.outputs.${FACTORY_PRODUCTION_IMPACT_DECISION_OUTPUT} != 'usable' || ${source}.outputs.${output} != 'withdrawn'`;
|
|
2023
|
-
return decisionJob ? `always() && (${condition})` : condition;
|
|
2024
|
-
},
|
|
2048
|
+
demandedIf: conditions.demandedIf,
|
|
2049
|
+
demandedIfAtStep: conditions.demandedIfAtStep,
|
|
2025
2050
|
targetOutputs: Object.freeze(targetOutputs)
|
|
2026
2051
|
});
|
|
2027
2052
|
};
|
|
@@ -2064,6 +2089,12 @@ const factoryCandidateImpactWorkflow = (options) => {
|
|
|
2064
2089
|
name: "Classify candidate impact",
|
|
2065
2090
|
run: `${cli} candidate:impact --base "$FACTORY_BASE_SHA" --head "$FACTORY_HEAD_SHA" --github-output "$GITHUB_OUTPUT" --github-summary "$GITHUB_STEP_SUMMARY"${profile}`
|
|
2066
2091
|
};
|
|
2092
|
+
const conditions = impactDemandConditions({
|
|
2093
|
+
decisionOutput: FACTORY_CANDIDATE_IMPACT_DECISION_OUTPUT,
|
|
2094
|
+
label: "candidate",
|
|
2095
|
+
stepId: FACTORY_CANDIDATE_IMPACT_STEP_ID,
|
|
2096
|
+
targetOutputs
|
|
2097
|
+
});
|
|
2067
2098
|
return Object.freeze({
|
|
2068
2099
|
decisionJobOutputs: Object.freeze({
|
|
2069
2100
|
basis: `\${{ steps.${FACTORY_CANDIDATE_IMPACT_STEP_ID}.outputs.${FACTORY_CANDIDATE_IMPACT_BASIS_OUTPUT} }}`,
|
|
@@ -2073,13 +2104,8 @@ const factoryCandidateImpactWorkflow = (options) => {
|
|
|
2073
2104
|
...Object.fromEntries(Object.values(targetOutputs).map((output) => [output, `\${{ steps.${FACTORY_CANDIDATE_IMPACT_STEP_ID}.outputs.${output} }}`]))
|
|
2074
2105
|
}),
|
|
2075
2106
|
decisionStep: Object.freeze(decisionStep),
|
|
2076
|
-
demandedIf:
|
|
2077
|
-
|
|
2078
|
-
if (output === void 0) throw new Error(`Unknown candidate impact target "${targetName}".`);
|
|
2079
|
-
const source = decisionJob ? `needs.${decisionJob}` : `steps.${FACTORY_CANDIDATE_IMPACT_STEP_ID}`;
|
|
2080
|
-
const condition = `${source}.${decisionJob ? "result" : "outcome"} != 'success' || ${source}.outputs.${FACTORY_CANDIDATE_IMPACT_DECISION_OUTPUT} != 'usable' || ${source}.outputs.${output} != 'withdrawn'`;
|
|
2081
|
-
return decisionJob ? `always() && (${condition})` : condition;
|
|
2082
|
-
},
|
|
2107
|
+
demandedIf: conditions.demandedIf,
|
|
2108
|
+
demandedIfAtStep: conditions.demandedIfAtStep,
|
|
2083
2109
|
targetOutputs: Object.freeze(targetOutputs)
|
|
2084
2110
|
});
|
|
2085
2111
|
};
|
|
@@ -2172,6 +2198,13 @@ const factoryLifecycleContractLane = (declaration) => {
|
|
|
2172
2198
|
* `gh` and parses the emitted payload with `validateMergeFreezeState`, so
|
|
2173
2199
|
* writer and reader cannot drift silently.
|
|
2174
2200
|
*
|
|
2201
|
+
* Those two API calls are what the reader's two settling phases observe
|
|
2202
|
+
* (#890). Before the POST the merge target carries no generation at all, which
|
|
2203
|
+
* a reader reports as `awaiting-generation` while the pushed hosted verify run
|
|
2204
|
+
* is still pending. Between the POST and the PATCH the generation exists with
|
|
2205
|
+
* no `output.text`, which a reader reports as `running-generation`. Only the
|
|
2206
|
+
* PATCH writes a verdict, so neither phase is one.
|
|
2207
|
+
*
|
|
2175
2208
|
* Failure posture is deliberately fail-closed and the opposite of the
|
|
2176
2209
|
* proof-reuse gate's fail-open shell: this step runs under the runner's
|
|
2177
2210
|
* default `bash -e {0}`, so any API refusal aborts the step, the generation
|
|
@@ -2428,6 +2461,10 @@ const factoryPreviewCleanupTopology = (options) => {
|
|
|
2428
2461
|
};
|
|
2429
2462
|
//#endregion
|
|
2430
2463
|
//#region src/push-identity-workflow.ts
|
|
2464
|
+
/** `owner/repo` slice of the catalog's `UPLOAD_ARTIFACT` pin, derived rather
|
|
2465
|
+
* than repeated as a literal, so a repository rename here cannot drift from
|
|
2466
|
+
* the pin it validates against. */
|
|
2467
|
+
const UPLOAD_ARTIFACT_REPOSITORY = UPLOAD_ARTIFACT.uses.slice(0, UPLOAD_ARTIFACT.uses.indexOf("@"));
|
|
2431
2468
|
const FACTORY_PUSH_IDENTITY_SCHEMA_VERSION = 1;
|
|
2432
2469
|
/**
|
|
2433
2470
|
* The artifact prefix and the five step IDs are module-private. Alpha.13
|
|
@@ -2683,7 +2720,7 @@ printf 'disposition=%s\nreason=%s\nbefore=%s\nafter=%s\nprovenance=%s\n' \
|
|
|
2683
2720
|
* result when transport is unavailable. Place these steps after verification.
|
|
2684
2721
|
*/
|
|
2685
2722
|
const factoryPushIdentityProducer = (options) => {
|
|
2686
|
-
assertPinnedAction("uploadArtifact", options.uploadArtifact,
|
|
2723
|
+
assertPinnedAction("uploadArtifact", options.uploadArtifact, UPLOAD_ARTIFACT_REPOSITORY);
|
|
2687
2724
|
const condition = options.if ? `github.event_name == 'push' && (${options.if})` : "github.event_name == 'push'";
|
|
2688
2725
|
const name = artifactName(expression("github.run_id"));
|
|
2689
2726
|
return Object.freeze({
|
|
@@ -3499,4 +3536,4 @@ const assertWorkflowShellParses = (yaml, options) => {
|
|
|
3499
3536
|
throw new Error(`${options.source} emits shell bash cannot parse; the runner would treat it as a no-op:\n${detail}`);
|
|
3500
3537
|
};
|
|
3501
3538
|
//#endregion
|
|
3502
|
-
export { EXECUTE_ALCHEMY_ENTRY_MAX_BUFFER, 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, GitHubApiError, NODE_PNPM_ACTION_FAMILY_NODE24, PREVIEW_PROOF_INVENTORY_CHECK_NAME, PREVIEW_PROOF_INVENTORY_LIST_PERMISSIONS, PreviewProofTransportError, VITEST_PROFILE_SCHEMA_VERSION, VITEST_PROFILE_TOOL, VitestProfileError, 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 };
|
|
3539
|
+
export { EXECUTE_ALCHEMY_ENTRY_MAX_BUFFER, 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, GitHubApiError, NODE_PNPM_ACTION_FAMILY_NODE24, PREVIEW_PROOF_INVENTORY_CHECK_NAME, PREVIEW_PROOF_INVENTORY_LIST_PERMISSIONS, PreviewProofTransportError, UPLOAD_ARTIFACT, VITEST_PROFILE_SCHEMA_VERSION, VITEST_PROFILE_TOOL, VitestProfileError, 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 };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@patronage/factory-ci",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.23",
|
|
4
4
|
"description": "Deep CI and deploy building blocks for Patronage factory projects: workflow source artifacts, hosted diff classification, Alchemy entry execution, and disposable-stage semantics",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"alchemy",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"node": "^24.0.0"
|
|
49
49
|
},
|
|
50
50
|
"scripts": {
|
|
51
|
-
"cleanup:check": "knip --files --dependencies",
|
|
51
|
+
"cleanup:check": "knip --files --dependencies --exports",
|
|
52
52
|
"prebuild": "bash ../scripts/ensure-worktree-bootstrap.sh",
|
|
53
53
|
"build": "tsdown",
|
|
54
54
|
"precheck": "bash ../scripts/ensure-worktree-bootstrap.sh",
|
package/src/actions.ts
CHANGED
|
@@ -52,3 +52,16 @@ export const NODE_PNPM_ACTION_FAMILY_NODE24 = {
|
|
|
52
52
|
uses: "pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271",
|
|
53
53
|
},
|
|
54
54
|
} as const satisfies NodePnpmActionFamily;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The one `actions/upload-artifact` pin every workflow generator shares
|
|
58
|
+
* (#887). Two callers pinned two different commits of the same tag family;
|
|
59
|
+
* naming the pin here makes that drift impossible the same way the Node/pnpm
|
|
60
|
+
* family does. A caller passes this through `factoryWorkflow`'s
|
|
61
|
+
* `additionalActions.uploadArtifact` explicitly — the catalog names the pin,
|
|
62
|
+
* it does not inject the upload step.
|
|
63
|
+
*/
|
|
64
|
+
export const UPLOAD_ARTIFACT = {
|
|
65
|
+
tag: "v4.6.2",
|
|
66
|
+
uses: "actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02",
|
|
67
|
+
} as const satisfies PinnedAction;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { WorkflowStep } from "./factory-workflow.ts";
|
|
2
|
+
import { impactDemandConditions } from "./impact-demand-condition.ts";
|
|
2
3
|
import { productionImpactTargetOutput } from "./production-impact-workflow.ts";
|
|
3
4
|
|
|
4
5
|
export const FACTORY_CANDIDATE_IMPACT_STEP_ID = "candidate_impact";
|
|
@@ -23,8 +24,13 @@ export interface FactoryCandidateImpactWorkflow {
|
|
|
23
24
|
/** Outputs for a caller-owned decision job that subsequent jobs may consume. */
|
|
24
25
|
readonly decisionJobOutputs: Readonly<Record<string, string>>;
|
|
25
26
|
readonly decisionStep: WorkflowStep;
|
|
26
|
-
/**
|
|
27
|
-
|
|
27
|
+
/**
|
|
28
|
+
* A fail-open cross-job condition: only an explicit usable withdrawal skips
|
|
29
|
+
* work. The decision job id is required.
|
|
30
|
+
*/
|
|
31
|
+
readonly demandedIf: (targetName: string, decisionJob: string) => string;
|
|
32
|
+
/** The same fail-open condition for a step in the decision job. */
|
|
33
|
+
readonly demandedIfAtStep: (targetName: string) => string;
|
|
28
34
|
readonly targetOutputs: Readonly<Record<string, string>>;
|
|
29
35
|
}
|
|
30
36
|
|
|
@@ -74,6 +80,13 @@ export const factoryCandidateImpactWorkflow = (
|
|
|
74
80
|
run: `${cli} candidate:impact --base "$FACTORY_BASE_SHA" --head "$FACTORY_HEAD_SHA" --github-output "$GITHUB_OUTPUT" --github-summary "$GITHUB_STEP_SUMMARY"${profile}`,
|
|
75
81
|
};
|
|
76
82
|
|
|
83
|
+
const conditions = impactDemandConditions({
|
|
84
|
+
decisionOutput: FACTORY_CANDIDATE_IMPACT_DECISION_OUTPUT,
|
|
85
|
+
label: "candidate",
|
|
86
|
+
stepId: FACTORY_CANDIDATE_IMPACT_STEP_ID,
|
|
87
|
+
targetOutputs,
|
|
88
|
+
});
|
|
89
|
+
|
|
77
90
|
return Object.freeze({
|
|
78
91
|
decisionJobOutputs: Object.freeze({
|
|
79
92
|
basis: `\${{ steps.${FACTORY_CANDIDATE_IMPACT_STEP_ID}.outputs.${FACTORY_CANDIDATE_IMPACT_BASIS_OUTPUT} }}`,
|
|
@@ -88,18 +101,8 @@ export const factoryCandidateImpactWorkflow = (
|
|
|
88
101
|
),
|
|
89
102
|
}),
|
|
90
103
|
decisionStep: Object.freeze(decisionStep),
|
|
91
|
-
demandedIf:
|
|
92
|
-
|
|
93
|
-
if (output === undefined) {
|
|
94
|
-
throw new Error(`Unknown candidate impact target "${targetName}".`);
|
|
95
|
-
}
|
|
96
|
-
const source = decisionJob
|
|
97
|
-
? `needs.${decisionJob}`
|
|
98
|
-
: `steps.${FACTORY_CANDIDATE_IMPACT_STEP_ID}`;
|
|
99
|
-
const outcome = decisionJob ? "result" : "outcome";
|
|
100
|
-
const condition = `${source}.${outcome} != 'success' || ${source}.outputs.${FACTORY_CANDIDATE_IMPACT_DECISION_OUTPUT} != 'usable' || ${source}.outputs.${output} != 'withdrawn'`;
|
|
101
|
-
return decisionJob ? `always() && (${condition})` : condition;
|
|
102
|
-
},
|
|
104
|
+
demandedIf: conditions.demandedIf,
|
|
105
|
+
demandedIfAtStep: conditions.demandedIfAtStep,
|
|
103
106
|
targetOutputs: Object.freeze(targetOutputs),
|
|
104
107
|
});
|
|
105
108
|
};
|
|
@@ -27,22 +27,12 @@ export const setExecuteAlchemyEntrySpawnForTests = (
|
|
|
27
27
|
spawnImplementation = next ?? spawnSync;
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
interface ExecuteAlchemyEntryCommon {
|
|
31
31
|
/**
|
|
32
32
|
* CLI arguments before the bundled entry. The absolute entry path is always
|
|
33
33
|
* appended as the final argument.
|
|
34
34
|
*/
|
|
35
35
|
readonly args: readonly string[];
|
|
36
|
-
/**
|
|
37
|
-
* Bundle the consumer entry before execution. Required unless `bundledEntry`
|
|
38
|
-
* is set. Ignored when `bundledEntry` is set.
|
|
39
|
-
*/
|
|
40
|
-
readonly bundle?: BundleAlchemyEntryOptions;
|
|
41
|
-
/**
|
|
42
|
-
* Already-bundled entry path. When set, skips the internal bundle step and
|
|
43
|
-
* appends this path as the CLI's final argument.
|
|
44
|
-
*/
|
|
45
|
-
readonly bundledEntry?: string;
|
|
46
36
|
/** Child working directory. Defaults to the bundle root, then process.cwd. */
|
|
47
37
|
readonly cwd?: string;
|
|
48
38
|
/** Child environment. Defaults to process.env. */
|
|
@@ -66,6 +56,27 @@ export interface ExecuteAlchemyEntryOptions {
|
|
|
66
56
|
readonly stdio?: "inherit" | "pipe";
|
|
67
57
|
}
|
|
68
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Exactly one entry source. The union refuses `bundle` and `bundledEntry`
|
|
61
|
+
* together, and refuses neither, so no runtime check is needed.
|
|
62
|
+
*/
|
|
63
|
+
export type ExecuteAlchemyEntryOptions = ExecuteAlchemyEntryCommon &
|
|
64
|
+
(
|
|
65
|
+
| {
|
|
66
|
+
/** Bundle the consumer entry before execution. */
|
|
67
|
+
readonly bundle: BundleAlchemyEntryOptions;
|
|
68
|
+
readonly bundledEntry?: never;
|
|
69
|
+
}
|
|
70
|
+
| {
|
|
71
|
+
readonly bundle?: never;
|
|
72
|
+
/**
|
|
73
|
+
* Already-bundled entry path. Skips the internal bundle step and
|
|
74
|
+
* appends this path as the CLI's final argument.
|
|
75
|
+
*/
|
|
76
|
+
readonly bundledEntry: string;
|
|
77
|
+
}
|
|
78
|
+
);
|
|
79
|
+
|
|
69
80
|
export interface ExecuteAlchemyEntryResult {
|
|
70
81
|
readonly bundledEntry: string;
|
|
71
82
|
readonly stderr: string | null;
|
|
@@ -75,16 +86,10 @@ export interface ExecuteAlchemyEntryResult {
|
|
|
75
86
|
const resolveBundledEntry = async (
|
|
76
87
|
options: ExecuteAlchemyEntryOptions
|
|
77
88
|
): Promise<string> => {
|
|
78
|
-
if (options.bundledEntry
|
|
79
|
-
return
|
|
80
|
-
options.cwd ?? options.bundle?.absWorkingDir ?? process.cwd(),
|
|
81
|
-
options.bundledEntry
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
if (options.bundle === undefined) {
|
|
85
|
-
throw new Error("executeAlchemyEntry requires bundle or bundledEntry.");
|
|
89
|
+
if (options.bundledEntry === undefined) {
|
|
90
|
+
return await bundleAlchemyEntry(options.bundle);
|
|
86
91
|
}
|
|
87
|
-
return
|
|
92
|
+
return path.resolve(options.cwd ?? process.cwd(), options.bundledEntry);
|
|
88
93
|
};
|
|
89
94
|
|
|
90
95
|
const captured = (
|
|
@@ -101,8 +106,8 @@ const captured = (
|
|
|
101
106
|
* Bundle a consumer-owned Alchemy entry, resolve that consumer's Alchemy CLI,
|
|
102
107
|
* run it under the current Node binary, and return only on a successful exit.
|
|
103
108
|
*
|
|
104
|
-
*
|
|
105
|
-
* path. Captured stdout and stderr (`stdio: "pipe"`) can be redacted through
|
|
109
|
+
* Options carry exactly one entry source: `bundle` to bundle now, or
|
|
110
|
+
* `bundledEntry` to reuse an already-bundled path. Captured stdout and stderr (`stdio: "pipe"`) can be redacted through
|
|
106
111
|
* `redact` before they are returned; `stdio: "inherit"` still streams the
|
|
107
112
|
* child unredacted.
|
|
108
113
|
*
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-target withdrawal conditions for the candidate and production impact
|
|
3
|
+
* routing seams. Both seams emit the same condition text, so one
|
|
4
|
+
* implementation owns it.
|
|
5
|
+
*
|
|
6
|
+
* A cross-job condition and a same-job condition read different GitHub
|
|
7
|
+
* expression sources. `demandedIf` requires the decision job id and reads
|
|
8
|
+
* `needs.<job>`; `demandedIfAtStep` reads `steps.<stepId>` and takes no job
|
|
9
|
+
* id. The caller picks the seam by name, so an omitted job id can no longer
|
|
10
|
+
* emit a same-job condition where a cross-job condition was meant.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
interface ImpactDemandConditionOptions {
|
|
14
|
+
/** Output key that carries the decision verdict. */
|
|
15
|
+
readonly decisionOutput: string;
|
|
16
|
+
/** Seam name used in the unknown-target error. */
|
|
17
|
+
readonly label: string;
|
|
18
|
+
/** Id of the decision step inside the decision job. */
|
|
19
|
+
readonly stepId: string;
|
|
20
|
+
/** Target name to GitHub output key. */
|
|
21
|
+
readonly targetOutputs: Readonly<Record<string, string>>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface ImpactDemandConditions {
|
|
25
|
+
/**
|
|
26
|
+
* Cross-job condition for a job that lists the decision job in `needs`.
|
|
27
|
+
* The decision job id is required.
|
|
28
|
+
*/
|
|
29
|
+
readonly demandedIf: (targetName: string, decisionJob: string) => string;
|
|
30
|
+
/** Same-job condition for a step that follows the decision step. */
|
|
31
|
+
readonly demandedIfAtStep: (targetName: string) => string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const impactDemandConditions = (
|
|
35
|
+
options: ImpactDemandConditionOptions
|
|
36
|
+
): ImpactDemandConditions => {
|
|
37
|
+
const targetOutput = (targetName: string): string => {
|
|
38
|
+
const output = options.targetOutputs[targetName];
|
|
39
|
+
if (output === undefined) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`Unknown ${options.label} impact target "${targetName}".`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
return output;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const condition = (
|
|
48
|
+
source: string,
|
|
49
|
+
outcomeKey: string,
|
|
50
|
+
output: string
|
|
51
|
+
): string =>
|
|
52
|
+
`${source}.${outcomeKey} != 'success' || ${source}.outputs.${options.decisionOutput} != 'usable' || ${source}.outputs.${output} != 'withdrawn'`;
|
|
53
|
+
|
|
54
|
+
return Object.freeze({
|
|
55
|
+
demandedIf: (targetName: string, decisionJob: string) => {
|
|
56
|
+
const output = targetOutput(targetName);
|
|
57
|
+
if (decisionJob.trim().length === 0) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
`A cross-job ${options.label} impact condition requires the decision job id.`
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
return `always() && (${condition(`needs.${decisionJob}`, "result", output)})`;
|
|
63
|
+
},
|
|
64
|
+
demandedIfAtStep: (targetName: string) =>
|
|
65
|
+
condition(`steps.${options.stepId}`, "outcome", targetOutput(targetName)),
|
|
66
|
+
});
|
|
67
|
+
};
|
package/src/index.ts
CHANGED
package/src/merge-freeze-job.ts
CHANGED
|
@@ -23,6 +23,13 @@ import { FACTORY_PROOF_GATE_APP_ID } from "./proof-reuse-gate.ts";
|
|
|
23
23
|
* `gh` and parses the emitted payload with `validateMergeFreezeState`, so
|
|
24
24
|
* writer and reader cannot drift silently.
|
|
25
25
|
*
|
|
26
|
+
* Those two API calls are what the reader's two settling phases observe
|
|
27
|
+
* (#890). Before the POST the merge target carries no generation at all, which
|
|
28
|
+
* a reader reports as `awaiting-generation` while the pushed hosted verify run
|
|
29
|
+
* is still pending. Between the POST and the PATCH the generation exists with
|
|
30
|
+
* no `output.text`, which a reader reports as `running-generation`. Only the
|
|
31
|
+
* PATCH writes a verdict, so neither phase is one.
|
|
32
|
+
*
|
|
26
33
|
* Failure posture is deliberately fail-closed and the opposite of the
|
|
27
34
|
* proof-reuse gate's fail-open shell: this step runs under the runner's
|
|
28
35
|
* default `bash -e {0}`, so any API refusal aborts the step, the generation
|
|
@@ -193,7 +193,7 @@ const readCleanup = (value: Record<string, unknown>) => {
|
|
|
193
193
|
return null;
|
|
194
194
|
}
|
|
195
195
|
const evidence = readStringArray(value.cleanup.evidence);
|
|
196
|
-
if (evidence === null
|
|
196
|
+
if (evidence === null) {
|
|
197
197
|
return null;
|
|
198
198
|
}
|
|
199
199
|
return typeof value.cleanup.runUrl === "string" &&
|
|
@@ -212,10 +212,8 @@ const readPassingChecks = (value: Record<string, unknown>) => {
|
|
|
212
212
|
}
|
|
213
213
|
if (
|
|
214
214
|
value.convergence.status !== "passed" ||
|
|
215
|
-
value.convergenceStatus !== "passed" ||
|
|
216
215
|
typeof value.convergence.detail !== "string" ||
|
|
217
216
|
value.smoke.outcome !== "passed" ||
|
|
218
|
-
value.smokeStatus !== "passed" ||
|
|
219
217
|
typeof value.smoke.detail !== "string"
|
|
220
218
|
) {
|
|
221
219
|
return null;
|
|
@@ -276,14 +274,11 @@ const readPreviewProofRegistration = (
|
|
|
276
274
|
}
|
|
277
275
|
return {
|
|
278
276
|
cleanup,
|
|
279
|
-
cleanupStatus: cleanup.outcome,
|
|
280
277
|
convergence: passing.convergence,
|
|
281
|
-
convergenceStatus: "passed",
|
|
282
278
|
headSha: identity.headSha,
|
|
283
279
|
pr: identity.pr,
|
|
284
280
|
proof,
|
|
285
281
|
smoke: passing.smoke,
|
|
286
|
-
smokeStatus: "passed",
|
|
287
282
|
source: "local-self-certified",
|
|
288
283
|
stack: identity.stack,
|
|
289
284
|
stage: identity.stage,
|
|
@@ -604,7 +599,7 @@ const checkRunOutput = (registration: PreviewProofRegistration) => {
|
|
|
604
599
|
const title =
|
|
605
600
|
`Preview proof ${registration.stack}/${registration.stage}`.slice(0, 255);
|
|
606
601
|
return {
|
|
607
|
-
summary: `cleanup ${registration.
|
|
602
|
+
summary: `cleanup ${registration.cleanup.outcome}`,
|
|
608
603
|
text: JSON.stringify({
|
|
609
604
|
kind: INVENTORY_KIND,
|
|
610
605
|
registration,
|
|
@@ -49,9 +49,7 @@ export interface PreviewProofRegistration {
|
|
|
49
49
|
readonly outcome: PreviewProofCleanupStatus;
|
|
50
50
|
readonly runUrl?: string;
|
|
51
51
|
};
|
|
52
|
-
readonly cleanupStatus: PreviewProofCleanupStatus;
|
|
53
52
|
readonly convergence: { readonly detail: string; readonly status: "passed" };
|
|
54
|
-
readonly convergenceStatus: "passed";
|
|
55
53
|
readonly headSha: string;
|
|
56
54
|
readonly pr: number;
|
|
57
55
|
readonly proof: {
|
|
@@ -61,7 +59,6 @@ export interface PreviewProofRegistration {
|
|
|
61
59
|
readonly patchId: string;
|
|
62
60
|
};
|
|
63
61
|
readonly smoke: { readonly detail: string; readonly outcome: "passed" };
|
|
64
|
-
readonly smokeStatus: "passed";
|
|
65
62
|
readonly source: "local-self-certified";
|
|
66
63
|
readonly stack: string;
|
|
67
64
|
readonly stage: LocalPreviewStage;
|
|
@@ -239,12 +236,10 @@ const transition = (
|
|
|
239
236
|
|
|
240
237
|
return replaceRegistration(state, {
|
|
241
238
|
cleanup: { evidence: [], outcome: "pending" },
|
|
242
|
-
cleanupStatus: "pending",
|
|
243
239
|
convergence: {
|
|
244
240
|
detail: event.convergence.detail,
|
|
245
241
|
status: "passed",
|
|
246
242
|
},
|
|
247
|
-
convergenceStatus: "passed",
|
|
248
243
|
headSha: event.candidate.headSha,
|
|
249
244
|
pr: event.candidate.pr,
|
|
250
245
|
proof: {
|
|
@@ -254,7 +249,6 @@ const transition = (
|
|
|
254
249
|
patchId: event.evidence.patchId,
|
|
255
250
|
},
|
|
256
251
|
smoke: { detail: event.smoke.detail, outcome: "passed" },
|
|
257
|
-
smokeStatus: "passed",
|
|
258
252
|
source: "local-self-certified",
|
|
259
253
|
stack: event.stack,
|
|
260
254
|
stage,
|
|
@@ -286,7 +280,6 @@ const transition = (
|
|
|
286
280
|
return replaceRegistration(state, {
|
|
287
281
|
...existing,
|
|
288
282
|
cleanup,
|
|
289
|
-
cleanupStatus: event.outcome,
|
|
290
283
|
});
|
|
291
284
|
};
|
|
292
285
|
|
|
@@ -300,7 +293,7 @@ const recover = (
|
|
|
300
293
|
row.stack === event.stack &&
|
|
301
294
|
row.stage === event.stage
|
|
302
295
|
);
|
|
303
|
-
if (existing?.
|
|
296
|
+
if (existing?.cleanup.outcome !== "failed") {
|
|
304
297
|
throw new Error(
|
|
305
298
|
"Preview proof recovery re-enters cleanup only from a failed outcome."
|
|
306
299
|
);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { WorkflowStep } from "./factory-workflow.ts";
|
|
2
|
+
import { impactDemandConditions } from "./impact-demand-condition.ts";
|
|
2
3
|
|
|
3
4
|
export const FACTORY_PRODUCTION_IMPACT_STEP_ID = "production_impact";
|
|
4
5
|
export const FACTORY_PRODUCTION_IMPACT_DECISION_OUTPUT = "decision";
|
|
@@ -36,8 +37,13 @@ export interface FactoryProductionImpactWorkflow {
|
|
|
36
37
|
/** Outputs for a caller-owned decision job that subsequent jobs may consume. */
|
|
37
38
|
readonly decisionJobOutputs: Readonly<Record<string, string>>;
|
|
38
39
|
readonly decisionStep: WorkflowStep;
|
|
39
|
-
/**
|
|
40
|
-
|
|
40
|
+
/**
|
|
41
|
+
* A fail-open cross-job condition: only an explicit usable withdrawal skips
|
|
42
|
+
* work. The decision job id is required.
|
|
43
|
+
*/
|
|
44
|
+
readonly demandedIf: (targetName: string, decisionJob: string) => string;
|
|
45
|
+
/** The same fail-open condition for a step in the decision job. */
|
|
46
|
+
readonly demandedIfAtStep: (targetName: string) => string;
|
|
41
47
|
readonly targetOutputs: Readonly<Record<string, string>>;
|
|
42
48
|
}
|
|
43
49
|
|
|
@@ -79,6 +85,13 @@ export const factoryProductionImpactWorkflow = (
|
|
|
79
85
|
run: `${cli} production:impact --before "$FACTORY_BEFORE_SHA" --after "$FACTORY_AFTER_SHA" --github-output "$GITHUB_OUTPUT" --github-summary "$GITHUB_STEP_SUMMARY"${profile}`,
|
|
80
86
|
};
|
|
81
87
|
|
|
88
|
+
const conditions = impactDemandConditions({
|
|
89
|
+
decisionOutput: FACTORY_PRODUCTION_IMPACT_DECISION_OUTPUT,
|
|
90
|
+
label: "production",
|
|
91
|
+
stepId: FACTORY_PRODUCTION_IMPACT_STEP_ID,
|
|
92
|
+
targetOutputs,
|
|
93
|
+
});
|
|
94
|
+
|
|
82
95
|
return Object.freeze({
|
|
83
96
|
decisionJobOutputs: Object.freeze({
|
|
84
97
|
basis: `\${{ steps.${FACTORY_PRODUCTION_IMPACT_STEP_ID}.outputs.${FACTORY_PRODUCTION_IMPACT_BASIS_OUTPUT} }}`,
|
|
@@ -92,18 +105,8 @@ export const factoryProductionImpactWorkflow = (
|
|
|
92
105
|
),
|
|
93
106
|
}),
|
|
94
107
|
decisionStep: Object.freeze(decisionStep),
|
|
95
|
-
demandedIf:
|
|
96
|
-
|
|
97
|
-
if (output === undefined) {
|
|
98
|
-
throw new Error(`Unknown production impact target "${targetName}".`);
|
|
99
|
-
}
|
|
100
|
-
const source = decisionJob
|
|
101
|
-
? `needs.${decisionJob}`
|
|
102
|
-
: `steps.${FACTORY_PRODUCTION_IMPACT_STEP_ID}`;
|
|
103
|
-
const outcome = decisionJob ? "result" : "outcome";
|
|
104
|
-
const condition = `${source}.${outcome} != 'success' || ${source}.outputs.${FACTORY_PRODUCTION_IMPACT_DECISION_OUTPUT} != 'usable' || ${source}.outputs.${output} != 'withdrawn'`;
|
|
105
|
-
return decisionJob ? `always() && (${condition})` : condition;
|
|
106
|
-
},
|
|
108
|
+
demandedIf: conditions.demandedIf,
|
|
109
|
+
demandedIfAtStep: conditions.demandedIfAtStep,
|
|
107
110
|
targetOutputs: Object.freeze(targetOutputs),
|
|
108
111
|
});
|
|
109
112
|
};
|
package/src/proof-reuse-gate.ts
CHANGED
|
@@ -245,7 +245,7 @@ const COMMAND_IDENTITY_MAX_LENGTH = 120;
|
|
|
245
245
|
* cannot subtract hosted work. This is intentionally fail-closed and moves in
|
|
246
246
|
* lockstep with the proof/check-payload producer.
|
|
247
247
|
*/
|
|
248
|
-
const TRUSTED_IMPACT_STAMP_VERSION =
|
|
248
|
+
const TRUSTED_IMPACT_STAMP_VERSION = 4;
|
|
249
249
|
|
|
250
250
|
const isProofReuseCommand = (value: unknown): value is ProofReuseCommand => {
|
|
251
251
|
if (!(value && typeof value === "object")) {
|
|
@@ -464,7 +464,9 @@ def releaseAuthorized($release; $commands; $stamp):
|
|
|
464
464
|
type == "object"
|
|
465
465
|
and (.name | nonemptyString)
|
|
466
466
|
and (.basis | nonemptyString)
|
|
467
|
-
and ((.impact == "affected") or (.impact == "not-affected"))
|
|
467
|
+
and ((.impact == "affected") or (.impact == "not-affected"))
|
|
468
|
+
and ((.subscribedPaths | type) == "array")
|
|
469
|
+
and (.subscribedPaths | all(.[]; type == "string"))))
|
|
468
470
|
and ($stamp.targets | distinctNames)
|
|
469
471
|
and (([$releases[].name] - $executed | length) == ($releases | length))
|
|
470
472
|
and ($releases | all(.[]; releaseAuthorized(.; $commands; $stamp)))
|
|
@@ -1,7 +1,16 @@
|
|
|
1
|
+
import { UPLOAD_ARTIFACT } from "./actions.ts";
|
|
1
2
|
import type { PinnedAction } from "./actions.ts";
|
|
2
3
|
import type { WorkflowStep } from "./factory-workflow.ts";
|
|
3
4
|
import { assertPinnedAction } from "./pinned-action.ts";
|
|
4
5
|
|
|
6
|
+
/** `owner/repo` slice of the catalog's `UPLOAD_ARTIFACT` pin, derived rather
|
|
7
|
+
* than repeated as a literal, so a repository rename here cannot drift from
|
|
8
|
+
* the pin it validates against. */
|
|
9
|
+
const UPLOAD_ARTIFACT_REPOSITORY = UPLOAD_ARTIFACT.uses.slice(
|
|
10
|
+
0,
|
|
11
|
+
UPLOAD_ARTIFACT.uses.indexOf("@")
|
|
12
|
+
);
|
|
13
|
+
|
|
5
14
|
export const FACTORY_PUSH_IDENTITY_SCHEMA_VERSION = 1;
|
|
6
15
|
/**
|
|
7
16
|
* The artifact prefix and the five step IDs are module-private. Alpha.13
|
|
@@ -271,7 +280,12 @@ printf 'disposition=%s\nreason=%s\nbefore=%s\nafter=%s\nprovenance=%s\n' \
|
|
|
271
280
|
} >> "$GITHUB_STEP_SUMMARY"`;
|
|
272
281
|
|
|
273
282
|
export interface FactoryPushIdentityProducerOptions {
|
|
274
|
-
/**
|
|
283
|
+
/**
|
|
284
|
+
* Explicit caller-owned upload-artifact pin from its workflow artifact.
|
|
285
|
+
* Pass factory-ci's `UPLOAD_ARTIFACT` catalog export (or another
|
|
286
|
+
* `actions/upload-artifact` pin); the caller wires the step, the catalog
|
|
287
|
+
* only names the pin.
|
|
288
|
+
*/
|
|
275
289
|
readonly uploadArtifact: PinnedAction;
|
|
276
290
|
/** Optional consumer trigger policy combined with the required push event. */
|
|
277
291
|
readonly if?: string;
|
|
@@ -292,7 +306,7 @@ export const factoryPushIdentityProducer = (
|
|
|
292
306
|
assertPinnedAction(
|
|
293
307
|
"uploadArtifact",
|
|
294
308
|
options.uploadArtifact,
|
|
295
|
-
|
|
309
|
+
UPLOAD_ARTIFACT_REPOSITORY
|
|
296
310
|
);
|
|
297
311
|
const condition = options.if
|
|
298
312
|
? `github.event_name == 'push' && (${options.if})`
|