@patronage/factory-ci 1.0.0-alpha.24 → 1.0.0-alpha.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -8
- package/dist/.build-fingerprint.json +3 -0
- package/dist/index.d.ts +48 -1
- package/dist/index.js +119 -2
- package/package.json +2 -2
- package/src/index.ts +14 -0
- package/src/proof-reuse-presentation.ts +1 -1
- package/src/verify-runner-policy.ts +290 -0
package/README.md
CHANGED
|
@@ -61,22 +61,50 @@ workflow({/* caller-owned jobs and topology */}).writeOrLint({
|
|
|
61
61
|
});
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
Candidate lifecycle support follows the same boundary. Use `FACTORY_CANDIDATE_PULL_REQUEST_TYPES` for the pull-request trigger matrix and `factoryCandidateOrPushCondition()` on each substantive job. With an optional caller-owned path condition, the helper emits the GitHub expression that runs merge-target pushes unconditionally and runs pull-request work only when GitHub's draft boolean says the pull request is a Candidate:
|
|
64
|
+
`factoryWorkflow()` still does not set `runsOn`. This package does not depend on gagen. Job ids, permissions, topology, and deploy policy stay with the caller. Verify runner kinds are this package's business. Name each job's kind (or an override with a non-empty why), pass `.label` into gagen `job()`, and hand the same table to `assertVerifyRunnerPolicy` next to `assertWorkflowShellParses`:
|
|
67
65
|
|
|
68
66
|
```ts
|
|
67
|
+
import {
|
|
68
|
+
assertVerifyRunnerPolicy,
|
|
69
|
+
assertWorkflowShellParses,
|
|
70
|
+
factoryVerifyRunner,
|
|
71
|
+
type FactoryVerifyRunner,
|
|
72
|
+
} from "@patronage/factory-ci";
|
|
73
|
+
|
|
74
|
+
type VerifyJobId = "changes" | "core";
|
|
75
|
+
|
|
76
|
+
const verifyRunners: Record<VerifyJobId, FactoryVerifyRunner> = {
|
|
77
|
+
changes: factoryVerifyRunner("rounding-tax"),
|
|
78
|
+
core: factoryVerifyRunner("sustained-compute"),
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const runsOnFor = (id: VerifyJobId) => verifyRunners[id].label;
|
|
82
|
+
|
|
69
83
|
const changes = job("changes", {
|
|
70
84
|
if: factoryCandidateOrPushCondition(),
|
|
71
|
-
|
|
85
|
+
runsOn: runsOnFor("changes"),
|
|
72
86
|
});
|
|
73
87
|
|
|
74
88
|
const core = job("core", {
|
|
75
89
|
if: factoryCandidateOrPushCondition("needs.changes.outputs.core == 'true'"),
|
|
76
|
-
|
|
90
|
+
runsOn: runsOnFor("core"),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const generated = workflow({ jobs: [changes, core] });
|
|
94
|
+
|
|
95
|
+
assertWorkflowShellParses(generated.toYamlString(), {
|
|
96
|
+
source: ".github/workflows/verify.ts",
|
|
97
|
+
});
|
|
98
|
+
assertVerifyRunnerPolicy(generated.toYamlString(), {
|
|
99
|
+
source: ".github/workflows/verify.ts",
|
|
100
|
+
jobs: verifyRunners,
|
|
77
101
|
});
|
|
78
102
|
```
|
|
79
103
|
|
|
104
|
+
`sustained-compute` and `rounding-tax` both resolve to `depot-ubuntu-24.04`. They stay distinct so a short billed-rounding job cannot be confused with a long compute job in the table. `app-token` resolves to `ubuntu-latest`. `deploy-credentials` also resolves to `ubuntu-latest` and is refused inside `assertVerifyRunnerPolicy`, because a Verify workflow has no Cloudflare production secret. HQ deploy keeps a literal `ubuntu-latest` and never calls this assert.
|
|
105
|
+
|
|
106
|
+
Candidate lifecycle support follows the same boundary. Use `FACTORY_CANDIDATE_PULL_REQUEST_TYPES` for the pull-request trigger matrix and `factoryCandidateOrPushCondition()` on each substantive job. With an optional caller-owned path condition, the helper emits the GitHub expression that runs merge-target pushes unconditionally and runs pull-request work only when GitHub's draft boolean says the pull request is a Candidate. The runner label still comes from the kind table above.
|
|
107
|
+
|
|
80
108
|
Migration: upgrade `@patronage/factory-ci`, apply the shared trigger types and job condition in the TypeScript workflow source, then regenerate and commit the emitted YAML. Draft `opened` and `synchronize` events will stop running substantive Verify work. Promotion through `ready_for_review`, later non-draft Candidate events, and merge-target pushes continue to run their applicable battery. A skipped draft run is presentation, not proof.
|
|
81
109
|
|
|
82
110
|
Production impact support follows the same ownership line. A generated deploy workflow declares only its target names and consumes the returned decision step and fail-open `demandedIf(target, decisionJob)` expressions:
|
|
@@ -257,7 +285,10 @@ The audit job is `always()` so a failed destroy still runs the stage-wide check.
|
|
|
257
285
|
### Merge-freeze writer job
|
|
258
286
|
|
|
259
287
|
```ts
|
|
260
|
-
import {
|
|
288
|
+
import {
|
|
289
|
+
factoryMergeFreezeJob,
|
|
290
|
+
factoryVerifyRunner,
|
|
291
|
+
} from "@patronage/factory-ci";
|
|
261
292
|
|
|
262
293
|
const freeze = factoryMergeFreezeJob({
|
|
263
294
|
createGithubAppToken: workflowArtifact.actions.createGithubAppToken,
|
|
@@ -272,7 +303,7 @@ const jobs = [
|
|
|
272
303
|
name: freeze.jobName,
|
|
273
304
|
needs: freeze.needs,
|
|
274
305
|
if: freeze.if,
|
|
275
|
-
runsOn: "
|
|
306
|
+
runsOn: factoryVerifyRunner("app-token").label,
|
|
276
307
|
permissions: freeze.permissions,
|
|
277
308
|
timeoutMinutes: 10,
|
|
278
309
|
steps: freeze.steps,
|
|
@@ -282,7 +313,7 @@ const jobs = [
|
|
|
282
313
|
|
|
283
314
|
A consumer's generated merge-target-push Verify workflow is the only producer of `patronage-factory/merge-freeze` generations (#356, ADR 0016 as amended; #429, #872). A red merge-target Verify completes a generation active, a green one completes it inactive, and `pr:ready` reads the generation on the candidate's own base tip. No command, scheduled job, or second workflow writes that check run; the operator override (`demand:waive --demand merge-freeze`) waives the demand for one candidate and never writes here.
|
|
284
315
|
|
|
285
|
-
`factoryMergeFreezeJob` owns everything a reader trusts: the check name (`FACTORY_MERGE_FREEZE_CHECK_NAME`), the pinned Patronage Factory App identity, the `actions/create-github-app-token` inputs, the `needs.*.result` fold (`FACTORY_MERGE_FREEZE_VERIFY_RESULT_EXPRESSION`), the merge-target condition (`FACTORY_MERGE_FREEZE_IF`, built from `FACTORY_MERGE_TARGET_REF_CONDITION`), and the job's least-privilege `permissions` (an empty block: the App installation token carries the check-run write, the job uses no `GITHUB_TOKEN` scope, and it never checks out the repository). The caller owns the runner, the timeout, and the `needs` list.
|
|
316
|
+
`factoryMergeFreezeJob` owns everything a reader trusts: the check name (`FACTORY_MERGE_FREEZE_CHECK_NAME`), the pinned Patronage Factory App identity, the `actions/create-github-app-token` inputs, the `needs.*.result` fold (`FACTORY_MERGE_FREEZE_VERIFY_RESULT_EXPRESSION`), the merge-target condition (`FACTORY_MERGE_FREEZE_IF`, built from `FACTORY_MERGE_TARGET_REF_CONDITION`), and the job's least-privilege `permissions` (an empty block: the App installation token carries the check-run write, the job uses no `GITHUB_TOKEN` scope, and it never checks out the repository). The caller owns the runner kind (`app-token` for the Factory App token), the timeout, and the `needs` list.
|
|
286
317
|
|
|
287
318
|
That `needs` list is the whole trust input: it must name **every authoritative verification leaf**, because the fold is what decides active or inactive. An empty list is refused — a fold over no needed job always reports success, which would report a merge target nothing verified as green.
|
|
288
319
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1577,6 +1577,53 @@ type VitestProfileReadResult = {
|
|
|
1577
1577
|
*/
|
|
1578
1578
|
declare const readVitestProfileDocument: (value: unknown) => VitestProfileReadResult;
|
|
1579
1579
|
//#endregion
|
|
1580
|
+
//#region src/verify-runner-policy.d.ts
|
|
1581
|
+
declare const DEPOT_LABEL = "depot-ubuntu-24.04";
|
|
1582
|
+
declare const HOSTED_LABEL = "ubuntu-latest";
|
|
1583
|
+
/**
|
|
1584
|
+
* Why a job runs where it does. The two Depot kinds share a label. They stay
|
|
1585
|
+
* distinct so billed-rounding jobs and long compute jobs cannot be confused
|
|
1586
|
+
* in the declaration table.
|
|
1587
|
+
*
|
|
1588
|
+
* `deploy-credentials` is legal to construct (HQ deploy).
|
|
1589
|
+
* `assertVerifyRunnerPolicy` refuses it, because a Verify workflow has no
|
|
1590
|
+
* Cloudflare production secret.
|
|
1591
|
+
*/
|
|
1592
|
+
type FactoryRunnerKind = "sustained-compute" | "rounding-tax" | "app-token" | "deploy-credentials";
|
|
1593
|
+
type FactoryRunnerHome = "depot" | "hosted";
|
|
1594
|
+
interface FactoryRunnerOverride {
|
|
1595
|
+
readonly kind: "override";
|
|
1596
|
+
readonly home: FactoryRunnerHome;
|
|
1597
|
+
readonly why: string;
|
|
1598
|
+
}
|
|
1599
|
+
type FactoryRunnerDecision = FactoryRunnerKind | FactoryRunnerOverride;
|
|
1600
|
+
interface FactoryVerifyRunner {
|
|
1601
|
+
readonly decision: FactoryRunnerDecision;
|
|
1602
|
+
readonly home: FactoryRunnerHome;
|
|
1603
|
+
readonly label: typeof DEPOT_LABEL | typeof HOSTED_LABEL;
|
|
1604
|
+
readonly why?: string;
|
|
1605
|
+
}
|
|
1606
|
+
type FactoryVerifyRunnerTable = Readonly<Record<string, FactoryVerifyRunner>>;
|
|
1607
|
+
interface FactoryVerifyRunnerPolicyInput {
|
|
1608
|
+
readonly source: string;
|
|
1609
|
+
readonly jobs: FactoryVerifyRunnerTable;
|
|
1610
|
+
}
|
|
1611
|
+
interface FactoryVerifyRunnerMismatch {
|
|
1612
|
+
readonly jobId: string;
|
|
1613
|
+
readonly expected: string;
|
|
1614
|
+
readonly actual: string;
|
|
1615
|
+
}
|
|
1616
|
+
interface FactoryVerifyRunnerPolicyReport {
|
|
1617
|
+
readonly ok: boolean;
|
|
1618
|
+
readonly undeclared: readonly string[];
|
|
1619
|
+
readonly missing: readonly string[];
|
|
1620
|
+
readonly mismatches: readonly FactoryVerifyRunnerMismatch[];
|
|
1621
|
+
readonly forbiddenKinds: readonly string[];
|
|
1622
|
+
}
|
|
1623
|
+
declare const factoryVerifyRunner: (decision: FactoryRunnerDecision) => FactoryVerifyRunner;
|
|
1624
|
+
declare const verifyRunnerPolicy: (yaml: string, input: FactoryVerifyRunnerPolicyInput) => FactoryVerifyRunnerPolicyReport;
|
|
1625
|
+
declare const assertVerifyRunnerPolicy: (yaml: string, input: FactoryVerifyRunnerPolicyInput) => FactoryVerifyRunnerPolicyReport;
|
|
1626
|
+
//#endregion
|
|
1580
1627
|
//#region src/workflow-shell-lint.d.ts
|
|
1581
1628
|
/**
|
|
1582
1629
|
* Parse-check the shell embedded in generated workflow YAML (#376).
|
|
@@ -1628,4 +1675,4 @@ declare const assertWorkflowShellParses: (yaml: string, options: {
|
|
|
1628
1675
|
readonly source: string;
|
|
1629
1676
|
}) => void;
|
|
1630
1677
|
//#endregion
|
|
1631
|
-
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, GitHubAppTokenTransportError, type GithubAppCredentials, type GithubAppTokenOptions, type InstallStepOptions, type LocalPreviewStage, type LocalPreviewStageOptions, NODE_PNPM_ACTION_FAMILY_NODE24, type NodePnpmActionFamily, PREVIEW_PROOF_INVENTORY_CHECK_NAME, PREVIEW_PROOF_INVENTORY_LIST_PERMISSIONS, type ParseLocalPreviewStageExpected, type ParsedLocalPreviewStage, type PinnedAction, type PreviewProofCandidate, type PreviewProofCleanupOutcome, type PreviewProofCleanupStatus, type PreviewProofEnvelopeEvidence, type PreviewProofInventoryAccess, type PreviewProofInventoryCleanupInput, type PreviewProofInventoryListInput, type PreviewProofInventoryPersistInput, type PreviewProofInventoryStore, type PreviewProofInventoryTransport, type PreviewProofLifecycleEvent, type PreviewProofLifecycleState, type PreviewProofRegistration, type PreviewProofResolveQuery, PreviewProofTransportError, type ProofReuseCommand, type ProofReuseCoverageInput, type ProofReuseCoverageReport, type SetupNodeStepOptions, UPLOAD_ARTIFACT, VITEST_PROFILE_SCHEMA_VERSION, VITEST_PROFILE_TOOL, type VitestJsonReport, type VitestProfile, type VitestProfileDependencies, type VitestProfileDurationSummary, type VitestProfileEnvironment, VitestProfileError, type VitestProfileOptions, type VitestProfileReadResult, type VitestProfileSample, type VitestProfileSampleExecution, type VitestTestStatus, type WorkflowShellParseFailure, type WorkflowStep, assertProofReuseCoverage, assertWorkflowShellParses, bundleAlchemyEntry, captureVitestProfileEnvironment, executeAlchemyEntry, factoryCandidateImpactWorkflow, factoryCandidateOrPushCondition, factoryLifecycleContractLane, factoryMergeFreezeJob, factoryMergeFreezeScript, factoryPrStatusHudWorkflow, factoryPreviewCleanupTopology, factoryProductionImpactWorkflow, factoryProofGateScript, factoryProofGateStep, factoryProofReuseSummaryStep, factoryProofTimingStartStep, factoryPushIdentityConsumer, factoryPushIdentityProducer, factoryWorkflow, githubAppJwt, isLocalPreviewStage, localPreviewStage, mintInstallationToken, normalizeVitestProfileSample, parseLocalPreviewStage, previewCleanupDestroyJobId, previewProofInventory, previewProofLifecycle, productionImpactTargetOutput, proofReuseCoverage, proofReuseRequiredCommands, readVitestProfileDocument, resolveProofReuseCommands, runVitestProfile, workflowRunBlocks, workflowShellParseFailures, writeVitestProfile };
|
|
1678
|
+
export { type BundleAlchemyEntryOptions, type CheckoutStepOptions, EXECUTE_ALCHEMY_ENTRY_MAX_BUFFER, type ExecuteAlchemyEntryOptions, type ExecuteAlchemyEntryResult, FACTORY_CANDIDATE_IMPACT_BASIS_OUTPUT, FACTORY_CANDIDATE_IMPACT_DECISION_OUTPUT, FACTORY_CANDIDATE_IMPACT_INERT_OUTPUT, FACTORY_CANDIDATE_IMPACT_STEP_ID, FACTORY_CANDIDATE_IMPACT_UNSUBSCRIBED_OUTPUT, FACTORY_CANDIDATE_PULL_REQUEST_TYPES, FACTORY_LIFECYCLE_CONTRACT_JOB_ID, FACTORY_LIFECYCLE_CONTRACT_JOB_NAME, FACTORY_MERGE_FREEZE_APP_TOKEN_STEP_ID, FACTORY_MERGE_FREEZE_CHECK_NAME, FACTORY_MERGE_FREEZE_IF, FACTORY_MERGE_FREEZE_JOB_ID, FACTORY_MERGE_FREEZE_JOB_NAME, FACTORY_MERGE_FREEZE_PERMISSIONS, FACTORY_MERGE_FREEZE_VERIFY_RESULT_EXPRESSION, FACTORY_MERGE_TARGET_REF_CONDITION, FACTORY_PREVIEW_CLEANUP_AUDIT_JOB_ID, FACTORY_PREVIEW_CLEANUP_AUDIT_JOB_NAME, FACTORY_PRODUCTION_IMPACT_BASIS_OUTPUT, FACTORY_PRODUCTION_IMPACT_DECISION_OUTPUT, FACTORY_PRODUCTION_IMPACT_STEP_ID, FACTORY_PRODUCTION_IMPACT_UNSUBSCRIBED_OUTPUT, FACTORY_PROOF_GATE_APP_ID, FACTORY_PROOF_GATE_CHECK_NAME, FACTORY_PROOF_GATE_GUARD, FACTORY_PROOF_GATE_IF, FACTORY_PROOF_GATE_MODE_OUTPUT, FACTORY_PROOF_GATE_OUTPUT, FACTORY_PROOF_GATE_PULL_REQUEST_IF, FACTORY_PROOF_GATE_REASONS, FACTORY_PROOF_GATE_REASON_OUTPUT, FACTORY_PROOF_GATE_SHELL, FACTORY_PROOF_GATE_SOURCE_URL_OUTPUT, FACTORY_PROOF_GATE_STEP_ID, FACTORY_PROOF_GATE_STEP_NAME, FACTORY_PR_STATUS_HUD_APP_TOKEN_STEP_ID, FACTORY_PR_STATUS_HUD_CONCURRENCY, FACTORY_PR_STATUS_HUD_IF, FACTORY_PR_STATUS_HUD_JOB_ID, FACTORY_PR_STATUS_HUD_JOB_NAME, FACTORY_PR_STATUS_HUD_PERMISSIONS, FACTORY_PR_STATUS_HUD_PLAN_PATH, FACTORY_PUSH_IDENTITY_SCHEMA_VERSION, type FactoryCandidateImpactWorkflow, type FactoryCandidateImpactWorkflowOptions, type FactoryLifecycleContractLane, type FactoryLifecycleContractLaneOptions, type FactoryMergeFreezeJob, type FactoryMergeFreezeJobOptions, type FactoryPrStatusHudWorkflow, type FactoryPrStatusHudWorkflowOptions, type FactoryPreviewCleanupAuditJob, type FactoryPreviewCleanupDestroyJob, type FactoryPreviewCleanupTopology, type FactoryPreviewCleanupTopologyOptions, type FactoryProductionImpactWorkflow, type FactoryProductionImpactWorkflowOptions, type FactoryProofGateOptions, type FactoryProofGateReason, type FactoryProofGateReuse, type FactoryProofGateStep, type FactoryProofReusePresentationOptions, type FactoryProofReuseSummaryStep, type FactoryProofTimingStartStep, type FactoryPushIdentityConsumer, type FactoryPushIdentityConsumerOptions, type FactoryPushIdentityDisposition, type FactoryPushIdentityEnvelope, type FactoryPushIdentityProducer, type FactoryPushIdentityProducerOptions, type FactoryRunnerDecision, type FactoryRunnerHome, type FactoryRunnerKind, type FactoryRunnerOverride, type FactoryVerifyRunner, type FactoryVerifyRunnerMismatch, type FactoryVerifyRunnerPolicyInput, type FactoryVerifyRunnerPolicyReport, type FactoryVerifyRunnerTable, type FactoryWorkflowArtifact, type FactoryWorkflowOptions, type FactoryWorkflowSetupOptions, GitHubApiError, GitHubAppTokenTransportError, type GithubAppCredentials, type GithubAppTokenOptions, type InstallStepOptions, type LocalPreviewStage, type LocalPreviewStageOptions, NODE_PNPM_ACTION_FAMILY_NODE24, type NodePnpmActionFamily, PREVIEW_PROOF_INVENTORY_CHECK_NAME, PREVIEW_PROOF_INVENTORY_LIST_PERMISSIONS, type ParseLocalPreviewStageExpected, type ParsedLocalPreviewStage, type PinnedAction, type PreviewProofCandidate, type PreviewProofCleanupOutcome, type PreviewProofCleanupStatus, type PreviewProofEnvelopeEvidence, type PreviewProofInventoryAccess, type PreviewProofInventoryCleanupInput, type PreviewProofInventoryListInput, type PreviewProofInventoryPersistInput, type PreviewProofInventoryStore, type PreviewProofInventoryTransport, type PreviewProofLifecycleEvent, type PreviewProofLifecycleState, type PreviewProofRegistration, type PreviewProofResolveQuery, PreviewProofTransportError, type ProofReuseCommand, type ProofReuseCoverageInput, type ProofReuseCoverageReport, type SetupNodeStepOptions, UPLOAD_ARTIFACT, VITEST_PROFILE_SCHEMA_VERSION, VITEST_PROFILE_TOOL, type VitestJsonReport, type VitestProfile, type VitestProfileDependencies, type VitestProfileDurationSummary, type VitestProfileEnvironment, VitestProfileError, type VitestProfileOptions, type VitestProfileReadResult, type VitestProfileSample, type VitestProfileSampleExecution, type VitestTestStatus, type WorkflowShellParseFailure, type WorkflowStep, assertProofReuseCoverage, assertVerifyRunnerPolicy, assertWorkflowShellParses, bundleAlchemyEntry, captureVitestProfileEnvironment, executeAlchemyEntry, factoryCandidateImpactWorkflow, factoryCandidateOrPushCondition, factoryLifecycleContractLane, factoryMergeFreezeJob, factoryMergeFreezeScript, factoryPrStatusHudWorkflow, factoryPreviewCleanupTopology, factoryProductionImpactWorkflow, factoryProofGateScript, factoryProofGateStep, factoryProofReuseSummaryStep, factoryProofTimingStartStep, factoryPushIdentityConsumer, factoryPushIdentityProducer, factoryVerifyRunner, factoryWorkflow, githubAppJwt, isLocalPreviewStage, localPreviewStage, mintInstallationToken, normalizeVitestProfileSample, parseLocalPreviewStage, previewCleanupDestroyJobId, previewProofInventory, previewProofLifecycle, productionImpactTargetOutput, proofReuseCoverage, proofReuseRequiredCommands, readVitestProfileDocument, resolveProofReuseCommands, runVitestProfile, verifyRunnerPolicy, workflowRunBlocks, workflowShellParseFailures, writeVitestProfile };
|
package/dist/index.js
CHANGED
|
@@ -2006,7 +2006,7 @@ if [ "${shellExpansion("GITHUB_EVENT_NAME:-")}" = 'push' ] && [ -z "${shellExpan
|
|
|
2006
2006
|
# the PR-only reuse default (#863) that is every push. A consumer on
|
|
2007
2007
|
# 'pull-request-and-default-branch' reaches it only on a ref outside its
|
|
2008
2008
|
# merge target.
|
|
2009
|
-
say '- Path:
|
|
2009
|
+
say '- Path: full execution, no proof consulted'
|
|
2010
2010
|
say '- Proof-reuse gate: did not run on this ref; the full suite ran.'
|
|
2011
2011
|
say "- Verified head: \`${shellExpansion("GITHUB_SHA:-unknown")}\`"
|
|
2012
2012
|
echo "::notice title=Factory proof reuse::The proof-reuse gate did not run on this ref; the full ${label} suite executed."
|
|
@@ -3342,6 +3342,123 @@ const readVitestProfileDocument = (value) => {
|
|
|
3342
3342
|
};
|
|
3343
3343
|
};
|
|
3344
3344
|
//#endregion
|
|
3345
|
+
//#region src/verify-runner-policy.ts
|
|
3346
|
+
const DEPOT_LABEL = "depot-ubuntu-24.04";
|
|
3347
|
+
const HOSTED_LABEL = "ubuntu-latest";
|
|
3348
|
+
const isOverride = (decision) => typeof decision === "object";
|
|
3349
|
+
const homeForKind = (kind) => {
|
|
3350
|
+
switch (kind) {
|
|
3351
|
+
case "sustained-compute":
|
|
3352
|
+
case "rounding-tax": return "depot";
|
|
3353
|
+
case "app-token":
|
|
3354
|
+
case "deploy-credentials": return "hosted";
|
|
3355
|
+
default: return kind;
|
|
3356
|
+
}
|
|
3357
|
+
};
|
|
3358
|
+
const labelForHome = (home) => home === "depot" ? DEPOT_LABEL : HOSTED_LABEL;
|
|
3359
|
+
const assertOverrideWhy = (why) => {
|
|
3360
|
+
if (why.includes("\n") || why.includes("\r") || why.trim().length === 0) throw new Error("factoryVerifyRunner override requires a non-empty single-line why");
|
|
3361
|
+
};
|
|
3362
|
+
const factoryVerifyRunner = (decision) => {
|
|
3363
|
+
if (isOverride(decision)) {
|
|
3364
|
+
assertOverrideWhy(decision.why);
|
|
3365
|
+
return Object.freeze({
|
|
3366
|
+
decision,
|
|
3367
|
+
home: decision.home,
|
|
3368
|
+
label: labelForHome(decision.home),
|
|
3369
|
+
why: decision.why
|
|
3370
|
+
});
|
|
3371
|
+
}
|
|
3372
|
+
const home = homeForKind(decision);
|
|
3373
|
+
return Object.freeze({
|
|
3374
|
+
decision,
|
|
3375
|
+
home,
|
|
3376
|
+
label: labelForHome(home)
|
|
3377
|
+
});
|
|
3378
|
+
};
|
|
3379
|
+
const indentOf$1 = (line) => line.length - line.trimStart().length;
|
|
3380
|
+
const unquote$1 = (raw) => {
|
|
3381
|
+
const value = raw.trim();
|
|
3382
|
+
if (value.startsWith("'") && value.endsWith("'") && value.length > 1) return value.slice(1, -1).replaceAll("''", "'");
|
|
3383
|
+
if (value.startsWith("\"") && value.endsWith("\"") && value.length > 1) return value.slice(1, -1);
|
|
3384
|
+
return value;
|
|
3385
|
+
};
|
|
3386
|
+
const JOB_ID = /^[\w-]+$/u;
|
|
3387
|
+
const MATRIX_EXPRESSION = /\$\{\{\s*matrix\b/u;
|
|
3388
|
+
const throwNonScalarRunsOn = (jobId) => {
|
|
3389
|
+
throw new Error(`job ${jobId} uses a list, group, or matrix runs-on; Verify policy accepts a scalar label only`);
|
|
3390
|
+
};
|
|
3391
|
+
const verifyWorkflowJobRunners = (yaml) => {
|
|
3392
|
+
const lines = yaml.split("\n");
|
|
3393
|
+
const jobsLine = lines.findIndex((line) => line.trim() === "jobs:");
|
|
3394
|
+
if (jobsLine === -1) return {};
|
|
3395
|
+
const jobsIndent = indentOf$1(lines[jobsLine]);
|
|
3396
|
+
const jobIdIndent = jobsIndent + 2;
|
|
3397
|
+
const jobKeyIndent = jobIdIndent + 2;
|
|
3398
|
+
const runners = {};
|
|
3399
|
+
for (let index = jobsLine + 1; index < lines.length; index += 1) {
|
|
3400
|
+
const line = lines[index];
|
|
3401
|
+
if (line.trim().length === 0) continue;
|
|
3402
|
+
const indent = indentOf$1(line);
|
|
3403
|
+
if (indent <= jobsIndent) break;
|
|
3404
|
+
if (indent !== jobIdIndent) continue;
|
|
3405
|
+
const trimmed = line.trim();
|
|
3406
|
+
if (!trimmed.endsWith(":") || trimmed.includes(" ")) continue;
|
|
3407
|
+
const jobId = trimmed.slice(0, -1);
|
|
3408
|
+
if (!JOB_ID.test(jobId)) continue;
|
|
3409
|
+
let runsOn;
|
|
3410
|
+
for (let cursor = index + 1; cursor < lines.length; cursor += 1) {
|
|
3411
|
+
const candidate = lines[cursor];
|
|
3412
|
+
if (candidate.trim().length === 0) continue;
|
|
3413
|
+
const candidateIndent = indentOf$1(candidate);
|
|
3414
|
+
if (candidateIndent <= jobIdIndent) break;
|
|
3415
|
+
if (candidateIndent !== jobKeyIndent) continue;
|
|
3416
|
+
const rest = candidate.trimStart();
|
|
3417
|
+
if (!rest.startsWith("runs-on:")) continue;
|
|
3418
|
+
const inline = rest.slice(8).trim();
|
|
3419
|
+
if (inline.length === 0) throwNonScalarRunsOn(jobId);
|
|
3420
|
+
if (inline.startsWith("[") || inline.startsWith("{")) throwNonScalarRunsOn(jobId);
|
|
3421
|
+
const label = unquote$1(inline);
|
|
3422
|
+
if (MATRIX_EXPRESSION.test(label)) throwNonScalarRunsOn(jobId);
|
|
3423
|
+
runsOn = label;
|
|
3424
|
+
break;
|
|
3425
|
+
}
|
|
3426
|
+
if (runsOn === void 0) throw new Error(`job ${jobId} has no runs-on`);
|
|
3427
|
+
runners[jobId] = runsOn;
|
|
3428
|
+
}
|
|
3429
|
+
return runners;
|
|
3430
|
+
};
|
|
3431
|
+
const isDeployCredentials = (runner) => runner.decision === "deploy-credentials";
|
|
3432
|
+
const verifyRunnerPolicy = (yaml, input) => {
|
|
3433
|
+
const actual = verifyWorkflowJobRunners(yaml);
|
|
3434
|
+
const declared = Object.keys(input.jobs);
|
|
3435
|
+
const undeclared = Object.keys(actual).filter((jobId) => input.jobs[jobId] === void 0).toSorted();
|
|
3436
|
+
const missing = declared.filter((jobId) => actual[jobId] === void 0).toSorted();
|
|
3437
|
+
const mismatches = declared.filter((jobId) => actual[jobId] !== void 0 && actual[jobId] !== input.jobs[jobId].label).toSorted().map((jobId) => ({
|
|
3438
|
+
actual: actual[jobId],
|
|
3439
|
+
expected: input.jobs[jobId].label,
|
|
3440
|
+
jobId
|
|
3441
|
+
}));
|
|
3442
|
+
const forbiddenKinds = declared.filter((jobId) => isDeployCredentials(input.jobs[jobId])).toSorted();
|
|
3443
|
+
return Object.freeze({
|
|
3444
|
+
forbiddenKinds,
|
|
3445
|
+
mismatches,
|
|
3446
|
+
missing,
|
|
3447
|
+
ok: undeclared.length === 0 && missing.length === 0 && mismatches.length === 0 && forbiddenKinds.length === 0,
|
|
3448
|
+
undeclared
|
|
3449
|
+
});
|
|
3450
|
+
};
|
|
3451
|
+
const assertVerifyRunnerPolicy = (yaml, input) => {
|
|
3452
|
+
const report = verifyRunnerPolicy(yaml, input);
|
|
3453
|
+
if (report.ok) return report;
|
|
3454
|
+
const details = [];
|
|
3455
|
+
if (report.undeclared.length > 0) details.push(`undeclared jobs: ${report.undeclared.join(", ")}`);
|
|
3456
|
+
if (report.missing.length > 0) details.push(`missing from YAML: ${report.missing.join(", ")}`);
|
|
3457
|
+
for (const mismatch of report.mismatches) details.push(`${mismatch.jobId} runs-on ${mismatch.actual}, expected ${mismatch.expected}`);
|
|
3458
|
+
if (report.forbiddenKinds.length > 0) details.push(`deploy-credentials is illegal inside Verify: ${report.forbiddenKinds.join(", ")}`);
|
|
3459
|
+
throw new Error(`${input.source} Verify runner policy failed:\n${details.map((detail) => ` - ${detail}`).join("\n")}`);
|
|
3460
|
+
};
|
|
3461
|
+
//#endregion
|
|
3345
3462
|
//#region src/workflow-shell-lint.ts
|
|
3346
3463
|
const RUN_KEY = /^(?<indent>\s*)(?:-\s+)?run:(?<inline>.*)$/u;
|
|
3347
3464
|
/**
|
|
@@ -3606,4 +3723,4 @@ const assertWorkflowShellParses = (yaml, options) => {
|
|
|
3606
3723
|
throw new Error(`${options.source} emits shell bash cannot parse; the runner would treat it as a no-op:\n${detail}`);
|
|
3607
3724
|
};
|
|
3608
3725
|
//#endregion
|
|
3609
|
-
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, GitHubAppTokenTransportError, 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 };
|
|
3726
|
+
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, GitHubAppTokenTransportError, 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, 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 };
|
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.25",
|
|
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",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"scripts": {
|
|
51
51
|
"cleanup:check": "knip --files --dependencies --exports",
|
|
52
52
|
"prebuild": "bash ../scripts/ensure-worktree-bootstrap.sh",
|
|
53
|
-
"build": "tsdown",
|
|
53
|
+
"build": "tsdown && node ../scripts/factory-ci-dist-stamp.mjs write",
|
|
54
54
|
"precheck": "bash ../scripts/ensure-worktree-bootstrap.sh",
|
|
55
55
|
"check": "ultracite check",
|
|
56
56
|
"prefix": "bash ../scripts/ensure-worktree-bootstrap.sh",
|
package/src/index.ts
CHANGED
|
@@ -209,6 +209,20 @@ export {
|
|
|
209
209
|
readVitestProfileDocument,
|
|
210
210
|
type VitestProfileReadResult,
|
|
211
211
|
} from "./vitest-profile-reader.ts";
|
|
212
|
+
export {
|
|
213
|
+
assertVerifyRunnerPolicy,
|
|
214
|
+
factoryVerifyRunner,
|
|
215
|
+
type FactoryRunnerDecision,
|
|
216
|
+
type FactoryRunnerHome,
|
|
217
|
+
type FactoryRunnerKind,
|
|
218
|
+
type FactoryRunnerOverride,
|
|
219
|
+
type FactoryVerifyRunner,
|
|
220
|
+
type FactoryVerifyRunnerMismatch,
|
|
221
|
+
type FactoryVerifyRunnerPolicyInput,
|
|
222
|
+
type FactoryVerifyRunnerPolicyReport,
|
|
223
|
+
type FactoryVerifyRunnerTable,
|
|
224
|
+
verifyRunnerPolicy,
|
|
225
|
+
} from "./verify-runner-policy.ts";
|
|
212
226
|
export {
|
|
213
227
|
assertWorkflowShellParses,
|
|
214
228
|
workflowRunBlocks,
|
|
@@ -65,7 +65,7 @@ if [ "${shellExpansion("GITHUB_EVENT_NAME:-")}" = 'push' ] && [ -z "${shellExpan
|
|
|
65
65
|
# the PR-only reuse default (#863) that is every push. A consumer on
|
|
66
66
|
# 'pull-request-and-default-branch' reaches it only on a ref outside its
|
|
67
67
|
# merge target.
|
|
68
|
-
say '- Path:
|
|
68
|
+
say '- Path: full execution, no proof consulted'
|
|
69
69
|
say '- Proof-reuse gate: did not run on this ref; the full suite ran.'
|
|
70
70
|
say "- Verified head: \`${shellExpansion("GITHUB_SHA:-unknown")}\`"
|
|
71
71
|
echo "::notice title=Factory proof reuse::The proof-reuse gate did not run on this ref; the full ${label} suite executed."
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
const DEPOT_LABEL = "depot-ubuntu-24.04";
|
|
2
|
+
const HOSTED_LABEL = "ubuntu-latest";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Why a job runs where it does. The two Depot kinds share a label. They stay
|
|
6
|
+
* distinct so billed-rounding jobs and long compute jobs cannot be confused
|
|
7
|
+
* in the declaration table.
|
|
8
|
+
*
|
|
9
|
+
* `deploy-credentials` is legal to construct (HQ deploy).
|
|
10
|
+
* `assertVerifyRunnerPolicy` refuses it, because a Verify workflow has no
|
|
11
|
+
* Cloudflare production secret.
|
|
12
|
+
*/
|
|
13
|
+
export type FactoryRunnerKind =
|
|
14
|
+
| "sustained-compute"
|
|
15
|
+
| "rounding-tax"
|
|
16
|
+
| "app-token"
|
|
17
|
+
| "deploy-credentials";
|
|
18
|
+
|
|
19
|
+
export type FactoryRunnerHome = "depot" | "hosted";
|
|
20
|
+
|
|
21
|
+
export interface FactoryRunnerOverride {
|
|
22
|
+
readonly kind: "override";
|
|
23
|
+
readonly home: FactoryRunnerHome;
|
|
24
|
+
readonly why: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type FactoryRunnerDecision = FactoryRunnerKind | FactoryRunnerOverride;
|
|
28
|
+
|
|
29
|
+
export interface FactoryVerifyRunner {
|
|
30
|
+
readonly decision: FactoryRunnerDecision;
|
|
31
|
+
readonly home: FactoryRunnerHome;
|
|
32
|
+
readonly label: typeof DEPOT_LABEL | typeof HOSTED_LABEL;
|
|
33
|
+
readonly why?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type FactoryVerifyRunnerTable = Readonly<
|
|
37
|
+
Record<string, FactoryVerifyRunner>
|
|
38
|
+
>;
|
|
39
|
+
|
|
40
|
+
export interface FactoryVerifyRunnerPolicyInput {
|
|
41
|
+
readonly source: string;
|
|
42
|
+
readonly jobs: FactoryVerifyRunnerTable;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface FactoryVerifyRunnerMismatch {
|
|
46
|
+
readonly jobId: string;
|
|
47
|
+
readonly expected: string;
|
|
48
|
+
readonly actual: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface FactoryVerifyRunnerPolicyReport {
|
|
52
|
+
readonly ok: boolean;
|
|
53
|
+
readonly undeclared: readonly string[];
|
|
54
|
+
readonly missing: readonly string[];
|
|
55
|
+
readonly mismatches: readonly FactoryVerifyRunnerMismatch[];
|
|
56
|
+
readonly forbiddenKinds: readonly string[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const isOverride = (
|
|
60
|
+
decision: FactoryRunnerDecision
|
|
61
|
+
): decision is FactoryRunnerOverride => typeof decision === "object";
|
|
62
|
+
|
|
63
|
+
const homeForKind = (kind: FactoryRunnerKind): FactoryRunnerHome => {
|
|
64
|
+
switch (kind) {
|
|
65
|
+
case "sustained-compute":
|
|
66
|
+
case "rounding-tax": {
|
|
67
|
+
return "depot";
|
|
68
|
+
}
|
|
69
|
+
case "app-token":
|
|
70
|
+
case "deploy-credentials": {
|
|
71
|
+
return "hosted";
|
|
72
|
+
}
|
|
73
|
+
default: {
|
|
74
|
+
const exhaustive: never = kind;
|
|
75
|
+
return exhaustive;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const labelForHome = (
|
|
81
|
+
home: FactoryRunnerHome
|
|
82
|
+
): typeof DEPOT_LABEL | typeof HOSTED_LABEL =>
|
|
83
|
+
home === "depot" ? DEPOT_LABEL : HOSTED_LABEL;
|
|
84
|
+
|
|
85
|
+
const assertOverrideWhy = (why: string): void => {
|
|
86
|
+
if (why.includes("\n") || why.includes("\r") || why.trim().length === 0) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
"factoryVerifyRunner override requires a non-empty single-line why"
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export const factoryVerifyRunner = (
|
|
94
|
+
decision: FactoryRunnerDecision
|
|
95
|
+
): FactoryVerifyRunner => {
|
|
96
|
+
if (isOverride(decision)) {
|
|
97
|
+
assertOverrideWhy(decision.why);
|
|
98
|
+
return Object.freeze({
|
|
99
|
+
decision,
|
|
100
|
+
home: decision.home,
|
|
101
|
+
label: labelForHome(decision.home),
|
|
102
|
+
why: decision.why,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const home = homeForKind(decision);
|
|
107
|
+
return Object.freeze({
|
|
108
|
+
decision,
|
|
109
|
+
home,
|
|
110
|
+
label: labelForHome(home),
|
|
111
|
+
});
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const indentOf = (line: string): number =>
|
|
115
|
+
line.length - line.trimStart().length;
|
|
116
|
+
|
|
117
|
+
const unquote = (raw: string): string => {
|
|
118
|
+
const value = raw.trim();
|
|
119
|
+
if (value.startsWith("'") && value.endsWith("'") && value.length > 1) {
|
|
120
|
+
return value.slice(1, -1).replaceAll("''", "'");
|
|
121
|
+
}
|
|
122
|
+
if (value.startsWith('"') && value.endsWith('"') && value.length > 1) {
|
|
123
|
+
return value.slice(1, -1);
|
|
124
|
+
}
|
|
125
|
+
return value;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const JOB_ID = /^[\w-]+$/u;
|
|
129
|
+
const MATRIX_EXPRESSION = /\$\{\{\s*matrix\b/u;
|
|
130
|
+
|
|
131
|
+
const throwNonScalarRunsOn = (jobId: string): never => {
|
|
132
|
+
throw new Error(
|
|
133
|
+
`job ${jobId} uses a list, group, or matrix runs-on; Verify policy accepts a scalar label only`
|
|
134
|
+
);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const verifyWorkflowJobRunners = (
|
|
138
|
+
yaml: string
|
|
139
|
+
): Readonly<Record<string, string>> => {
|
|
140
|
+
const lines = yaml.split("\n");
|
|
141
|
+
const jobsLine = lines.findIndex((line) => line.trim() === "jobs:");
|
|
142
|
+
if (jobsLine === -1) {
|
|
143
|
+
return {};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const jobsIndent = indentOf(lines[jobsLine]);
|
|
147
|
+
const jobIdIndent = jobsIndent + 2;
|
|
148
|
+
const jobKeyIndent = jobIdIndent + 2;
|
|
149
|
+
const runners: Record<string, string> = {};
|
|
150
|
+
|
|
151
|
+
for (let index = jobsLine + 1; index < lines.length; index += 1) {
|
|
152
|
+
const line = lines[index];
|
|
153
|
+
if (line.trim().length === 0) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const indent = indentOf(line);
|
|
157
|
+
if (indent <= jobsIndent) {
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
if (indent !== jobIdIndent) {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const trimmed = line.trim();
|
|
165
|
+
if (!trimmed.endsWith(":") || trimmed.includes(" ")) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const jobId = trimmed.slice(0, -1);
|
|
169
|
+
if (!JOB_ID.test(jobId)) {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let runsOn: string | undefined;
|
|
174
|
+
for (let cursor = index + 1; cursor < lines.length; cursor += 1) {
|
|
175
|
+
const candidate = lines[cursor];
|
|
176
|
+
if (candidate.trim().length === 0) {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
const candidateIndent = indentOf(candidate);
|
|
180
|
+
if (candidateIndent <= jobIdIndent) {
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
if (candidateIndent !== jobKeyIndent) {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
const rest = candidate.trimStart();
|
|
187
|
+
if (!rest.startsWith("runs-on:")) {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
const inline = rest.slice("runs-on:".length).trim();
|
|
191
|
+
if (inline.length === 0) {
|
|
192
|
+
throwNonScalarRunsOn(jobId);
|
|
193
|
+
}
|
|
194
|
+
if (inline.startsWith("[") || inline.startsWith("{")) {
|
|
195
|
+
throwNonScalarRunsOn(jobId);
|
|
196
|
+
}
|
|
197
|
+
const label = unquote(inline);
|
|
198
|
+
if (MATRIX_EXPRESSION.test(label)) {
|
|
199
|
+
throwNonScalarRunsOn(jobId);
|
|
200
|
+
}
|
|
201
|
+
runsOn = label;
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
if (runsOn === undefined) {
|
|
205
|
+
throw new Error(`job ${jobId} has no runs-on`);
|
|
206
|
+
}
|
|
207
|
+
runners[jobId] = runsOn;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return runners;
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const isDeployCredentials = (runner: FactoryVerifyRunner): boolean =>
|
|
214
|
+
runner.decision === "deploy-credentials";
|
|
215
|
+
|
|
216
|
+
export const verifyRunnerPolicy = (
|
|
217
|
+
yaml: string,
|
|
218
|
+
input: FactoryVerifyRunnerPolicyInput
|
|
219
|
+
): FactoryVerifyRunnerPolicyReport => {
|
|
220
|
+
const actual = verifyWorkflowJobRunners(yaml);
|
|
221
|
+
const declared = Object.keys(input.jobs);
|
|
222
|
+
const emitted = Object.keys(actual);
|
|
223
|
+
|
|
224
|
+
const undeclared = emitted
|
|
225
|
+
.filter((jobId) => input.jobs[jobId] === undefined)
|
|
226
|
+
.toSorted();
|
|
227
|
+
const missing = declared
|
|
228
|
+
.filter((jobId) => actual[jobId] === undefined)
|
|
229
|
+
.toSorted();
|
|
230
|
+
const mismatches = declared
|
|
231
|
+
.filter(
|
|
232
|
+
(jobId) =>
|
|
233
|
+
actual[jobId] !== undefined && actual[jobId] !== input.jobs[jobId].label
|
|
234
|
+
)
|
|
235
|
+
.toSorted()
|
|
236
|
+
.map((jobId) => ({
|
|
237
|
+
actual: actual[jobId],
|
|
238
|
+
expected: input.jobs[jobId].label,
|
|
239
|
+
jobId,
|
|
240
|
+
}));
|
|
241
|
+
const forbiddenKinds = declared
|
|
242
|
+
.filter((jobId) => isDeployCredentials(input.jobs[jobId]))
|
|
243
|
+
.toSorted();
|
|
244
|
+
|
|
245
|
+
return Object.freeze({
|
|
246
|
+
forbiddenKinds,
|
|
247
|
+
mismatches,
|
|
248
|
+
missing,
|
|
249
|
+
ok:
|
|
250
|
+
undeclared.length === 0 &&
|
|
251
|
+
missing.length === 0 &&
|
|
252
|
+
mismatches.length === 0 &&
|
|
253
|
+
forbiddenKinds.length === 0,
|
|
254
|
+
undeclared,
|
|
255
|
+
});
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
export const assertVerifyRunnerPolicy = (
|
|
259
|
+
yaml: string,
|
|
260
|
+
input: FactoryVerifyRunnerPolicyInput
|
|
261
|
+
): FactoryVerifyRunnerPolicyReport => {
|
|
262
|
+
const report = verifyRunnerPolicy(yaml, input);
|
|
263
|
+
if (report.ok) {
|
|
264
|
+
return report;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const details: string[] = [];
|
|
268
|
+
if (report.undeclared.length > 0) {
|
|
269
|
+
details.push(`undeclared jobs: ${report.undeclared.join(", ")}`);
|
|
270
|
+
}
|
|
271
|
+
if (report.missing.length > 0) {
|
|
272
|
+
details.push(`missing from YAML: ${report.missing.join(", ")}`);
|
|
273
|
+
}
|
|
274
|
+
for (const mismatch of report.mismatches) {
|
|
275
|
+
details.push(
|
|
276
|
+
`${mismatch.jobId} runs-on ${mismatch.actual}, expected ${mismatch.expected}`
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
if (report.forbiddenKinds.length > 0) {
|
|
280
|
+
details.push(
|
|
281
|
+
`deploy-credentials is illegal inside Verify: ${report.forbiddenKinds.join(", ")}`
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
throw new Error(
|
|
286
|
+
`${input.source} Verify runner policy failed:\n${details
|
|
287
|
+
.map((detail) => ` - ${detail}`)
|
|
288
|
+
.join("\n")}`
|
|
289
|
+
);
|
|
290
|
+
};
|