@patronage/factory-ci 1.0.0-alpha.7 → 1.0.0-alpha.9
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 +51 -2
- package/dist/index.d.ts +109 -2
- package/dist/index.js +384 -25
- package/package.json +1 -1
- package/src/candidate-lifecycle.ts +29 -0
- package/src/factory-workflow.ts +25 -1
- package/src/index.ts +31 -0
- package/src/production-impact-workflow.ts +109 -0
- package/src/proof-reuse-gate.ts +96 -10
- package/src/proof-reuse-presentation.ts +125 -0
- package/src/vitest-profile-reader.test.ts +208 -0
- package/src/vitest-profile-reader.ts +220 -0
package/src/factory-workflow.ts
CHANGED
|
@@ -5,6 +5,9 @@ const ACTION_TAG_PATTERN = /^v\d+(?:\.\d+){0,2}$/u;
|
|
|
5
5
|
const RESERVED_ACTION_NAMES = new Set(["checkout", "setupNode", "setupPnpm"]);
|
|
6
6
|
|
|
7
7
|
export interface WorkflowStep {
|
|
8
|
+
readonly continueOnError?: boolean;
|
|
9
|
+
readonly env?: Readonly<Record<string, string>>;
|
|
10
|
+
readonly id?: string;
|
|
8
11
|
readonly name: string;
|
|
9
12
|
readonly uses?: string;
|
|
10
13
|
readonly with?: Readonly<Record<string, string>>;
|
|
@@ -12,6 +15,7 @@ export interface WorkflowStep {
|
|
|
12
15
|
}
|
|
13
16
|
|
|
14
17
|
export interface CheckoutStepOptions {
|
|
18
|
+
readonly fetchDepth?: number;
|
|
15
19
|
readonly name?: string;
|
|
16
20
|
readonly ref?: string;
|
|
17
21
|
}
|
|
@@ -129,6 +133,25 @@ const assertAdditionalActions = (
|
|
|
129
133
|
}
|
|
130
134
|
};
|
|
131
135
|
|
|
136
|
+
const checkoutInputs = (
|
|
137
|
+
checkout: CheckoutStepOptions | undefined
|
|
138
|
+
): Readonly<Record<string, string>> | undefined => {
|
|
139
|
+
if (checkout === undefined) {
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
const inputs: Record<string, string> = {};
|
|
143
|
+
if (checkout.fetchDepth !== undefined) {
|
|
144
|
+
if (!Number.isSafeInteger(checkout.fetchDepth) || checkout.fetchDepth < 0) {
|
|
145
|
+
throw new Error("checkout.fetchDepth must be a non-negative integer.");
|
|
146
|
+
}
|
|
147
|
+
inputs["fetch-depth"] = String(checkout.fetchDepth);
|
|
148
|
+
}
|
|
149
|
+
if (checkout.ref) {
|
|
150
|
+
inputs.ref = checkout.ref;
|
|
151
|
+
}
|
|
152
|
+
return Object.keys(inputs).length > 0 ? inputs : undefined;
|
|
153
|
+
};
|
|
154
|
+
|
|
132
155
|
const setupSteps = (
|
|
133
156
|
family: NodePnpmActionFamily,
|
|
134
157
|
setup: FactoryWorkflowSetupOptions = {}
|
|
@@ -141,11 +164,12 @@ const setupSteps = (
|
|
|
141
164
|
}
|
|
142
165
|
|
|
143
166
|
const { checkout, setupNode } = setup;
|
|
167
|
+
const checkoutWith = checkoutInputs(checkout);
|
|
144
168
|
return Object.freeze([
|
|
145
169
|
{
|
|
146
170
|
name: checkout?.name ?? "Checkout",
|
|
147
171
|
uses: family.checkout.uses,
|
|
148
|
-
...(
|
|
172
|
+
...(checkoutWith ? { with: checkoutWith } : {}),
|
|
149
173
|
},
|
|
150
174
|
usesStep("Setup pnpm", family.setupPnpm),
|
|
151
175
|
{
|
package/src/index.ts
CHANGED
|
@@ -13,6 +13,10 @@ export {
|
|
|
13
13
|
type NodePnpmActionFamily,
|
|
14
14
|
type PinnedAction,
|
|
15
15
|
} from "./actions.ts";
|
|
16
|
+
export {
|
|
17
|
+
FACTORY_CANDIDATE_PULL_REQUEST_TYPES,
|
|
18
|
+
factoryCandidateOrPushCondition,
|
|
19
|
+
} from "./candidate-lifecycle.ts";
|
|
16
20
|
export {
|
|
17
21
|
bundleAlchemyEntry,
|
|
18
22
|
type BundleAlchemyEntryOptions,
|
|
@@ -59,7 +63,9 @@ export {
|
|
|
59
63
|
FACTORY_PROOF_GATE_REASON_OUTPUT,
|
|
60
64
|
FACTORY_PROOF_GATE_REASONS,
|
|
61
65
|
FACTORY_PROOF_GATE_SHELL,
|
|
66
|
+
FACTORY_PROOF_GATE_SOURCE_URL_OUTPUT,
|
|
62
67
|
FACTORY_PROOF_GATE_STEP_ID,
|
|
68
|
+
FACTORY_PROOF_GATE_STEP_NAME,
|
|
63
69
|
type FactoryProofGateOptions,
|
|
64
70
|
type FactoryProofGateReason,
|
|
65
71
|
factoryProofGateScript,
|
|
@@ -72,6 +78,27 @@ export {
|
|
|
72
78
|
proofReuseRequiredCommands,
|
|
73
79
|
resolveProofReuseCommands,
|
|
74
80
|
} from "./proof-reuse-gate.ts";
|
|
81
|
+
export {
|
|
82
|
+
type FactoryProofReusePresentationOptions,
|
|
83
|
+
type FactoryProofReuseSummaryStep,
|
|
84
|
+
type FactoryProofTimingStartStep,
|
|
85
|
+
FACTORY_PROOF_TIMING_START_STEP_NAME,
|
|
86
|
+
FACTORY_PROOF_TIMING_STEP_ID,
|
|
87
|
+
FACTORY_PROOF_TIMING_SUMMARY_STEP_NAME,
|
|
88
|
+
factoryProofReuseSummaryScript,
|
|
89
|
+
factoryProofReuseSummaryStep,
|
|
90
|
+
factoryProofTimingStartStep,
|
|
91
|
+
} from "./proof-reuse-presentation.ts";
|
|
92
|
+
export {
|
|
93
|
+
FACTORY_PRODUCTION_IMPACT_BASIS_OUTPUT,
|
|
94
|
+
FACTORY_PRODUCTION_IMPACT_DECISION_OUTPUT,
|
|
95
|
+
FACTORY_PRODUCTION_IMPACT_STEP_ID,
|
|
96
|
+
FACTORY_PRODUCTION_IMPACT_UNSUBSCRIBED_OUTPUT,
|
|
97
|
+
type FactoryProductionImpactWorkflow,
|
|
98
|
+
type FactoryProductionImpactWorkflowOptions,
|
|
99
|
+
factoryProductionImpactWorkflow,
|
|
100
|
+
productionImpactTargetOutput,
|
|
101
|
+
} from "./production-impact-workflow.ts";
|
|
75
102
|
export {
|
|
76
103
|
captureVitestProfileEnvironment,
|
|
77
104
|
normalizeVitestProfileSample,
|
|
@@ -90,6 +117,10 @@ export {
|
|
|
90
117
|
type VitestTestStatus,
|
|
91
118
|
writeVitestProfile,
|
|
92
119
|
} from "./vitest-profile.ts";
|
|
120
|
+
export {
|
|
121
|
+
readVitestProfileDocument,
|
|
122
|
+
type VitestProfileReadResult,
|
|
123
|
+
} from "./vitest-profile-reader.ts";
|
|
93
124
|
export {
|
|
94
125
|
assertWorkflowShellParses,
|
|
95
126
|
workflowRunBlocks,
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { WorkflowStep } from "./factory-workflow.ts";
|
|
2
|
+
|
|
3
|
+
export const FACTORY_PRODUCTION_IMPACT_STEP_ID = "production_impact";
|
|
4
|
+
export const FACTORY_PRODUCTION_IMPACT_DECISION_OUTPUT = "decision";
|
|
5
|
+
export const FACTORY_PRODUCTION_IMPACT_BASIS_OUTPUT = "basis";
|
|
6
|
+
export const FACTORY_PRODUCTION_IMPACT_UNSUBSCRIBED_OUTPUT =
|
|
7
|
+
"unsubscribed_paths";
|
|
8
|
+
|
|
9
|
+
const OUTPUT_NAME_PATTERN = /[^a-z0-9_]+/gu;
|
|
10
|
+
const shellQuote = (value: string): string =>
|
|
11
|
+
`'${value.replaceAll("'", `'"'"'`)}'`;
|
|
12
|
+
|
|
13
|
+
/** Stable GitHub-output key for one declared target. */
|
|
14
|
+
export const productionImpactTargetOutput = (targetName: string): string => {
|
|
15
|
+
const normalized = targetName
|
|
16
|
+
.toLowerCase()
|
|
17
|
+
.replaceAll(OUTPUT_NAME_PATTERN, "_")
|
|
18
|
+
.replaceAll(/^_+|_+$/gu, "");
|
|
19
|
+
if (normalized.length === 0) {
|
|
20
|
+
throw new Error(
|
|
21
|
+
`Impact target name "${targetName}" has no output-safe characters.`
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
return `target_${normalized}`;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export interface FactoryProductionImpactWorkflowOptions {
|
|
28
|
+
readonly after?: string;
|
|
29
|
+
readonly before?: string;
|
|
30
|
+
readonly cli?: string;
|
|
31
|
+
readonly profilePath?: string;
|
|
32
|
+
readonly targets: readonly string[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface FactoryProductionImpactWorkflow {
|
|
36
|
+
/** Outputs for a caller-owned decision job that subsequent jobs may consume. */
|
|
37
|
+
readonly decisionJobOutputs: Readonly<Record<string, string>>;
|
|
38
|
+
readonly decisionStep: WorkflowStep;
|
|
39
|
+
/** A fail-open condition: only an explicit usable withdrawal skips work. */
|
|
40
|
+
readonly demandedIf: (targetName: string, decisionJob?: string) => string;
|
|
41
|
+
readonly targetOutputs: Readonly<Record<string, string>>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Generate the small factory-owned decision seam for a consumer production
|
|
46
|
+
* workflow. Consumers retain jobs, deploy commands, credentials, topology,
|
|
47
|
+
* and convergence checks; this artifact supplies only the decision step and
|
|
48
|
+
* per-target withdrawal conditions.
|
|
49
|
+
*/
|
|
50
|
+
export const factoryProductionImpactWorkflow = (
|
|
51
|
+
options: FactoryProductionImpactWorkflowOptions
|
|
52
|
+
): FactoryProductionImpactWorkflow => {
|
|
53
|
+
const targetOutputs = Object.fromEntries(
|
|
54
|
+
options.targets.map((target) => [
|
|
55
|
+
target,
|
|
56
|
+
productionImpactTargetOutput(target),
|
|
57
|
+
])
|
|
58
|
+
);
|
|
59
|
+
if (new Set(Object.values(targetOutputs)).size !== options.targets.length) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
"Impact target names must map to distinct GitHub output keys."
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const before = options.before ?? `\${{ github.event.before }}`;
|
|
66
|
+
const after = options.after ?? `\${{ github.sha }}`;
|
|
67
|
+
const cli = options.cli ?? "pnpm exec psf";
|
|
68
|
+
const profile = options.profilePath
|
|
69
|
+
? ` --profile ${shellQuote(options.profilePath)}`
|
|
70
|
+
: "";
|
|
71
|
+
const decisionStep: WorkflowStep = {
|
|
72
|
+
continueOnError: true,
|
|
73
|
+
env: {
|
|
74
|
+
FACTORY_AFTER_SHA: after,
|
|
75
|
+
FACTORY_BEFORE_SHA: before,
|
|
76
|
+
},
|
|
77
|
+
id: FACTORY_PRODUCTION_IMPACT_STEP_ID,
|
|
78
|
+
name: "Classify production impact",
|
|
79
|
+
run: `${cli} production:impact --before "$FACTORY_BEFORE_SHA" --after "$FACTORY_AFTER_SHA" --github-output "$GITHUB_OUTPUT" --github-summary "$GITHUB_STEP_SUMMARY"${profile}`,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
return Object.freeze({
|
|
83
|
+
decisionJobOutputs: Object.freeze({
|
|
84
|
+
basis: `\${{ steps.${FACTORY_PRODUCTION_IMPACT_STEP_ID}.outputs.${FACTORY_PRODUCTION_IMPACT_BASIS_OUTPUT} }}`,
|
|
85
|
+
decision: `\${{ steps.${FACTORY_PRODUCTION_IMPACT_STEP_ID}.outputs.${FACTORY_PRODUCTION_IMPACT_DECISION_OUTPUT} }}`,
|
|
86
|
+
unsubscribed_paths: `\${{ steps.${FACTORY_PRODUCTION_IMPACT_STEP_ID}.outputs.${FACTORY_PRODUCTION_IMPACT_UNSUBSCRIBED_OUTPUT} }}`,
|
|
87
|
+
...Object.fromEntries(
|
|
88
|
+
Object.values(targetOutputs).map((output) => [
|
|
89
|
+
output,
|
|
90
|
+
`\${{ steps.${FACTORY_PRODUCTION_IMPACT_STEP_ID}.outputs.${output} }}`,
|
|
91
|
+
])
|
|
92
|
+
),
|
|
93
|
+
}),
|
|
94
|
+
decisionStep: Object.freeze(decisionStep),
|
|
95
|
+
demandedIf: (targetName: string, decisionJob?: string) => {
|
|
96
|
+
const output = targetOutputs[targetName];
|
|
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
|
+
},
|
|
107
|
+
targetOutputs: Object.freeze(targetOutputs),
|
|
108
|
+
});
|
|
109
|
+
};
|
package/src/proof-reuse-gate.ts
CHANGED
|
@@ -57,6 +57,15 @@ export const FACTORY_PROOF_GATE_APP_ID = "4314840";
|
|
|
57
57
|
/** Step id the guard condition refers to. */
|
|
58
58
|
export const FACTORY_PROOF_GATE_STEP_ID = "factory-proof";
|
|
59
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Human-visible name of the gate step as the Actions jobs API serves it. A
|
|
62
|
+
* read-only run analyzer (`psf ci:analyze`, #647) matches on this name to
|
|
63
|
+
* classify a run as proof-reuse versus full fallback, so it is exported from
|
|
64
|
+
* exactly the module that writes it — matching on a re-typed copy would drift.
|
|
65
|
+
*/
|
|
66
|
+
export const FACTORY_PROOF_GATE_STEP_NAME =
|
|
67
|
+
"Check for factory proof of this head";
|
|
68
|
+
|
|
60
69
|
/**
|
|
61
70
|
* The shell the gate runs under, and it is a correctness requirement rather
|
|
62
71
|
* than a preference.
|
|
@@ -98,6 +107,9 @@ export const FACTORY_PROOF_GATE_REASON_OUTPUT = "reason";
|
|
|
98
107
|
*/
|
|
99
108
|
export const FACTORY_PROOF_GATE_MODE_OUTPUT = "mode";
|
|
100
109
|
|
|
110
|
+
/** Exact Checks API URL of the proof generation selected by the gate. */
|
|
111
|
+
export const FACTORY_PROOF_GATE_SOURCE_URL_OUTPUT = "source-check-url";
|
|
112
|
+
|
|
101
113
|
/**
|
|
102
114
|
* Guard for every step the gate protects. Deliberately `!= 'true'` and not
|
|
103
115
|
* `== 'false'`: an unset, empty, or garbled output must run the suite.
|
|
@@ -120,7 +132,8 @@ export const FACTORY_PROOF_GATE_IF = "github.event_name == 'pull_request'";
|
|
|
120
132
|
* - `pending` the newest generation had not completed when this job read it
|
|
121
133
|
* - `failed` the newest generation records no pass
|
|
122
134
|
* - `unreadable` it passed but carries no binding for this repository and head
|
|
123
|
-
* - `incomplete`
|
|
135
|
+
* - `incomplete` its executed plus stamp-authorized released commands do not
|
|
136
|
+
* cover every required command
|
|
124
137
|
* - `ambiguous` two newest generations share the greatest start time
|
|
125
138
|
* - `error` the gate could not reach a decision (fail open)
|
|
126
139
|
*
|
|
@@ -170,6 +183,14 @@ export interface ProofReuseCommand {
|
|
|
170
183
|
const COMMAND_IDENTITY = /^\w[\w.:@/-]*$/u;
|
|
171
184
|
const COMMAND_IDENTITY_MAX_LENGTH = 120;
|
|
172
185
|
|
|
186
|
+
/**
|
|
187
|
+
* The impact-stamp interpretation the paired factory packages currently
|
|
188
|
+
* share. A release recorded under any other version is an unmodelled input and
|
|
189
|
+
* cannot subtract hosted work. This is intentionally fail-closed and moves in
|
|
190
|
+
* lockstep with the proof/check-payload producer.
|
|
191
|
+
*/
|
|
192
|
+
const TRUSTED_IMPACT_STAMP_VERSION = 3;
|
|
193
|
+
|
|
173
194
|
const isProofReuseCommand = (value: unknown): value is ProofReuseCommand => {
|
|
174
195
|
if (!(value && typeof value === "object")) {
|
|
175
196
|
return false;
|
|
@@ -262,7 +283,7 @@ export const resolveProofReuseCommands = (
|
|
|
262
283
|
|
|
263
284
|
/**
|
|
264
285
|
* jq program: every page of the Checks API result in, three sanitized lines
|
|
265
|
-
* (`reason`, `mode`,
|
|
286
|
+
* (`reason`, `mode`, uncovered commands) out.
|
|
266
287
|
*
|
|
267
288
|
* The input is what `gh api --paginate` actually writes: the pages
|
|
268
289
|
* *concatenated* as a stream of top-level response objects, not merged into
|
|
@@ -319,6 +340,20 @@ def startedAt: (.started_at // "") | tostring;
|
|
|
319
340
|
def rankable:
|
|
320
341
|
test("^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?Z$");
|
|
321
342
|
|
|
343
|
+
def nonemptyString:
|
|
344
|
+
type == "string" and length > 0;
|
|
345
|
+
|
|
346
|
+
def distinctNames:
|
|
347
|
+
(map(.name) | length) == (map(.name) | unique | length);
|
|
348
|
+
|
|
349
|
+
def releaseAuthorized($release; $commands; $stamp):
|
|
350
|
+
([ $commands[] | select(.name == $release.name) ]) as $commandRows
|
|
351
|
+
| ([ $stamp.targets[] | select(.name == $release.impactTarget) ]) as $targetRows
|
|
352
|
+
| ($commandRows | length) == 1
|
|
353
|
+
and $commandRows[0].impactTarget == $release.impactTarget
|
|
354
|
+
and ($targetRows | length) == 1
|
|
355
|
+
and $targetRows[0].impact == "not-affected";
|
|
356
|
+
|
|
322
357
|
[ .[]
|
|
323
358
|
| (.check_runs // [])[]
|
|
324
359
|
| select(.name == $name)
|
|
@@ -326,20 +361,60 @@ def rankable:
|
|
|
326
361
|
| { started: startedAt,
|
|
327
362
|
status: (.status // ""),
|
|
328
363
|
conclusion: (.conclusion // ""),
|
|
364
|
+
url: ((.html_url // "") | tostring),
|
|
329
365
|
binding: binding }
|
|
330
366
|
] as $runs
|
|
331
367
|
| ($runs | map(.started | rankable) | all) as $orderable
|
|
332
368
|
| ($runs | map(.started | sub("\\.[0-9]+Z$"; "Z")) | max) as $newest
|
|
333
369
|
| [ $runs[] | select((.started | sub("\\.[0-9]+Z$"; "Z")) == $newest) ] as $generation
|
|
334
|
-
| (if ($runs | length) == 0 then ["none", "", ""]
|
|
335
|
-
elif ($orderable | not) then ["ambiguous", "", ""]
|
|
336
|
-
elif ($generation | length) != 1 then ["ambiguous", "", ""]
|
|
370
|
+
| (if ($runs | length) == 0 then ["none", "", "", ""]
|
|
371
|
+
elif ($orderable | not) then ["ambiguous", "", "", ""]
|
|
372
|
+
elif ($generation | length) != 1 then ["ambiguous", "", "", ""]
|
|
337
373
|
else
|
|
338
374
|
$generation[0] as $run
|
|
339
375
|
| (if ($run.binding | type) == "object" then $run.binding else {} end) as $proof
|
|
340
376
|
| (($proof.mode // "") | tostring) as $mode
|
|
341
377
|
| (($proof.executedCommands // []) | map(select(type == "string"))) as $executed
|
|
342
|
-
| ($
|
|
378
|
+
| ($proof | has("notRequiredCommands")) as $hasReleases
|
|
379
|
+
| (if $hasReleases then $proof.notRequiredCommands else [] end) as $releases
|
|
380
|
+
| ($proof.verificationCommands // null) as $commands
|
|
381
|
+
| ($proof.impactStamp // null) as $stamp
|
|
382
|
+
| (if ($hasReleases | not) then true
|
|
383
|
+
else
|
|
384
|
+
($proof.proofSchemaVersion == 4)
|
|
385
|
+
and ($releases | type) == "array"
|
|
386
|
+
and ($releases | length) > 0
|
|
387
|
+
and ($releases | all(.[];
|
|
388
|
+
type == "object"
|
|
389
|
+
and (.name | nonemptyString)
|
|
390
|
+
and (.impactTarget | nonemptyString)
|
|
391
|
+
and (.basis | nonemptyString)))
|
|
392
|
+
and ($releases | distinctNames)
|
|
393
|
+
and ($commands | type) == "array"
|
|
394
|
+
and ($commands | all(.[];
|
|
395
|
+
type == "object"
|
|
396
|
+
and (.name | nonemptyString)
|
|
397
|
+
and ((has("impactTarget") | not) or (.impactTarget | nonemptyString))))
|
|
398
|
+
and ($commands | distinctNames)
|
|
399
|
+
and ($stamp | type) == "object"
|
|
400
|
+
and ($stamp.stampVersion == ${TRUSTED_IMPACT_STAMP_VERSION})
|
|
401
|
+
and ($stamp.basis == "target-scoped")
|
|
402
|
+
and (($stamp.reasons | type) == "array")
|
|
403
|
+
and ($stamp.reasons | all(.[]; type == "string"))
|
|
404
|
+
and (($stamp.unsubscribedPaths | type) == "array")
|
|
405
|
+
and ($stamp.unsubscribedPaths | all(.[]; type == "string"))
|
|
406
|
+
and (($stamp.targets | type) == "array")
|
|
407
|
+
and ($stamp.targets | all(.[];
|
|
408
|
+
type == "object"
|
|
409
|
+
and (.name | nonemptyString)
|
|
410
|
+
and (.basis | nonemptyString)
|
|
411
|
+
and ((.impact == "affected") or (.impact == "not-affected"))))
|
|
412
|
+
and ($stamp.targets | distinctNames)
|
|
413
|
+
and (([$releases[].name] - $executed | length) == ($releases | length))
|
|
414
|
+
and ($releases | all(.[]; releaseAuthorized(.; $commands; $stamp)))
|
|
415
|
+
end) as $releasesValid
|
|
416
|
+
| (if $releasesValid then [$releases[].name] else [] end) as $released
|
|
417
|
+
| ($required - $executed - $released) as $missing
|
|
343
418
|
| (if $run.status != "completed" then "pending"
|
|
344
419
|
elif $run.conclusion != "success" then "failed"
|
|
345
420
|
elif (($run.binding | type) != "object")
|
|
@@ -349,10 +424,11 @@ def rankable:
|
|
|
349
424
|
or ($proof.repository != $repository) then "unreadable"
|
|
350
425
|
elif $proof.outcome != "passed" then "failed"
|
|
351
426
|
elif ($required | length) == 0 then "incomplete"
|
|
427
|
+
elif ($releasesValid | not) then "incomplete"
|
|
352
428
|
elif ($missing | length) != 0 then "incomplete"
|
|
353
429
|
else "proven"
|
|
354
430
|
end) as $reason
|
|
355
|
-
| [$reason, $mode, ($missing | join(", "))]
|
|
431
|
+
| [$reason, $mode, ($missing | join(", ")), $run.url]
|
|
356
432
|
end)
|
|
357
433
|
| map(gsub("[\\r\\n\\t]"; " "))
|
|
358
434
|
| join("\n")
|
|
@@ -404,6 +480,7 @@ reason=error
|
|
|
404
480
|
detail=''
|
|
405
481
|
mode=''
|
|
406
482
|
missing=''
|
|
483
|
+
source_url=''
|
|
407
484
|
|
|
408
485
|
# filter=all with full pagination is load-bearing (ADR 0022). GitHub's
|
|
409
486
|
# default "latest" filter is ordered by completion, so a newer generation
|
|
@@ -442,6 +519,7 @@ else
|
|
|
442
519
|
IFS= read -r reason || :
|
|
443
520
|
IFS= read -r mode || :
|
|
444
521
|
IFS= read -r missing || :
|
|
522
|
+
IFS= read -r source_url || :
|
|
445
523
|
} <<< "$finding"
|
|
446
524
|
case "$reason" in
|
|
447
525
|
proven | none | pending | failed | unreadable | incomplete | ambiguous) ;;
|
|
@@ -468,6 +546,9 @@ fi
|
|
|
468
546
|
# line so it cannot restructure the summary it is written into.
|
|
469
547
|
detail=$(printf '%s' "$detail" | tr '\n\r\t' ' ' | cut -c1-240)
|
|
470
548
|
missing=$(printf '%s' "$missing" | tr '\n\r\t' ' ' | cut -c1-240)
|
|
549
|
+
if ! [[ "$source_url" =~ ^https://github\.com/[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+/runs/[0-9]+$ ]]; then
|
|
550
|
+
source_url=''
|
|
551
|
+
fi
|
|
471
552
|
|
|
472
553
|
SUMMARY="${shellExpansion("GITHUB_STEP_SUMMARY:-/dev/null")}"
|
|
473
554
|
say() { printf '%s\n' "$1" >> "$SUMMARY"; }
|
|
@@ -480,7 +561,11 @@ case "$reason" in
|
|
|
480
561
|
echo "Factory proof: reusing local verification of $HEAD_SHA; skipping the $SURFACE suite."
|
|
481
562
|
say "Skipped. The factory already verified this exact commit, so this job did not run the $SURFACE suite a second time."
|
|
482
563
|
say ''
|
|
483
|
-
|
|
564
|
+
if [ -n "$source_url" ]; then
|
|
565
|
+
say "- Reused proof: [\`$CHECK_NAME\`]($source_url), published by the pinned factory GitHub App."
|
|
566
|
+
else
|
|
567
|
+
say "- Reused proof: the \`$CHECK_NAME\` check run published by the pinned factory GitHub App."
|
|
568
|
+
fi
|
|
484
569
|
say "- Covers head: \`$HEAD_SHA\`"
|
|
485
570
|
say "- Recorded mode: \`$mode\` (diagnostic only), outcome \`passed\`."
|
|
486
571
|
;;
|
|
@@ -515,7 +600,7 @@ case "$reason" in
|
|
|
515
600
|
incomplete)
|
|
516
601
|
say "Ran the full suite. The factory proof for this commit does not cover every command the $SURFACE surface requires."
|
|
517
602
|
say ''
|
|
518
|
-
say "The newest \`$CHECK_NAME\` check run at head \`$HEAD_SHA\` passed, but its \`
|
|
603
|
+
say "The newest \`$CHECK_NAME\` check run at head \`$HEAD_SHA\` passed, but its executed commands plus stamp-authorized \`notRequiredCommands\` do not cover this surface. Missing: \`$missing\`"
|
|
519
604
|
say ''
|
|
520
605
|
say '${CORRECTIVE_LINE}'
|
|
521
606
|
;;
|
|
@@ -546,6 +631,7 @@ fi
|
|
|
546
631
|
printf '${FACTORY_PROOF_GATE_OUTPUT}=%s\n' "$verdict"
|
|
547
632
|
printf '${FACTORY_PROOF_GATE_REASON_OUTPUT}=%s\n' "$reason"
|
|
548
633
|
printf '${FACTORY_PROOF_GATE_MODE_OUTPUT}=%s\n' "$mode"
|
|
634
|
+
printf '${FACTORY_PROOF_GATE_SOURCE_URL_OUTPUT}=%s\n' "$source_url"
|
|
549
635
|
} >> "${shellExpansion("GITHUB_OUTPUT:-/dev/null")}"
|
|
550
636
|
`.trim();
|
|
551
637
|
|
|
@@ -619,7 +705,7 @@ export const factoryProofGateStep = (
|
|
|
619
705
|
}),
|
|
620
706
|
id: FACTORY_PROOF_GATE_STEP_ID,
|
|
621
707
|
if: FACTORY_PROOF_GATE_IF,
|
|
622
|
-
name:
|
|
708
|
+
name: FACTORY_PROOF_GATE_STEP_NAME,
|
|
623
709
|
run: factoryProofGateScript(options),
|
|
624
710
|
// Never omit: the runner's default `run:` shell supplies `-e`, which
|
|
625
711
|
// voids the fail-open design. See FACTORY_PROOF_GATE_SHELL.
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/** Generic timing and presentation steps around the proof-reuse gate (#652). */
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
FACTORY_PROOF_GATE_MODE_OUTPUT,
|
|
5
|
+
FACTORY_PROOF_GATE_OUTPUT,
|
|
6
|
+
FACTORY_PROOF_GATE_REASON_OUTPUT,
|
|
7
|
+
FACTORY_PROOF_GATE_SOURCE_URL_OUTPUT,
|
|
8
|
+
FACTORY_PROOF_GATE_STEP_ID,
|
|
9
|
+
} from "./proof-reuse-gate.ts";
|
|
10
|
+
|
|
11
|
+
const githubExpression = (expression: string): string =>
|
|
12
|
+
`\${{ ${expression} }}`;
|
|
13
|
+
|
|
14
|
+
const shellExpansion = (expression: string): string => `\${${expression}}`;
|
|
15
|
+
|
|
16
|
+
const safeLabel = (surface: string): string => {
|
|
17
|
+
const cleaned = (typeof surface === "string" ? surface : "")
|
|
18
|
+
.replaceAll(/[^\w -]/gu, "")
|
|
19
|
+
.trim()
|
|
20
|
+
.slice(0, 60);
|
|
21
|
+
return cleaned.length > 0 ? cleaned : "verification";
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const FACTORY_PROOF_TIMING_STEP_ID = "ci-timing";
|
|
25
|
+
export const FACTORY_PROOF_TIMING_START_STEP_NAME = "Start CI timing";
|
|
26
|
+
export const FACTORY_PROOF_TIMING_SUMMARY_STEP_NAME =
|
|
27
|
+
"Record proof-reuse timing";
|
|
28
|
+
|
|
29
|
+
export interface FactoryProofReusePresentationOptions {
|
|
30
|
+
/** Human-readable name of the guarded suite. */
|
|
31
|
+
readonly surface: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface FactoryProofTimingStartStep {
|
|
35
|
+
readonly continueOnError: true;
|
|
36
|
+
readonly id: typeof FACTORY_PROOF_TIMING_STEP_ID;
|
|
37
|
+
readonly name: typeof FACTORY_PROOF_TIMING_START_STEP_NAME;
|
|
38
|
+
readonly run: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const factoryProofTimingStartStep = (): FactoryProofTimingStartStep =>
|
|
42
|
+
Object.freeze({
|
|
43
|
+
continueOnError: true as const,
|
|
44
|
+
id: FACTORY_PROOF_TIMING_STEP_ID,
|
|
45
|
+
name: FACTORY_PROOF_TIMING_START_STEP_NAME,
|
|
46
|
+
run: String.raw`started_ms="$(node -e 'process.stdout.write(String(Date.now()))')"
|
|
47
|
+
printf 'started_ms=%s\n' "$started_ms" >> "$GITHUB_OUTPUT"`,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
export const factoryProofReuseSummaryScript = ({
|
|
51
|
+
surface,
|
|
52
|
+
}: FactoryProofReusePresentationOptions): string => {
|
|
53
|
+
const label = safeLabel(surface);
|
|
54
|
+
return String.raw`now_ms="$(node -e 'process.stdout.write(String(Date.now()))')"
|
|
55
|
+
ci_started_ms="${shellExpansion("CI_STARTED_MS:-")}"
|
|
56
|
+
|
|
57
|
+
SUMMARY="${shellExpansion("GITHUB_STEP_SUMMARY:-/dev/null")}"
|
|
58
|
+
say() { printf '%s\n' "$1" >> "$SUMMARY"; }
|
|
59
|
+
|
|
60
|
+
say '## ${label} timing'
|
|
61
|
+
say ''
|
|
62
|
+
|
|
63
|
+
if [ "${shellExpansion("GITHUB_EVENT_NAME:-")}" = 'push' ]; then
|
|
64
|
+
say '- Path: merge-target full execution'
|
|
65
|
+
say '- Proof-reuse gate: not applicable on merge-target runs; the full suite ran.'
|
|
66
|
+
say "- Verified head: \`${shellExpansion("GITHUB_SHA:-unknown")}\`"
|
|
67
|
+
echo "::notice title=Factory proof reuse::Proof reuse is not applicable on merge-target runs; the full ${label} suite executed."
|
|
68
|
+
elif [ "${shellExpansion("PROOF_REUSED:-")}" = 'true' ]; then
|
|
69
|
+
say '- Path: trusted local proof reused'
|
|
70
|
+
if [ -n "${shellExpansion("PROOF_SOURCE_URL:-")}" ]; then
|
|
71
|
+
say "- Reused proof: [exact source check](${shellExpansion("PROOF_SOURCE_URL")})"
|
|
72
|
+
fi
|
|
73
|
+
say "- Bound head: \`${shellExpansion("PROOF_HEAD_SHA:-unknown")}\`"
|
|
74
|
+
say "- Factory verification mode: ${shellExpansion("PROOF_MODE:-unavailable")} (diagnostic only)"
|
|
75
|
+
echo "::notice title=Factory proof reuse::Reused trusted local proof for ${label} at ${shellExpansion("PROOF_HEAD_SHA:-unknown")}."
|
|
76
|
+
else
|
|
77
|
+
say "- Path: full GitHub CI fallback (${shellExpansion("PROOF_REASON:-error")})"
|
|
78
|
+
say "- Bound head: \`${shellExpansion("PROOF_HEAD_SHA:-unknown")}\`"
|
|
79
|
+
say "- Factory verification mode: ${shellExpansion("PROOF_MODE:-unavailable")} (diagnostic only)"
|
|
80
|
+
echo "::notice title=Factory proof reuse::No reusable proof for ${label}; the full hosted suite executed (${shellExpansion("PROOF_REASON:-error")})."
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
if [[ "$ci_started_ms" =~ ^[0-9]+$ ]]; then
|
|
84
|
+
say "- GitHub workflow execution after the proof gate: $((now_ms - ci_started_ms)) ms"
|
|
85
|
+
else
|
|
86
|
+
say '- GitHub workflow execution after the proof gate: unavailable'
|
|
87
|
+
fi
|
|
88
|
+
say ''`;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export interface FactoryProofReuseSummaryStep {
|
|
92
|
+
readonly continueOnError: true;
|
|
93
|
+
readonly env: Readonly<Record<string, string>>;
|
|
94
|
+
readonly if: "always()";
|
|
95
|
+
readonly name: typeof FACTORY_PROOF_TIMING_SUMMARY_STEP_NAME;
|
|
96
|
+
readonly run: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export const factoryProofReuseSummaryStep = (
|
|
100
|
+
options: FactoryProofReusePresentationOptions
|
|
101
|
+
): FactoryProofReuseSummaryStep =>
|
|
102
|
+
Object.freeze({
|
|
103
|
+
continueOnError: true as const,
|
|
104
|
+
env: Object.freeze({
|
|
105
|
+
CI_STARTED_MS: githubExpression(
|
|
106
|
+
`steps.${FACTORY_PROOF_TIMING_STEP_ID}.outputs.started_ms`
|
|
107
|
+
),
|
|
108
|
+
PROOF_HEAD_SHA: githubExpression("github.event.pull_request.head.sha"),
|
|
109
|
+
PROOF_MODE: githubExpression(
|
|
110
|
+
`steps.${FACTORY_PROOF_GATE_STEP_ID}.outputs.${FACTORY_PROOF_GATE_MODE_OUTPUT}`
|
|
111
|
+
),
|
|
112
|
+
PROOF_REASON: githubExpression(
|
|
113
|
+
`steps.${FACTORY_PROOF_GATE_STEP_ID}.outputs.${FACTORY_PROOF_GATE_REASON_OUTPUT}`
|
|
114
|
+
),
|
|
115
|
+
PROOF_REUSED: githubExpression(
|
|
116
|
+
`steps.${FACTORY_PROOF_GATE_STEP_ID}.outputs.${FACTORY_PROOF_GATE_OUTPUT}`
|
|
117
|
+
),
|
|
118
|
+
PROOF_SOURCE_URL: githubExpression(
|
|
119
|
+
`steps.${FACTORY_PROOF_GATE_STEP_ID}.outputs.${FACTORY_PROOF_GATE_SOURCE_URL_OUTPUT}`
|
|
120
|
+
),
|
|
121
|
+
}),
|
|
122
|
+
if: "always()" as const,
|
|
123
|
+
name: FACTORY_PROOF_TIMING_SUMMARY_STEP_NAME,
|
|
124
|
+
run: factoryProofReuseSummaryScript(options),
|
|
125
|
+
});
|