@patronage/factory-ci 1.0.0-alpha.21 → 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 +46 -10
- package/dist/index.d.ts +253 -27
- package/dist/index.js +374 -124
- 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 +18 -0
- package/src/merge-freeze-job.ts +237 -0
- package/src/pr-status-hud-workflow.ts +6 -8
- package/src/preview-proof-inventory.ts +22 -9
- package/src/preview-proof-lifecycle.ts +1 -8
- package/src/production-impact-workflow.ts +17 -14
- package/src/proof-reuse-gate.ts +152 -83
- package/src/push-identity-workflow.ts +27 -12
|
@@ -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
|
@@ -142,6 +142,41 @@ export const FACTORY_PROOF_GATE_GUARD = `steps.${FACTORY_PROOF_GATE_STEP_ID}.out
|
|
|
142
142
|
export const FACTORY_PROOF_GATE_IF =
|
|
143
143
|
"github.event_name == 'pull_request' || (github.event_name == 'push' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch))";
|
|
144
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Pull requests only — the condition a consumer gets with
|
|
147
|
+
* `reuse: "pull-request"` (#873).
|
|
148
|
+
*
|
|
149
|
+
* A repository may want its default-branch pushes to always execute the full
|
|
150
|
+
* hosted suite, whatever proof exists: the merged commit is what the fleet
|
|
151
|
+
* deploys, so a periodic unconditional run of every command is a deliberate
|
|
152
|
+
* cost some consumers choose to pay. This condition is that choice, expressed
|
|
153
|
+
* once here rather than as a consumer-written override string. The mode also
|
|
154
|
+
* skips the push-event merge fallback, so no push path can reuse proof even if
|
|
155
|
+
* the workflow reaches the step through some other trigger.
|
|
156
|
+
*
|
|
157
|
+
* Which branches trigger the workflow at all stays repository-owned. This
|
|
158
|
+
* condition only keeps the gate from consulting proof outside a pull request.
|
|
159
|
+
*/
|
|
160
|
+
export const FACTORY_PROOF_GATE_PULL_REQUEST_IF =
|
|
161
|
+
"github.event_name == 'pull_request'";
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Which events may reuse proof.
|
|
165
|
+
*
|
|
166
|
+
* A two-value enum, not a free-text condition: a string lets a consumer write
|
|
167
|
+
* a condition this package cannot reason about — one that reuses proof on an
|
|
168
|
+
* unproven ref — and the emitted condition is a trust predicate. The consumer
|
|
169
|
+
* chooses the mode; `factory-ci` owns what each mode emits.
|
|
170
|
+
*
|
|
171
|
+
* - `pull-request-and-default-branch` (default) pull requests plus pushes to
|
|
172
|
+
* the merge target, with the merge fallback. Today's behaviour.
|
|
173
|
+
* - `pull-request` pull requests only. Default-branch pushes execute the full
|
|
174
|
+
* hosted suite, and the merge fallback is not emitted.
|
|
175
|
+
*/
|
|
176
|
+
export type FactoryProofGateReuse =
|
|
177
|
+
| "pull-request"
|
|
178
|
+
| "pull-request-and-default-branch";
|
|
179
|
+
|
|
145
180
|
/**
|
|
146
181
|
* The complete refusal vocabulary. Deliberately few, because these are the
|
|
147
182
|
* only distinctions the gate can honestly make from its Checks API reads.
|
|
@@ -210,7 +245,7 @@ const COMMAND_IDENTITY_MAX_LENGTH = 120;
|
|
|
210
245
|
* cannot subtract hosted work. This is intentionally fail-closed and moves in
|
|
211
246
|
* lockstep with the proof/check-payload producer.
|
|
212
247
|
*/
|
|
213
|
-
const TRUSTED_IMPACT_STAMP_VERSION =
|
|
248
|
+
const TRUSTED_IMPACT_STAMP_VERSION = 4;
|
|
214
249
|
|
|
215
250
|
const isProofReuseCommand = (value: unknown): value is ProofReuseCommand => {
|
|
216
251
|
if (!(value && typeof value === "object")) {
|
|
@@ -429,7 +464,9 @@ def releaseAuthorized($release; $commands; $stamp):
|
|
|
429
464
|
type == "object"
|
|
430
465
|
and (.name | nonemptyString)
|
|
431
466
|
and (.basis | nonemptyString)
|
|
432
|
-
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"))))
|
|
433
470
|
and ($stamp.targets | distinctNames)
|
|
434
471
|
and (([$releases[].name] - $executed | length) == ($releases | length))
|
|
435
472
|
and ($releases | all(.[]; releaseAuthorized(.; $commands; $stamp)))
|
|
@@ -488,7 +525,99 @@ const safeLabel = (surface: string): string => {
|
|
|
488
525
|
return cleaned.length > 0 ? cleaned : "verification";
|
|
489
526
|
};
|
|
490
527
|
|
|
491
|
-
|
|
528
|
+
/**
|
|
529
|
+
* The push-event merge fallback (#611), emitted only for the default reuse
|
|
530
|
+
* mode. `reuse: "pull-request"` omits it: that mode's whole point is that a
|
|
531
|
+
* default-branch push executes everything, so a push path that could still
|
|
532
|
+
* reuse proof would contradict the condition above it.
|
|
533
|
+
*/
|
|
534
|
+
const MERGE_FALLBACK_BLOCK = String.raw`# Merge fallback (#611). A squash merge mints a new commit, so the direct
|
|
535
|
+
# read at a pushed merge-target head finds nothing even when the factory
|
|
536
|
+
# proved the producing pull request head. Only when the direct read found no
|
|
537
|
+
# generation at all on a push event, look up the producing pull request and
|
|
538
|
+
# reuse its head proof — and only when the merge commit's TREE id equals the
|
|
539
|
+
# proven head's tree id, which makes the pushed content byte-identical to
|
|
540
|
+
# what was verified (a clean squash of an unchanged tip). Patch identity was
|
|
541
|
+
# considered and rejected as the comparator: git patch-id normalizes
|
|
542
|
+
# whitespace and ignores base motion, so an identical patch can still
|
|
543
|
+
# produce an integrated tree that was never tested. Everything else — no
|
|
544
|
+
# unique merged producing PR, an unreadable commit, a proof that is anything
|
|
545
|
+
# but proven, or tree drift from a dirty or stale merge — leaves the direct
|
|
546
|
+
# "none" refusal standing, so the full suite runs (fail open).
|
|
547
|
+
if [ "$reason" = 'none' ] && [ "${shellExpansion("GITHUB_EVENT_NAME:-")}" = 'push' ]; then
|
|
548
|
+
producing_head=''
|
|
549
|
+
merge_tree=''
|
|
550
|
+
head_tree=''
|
|
551
|
+
if ! producing_pulls=$(gh api --method GET --paginate \
|
|
552
|
+
"repos/$GITHUB_REPOSITORY/commits/$HEAD_SHA/pulls" \
|
|
553
|
+
-f per_page=100 2>&1); then
|
|
554
|
+
detail="producing pull request unreadable: $producing_pulls"
|
|
555
|
+
elif ! producing_head=$(printf '%s' "$producing_pulls" | jq -r -s \
|
|
556
|
+
--arg sha "$HEAD_SHA" \
|
|
557
|
+
'[ add[]?
|
|
558
|
+
| select((.merged_at // null) != null)
|
|
559
|
+
| select((.merge_commit_sha // "") == $sha)
|
|
560
|
+
| ((.head.sha // "") | tostring) ]
|
|
561
|
+
| if length == 1 then .[0] else "" end' 2>&1); then
|
|
562
|
+
detail="producing pull request unreadable: $producing_head"
|
|
563
|
+
producing_head=''
|
|
564
|
+
elif ! [[ "$producing_head" =~ ^[0-9a-f]{40}$ ]]; then
|
|
565
|
+
detail='no single merged producing pull request at this commit'
|
|
566
|
+
producing_head=''
|
|
567
|
+
elif ! merge_commit=$(gh api --method GET \
|
|
568
|
+
"repos/$GITHUB_REPOSITORY/commits/$HEAD_SHA" 2>&1); then
|
|
569
|
+
detail="merge commit unreadable: $merge_commit"
|
|
570
|
+
elif ! merge_tree=$(printf '%s' "$merge_commit" | jq -r \
|
|
571
|
+
'(.commit.tree.sha // "") | tostring' 2>&1); then
|
|
572
|
+
detail="merge commit unreadable: $merge_tree"
|
|
573
|
+
merge_tree=''
|
|
574
|
+
elif ! [[ "$merge_tree" =~ ^[0-9a-f]{40}$ ]]; then
|
|
575
|
+
detail='merge commit carries no readable tree id'
|
|
576
|
+
merge_tree=''
|
|
577
|
+
elif ! head_commit=$(gh api --method GET \
|
|
578
|
+
"repos/$GITHUB_REPOSITORY/commits/$producing_head" 2>&1); then
|
|
579
|
+
detail="producing head commit unreadable: $head_commit"
|
|
580
|
+
elif ! head_tree=$(printf '%s' "$head_commit" | jq -r \
|
|
581
|
+
'(.commit.tree.sha // "") | tostring' 2>&1); then
|
|
582
|
+
detail="producing head commit unreadable: $head_tree"
|
|
583
|
+
head_tree=''
|
|
584
|
+
elif ! [[ "$head_tree" =~ ^[0-9a-f]{40}$ ]]; then
|
|
585
|
+
detail='producing head commit carries no readable tree id'
|
|
586
|
+
head_tree=''
|
|
587
|
+
elif [ "$merge_tree" != "$head_tree" ]; then
|
|
588
|
+
detail='tree drift: the merge result is not the proven head tree'
|
|
589
|
+
merge_tree=''
|
|
590
|
+
fi
|
|
591
|
+
if [[ "$producing_head" =~ ^[0-9a-f]{40}$ ]] \
|
|
592
|
+
&& [[ "$merge_tree" =~ ^[0-9a-f]{40}$ ]] \
|
|
593
|
+
&& [ "$merge_tree" = "$head_tree" ]; then
|
|
594
|
+
gate_lookup "$producing_head"
|
|
595
|
+
if [ "$lookup_reason" = 'proven' ]; then
|
|
596
|
+
reason=proven
|
|
597
|
+
mode="$lookup_mode"
|
|
598
|
+
missing=''
|
|
599
|
+
source_url="$lookup_url"
|
|
600
|
+
proof_head="$producing_head"
|
|
601
|
+
merge_note="the merge result's tree is byte-identical to the tree proven at pull request head $producing_head"
|
|
602
|
+
else
|
|
603
|
+
detail="producing pull request proof not reusable ($lookup_reason)"
|
|
604
|
+
fi
|
|
605
|
+
fi
|
|
606
|
+
fi`;
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* The fallback with the blank lines that surround it, or nothing at all. Kept
|
|
610
|
+
* as one piece so the default mode emits the exact script it emitted before
|
|
611
|
+
* this option existed.
|
|
612
|
+
*/
|
|
613
|
+
const mergeFallbackSection = (reuse: FactoryProofGateReuse): string =>
|
|
614
|
+
reuse === "pull-request" ? "\n" : `\n${MERGE_FALLBACK_BLOCK}\n\n`;
|
|
615
|
+
|
|
616
|
+
const gateScript = (
|
|
617
|
+
required: readonly string[],
|
|
618
|
+
surface: string,
|
|
619
|
+
reuse: FactoryProofGateReuse
|
|
620
|
+
): string =>
|
|
492
621
|
String.raw`
|
|
493
622
|
set -uo pipefail
|
|
494
623
|
|
|
@@ -576,82 +705,7 @@ else
|
|
|
576
705
|
missing="$lookup_missing"
|
|
577
706
|
source_url="$lookup_url"
|
|
578
707
|
fi
|
|
579
|
-
|
|
580
|
-
# Merge fallback (#611). A squash merge mints a new commit, so the direct
|
|
581
|
-
# read at a pushed merge-target head finds nothing even when the factory
|
|
582
|
-
# proved the producing pull request head. Only when the direct read found no
|
|
583
|
-
# generation at all on a push event, look up the producing pull request and
|
|
584
|
-
# reuse its head proof — and only when the merge commit's TREE id equals the
|
|
585
|
-
# proven head's tree id, which makes the pushed content byte-identical to
|
|
586
|
-
# what was verified (a clean squash of an unchanged tip). Patch identity was
|
|
587
|
-
# considered and rejected as the comparator: git patch-id normalizes
|
|
588
|
-
# whitespace and ignores base motion, so an identical patch can still
|
|
589
|
-
# produce an integrated tree that was never tested. Everything else — no
|
|
590
|
-
# unique merged producing PR, an unreadable commit, a proof that is anything
|
|
591
|
-
# but proven, or tree drift from a dirty or stale merge — leaves the direct
|
|
592
|
-
# "none" refusal standing, so the full suite runs (fail open).
|
|
593
|
-
if [ "$reason" = 'none' ] && [ "${shellExpansion("GITHUB_EVENT_NAME:-")}" = 'push' ]; then
|
|
594
|
-
producing_head=''
|
|
595
|
-
merge_tree=''
|
|
596
|
-
head_tree=''
|
|
597
|
-
if ! producing_pulls=$(gh api --method GET --paginate \
|
|
598
|
-
"repos/$GITHUB_REPOSITORY/commits/$HEAD_SHA/pulls" \
|
|
599
|
-
-f per_page=100 2>&1); then
|
|
600
|
-
detail="producing pull request unreadable: $producing_pulls"
|
|
601
|
-
elif ! producing_head=$(printf '%s' "$producing_pulls" | jq -r -s \
|
|
602
|
-
--arg sha "$HEAD_SHA" \
|
|
603
|
-
'[ add[]?
|
|
604
|
-
| select((.merged_at // null) != null)
|
|
605
|
-
| select((.merge_commit_sha // "") == $sha)
|
|
606
|
-
| ((.head.sha // "") | tostring) ]
|
|
607
|
-
| if length == 1 then .[0] else "" end' 2>&1); then
|
|
608
|
-
detail="producing pull request unreadable: $producing_head"
|
|
609
|
-
producing_head=''
|
|
610
|
-
elif ! [[ "$producing_head" =~ ^[0-9a-f]{40}$ ]]; then
|
|
611
|
-
detail='no single merged producing pull request at this commit'
|
|
612
|
-
producing_head=''
|
|
613
|
-
elif ! merge_commit=$(gh api --method GET \
|
|
614
|
-
"repos/$GITHUB_REPOSITORY/commits/$HEAD_SHA" 2>&1); then
|
|
615
|
-
detail="merge commit unreadable: $merge_commit"
|
|
616
|
-
elif ! merge_tree=$(printf '%s' "$merge_commit" | jq -r \
|
|
617
|
-
'(.commit.tree.sha // "") | tostring' 2>&1); then
|
|
618
|
-
detail="merge commit unreadable: $merge_tree"
|
|
619
|
-
merge_tree=''
|
|
620
|
-
elif ! [[ "$merge_tree" =~ ^[0-9a-f]{40}$ ]]; then
|
|
621
|
-
detail='merge commit carries no readable tree id'
|
|
622
|
-
merge_tree=''
|
|
623
|
-
elif ! head_commit=$(gh api --method GET \
|
|
624
|
-
"repos/$GITHUB_REPOSITORY/commits/$producing_head" 2>&1); then
|
|
625
|
-
detail="producing head commit unreadable: $head_commit"
|
|
626
|
-
elif ! head_tree=$(printf '%s' "$head_commit" | jq -r \
|
|
627
|
-
'(.commit.tree.sha // "") | tostring' 2>&1); then
|
|
628
|
-
detail="producing head commit unreadable: $head_tree"
|
|
629
|
-
head_tree=''
|
|
630
|
-
elif ! [[ "$head_tree" =~ ^[0-9a-f]{40}$ ]]; then
|
|
631
|
-
detail='producing head commit carries no readable tree id'
|
|
632
|
-
head_tree=''
|
|
633
|
-
elif [ "$merge_tree" != "$head_tree" ]; then
|
|
634
|
-
detail='tree drift: the merge result is not the proven head tree'
|
|
635
|
-
merge_tree=''
|
|
636
|
-
fi
|
|
637
|
-
if [[ "$producing_head" =~ ^[0-9a-f]{40}$ ]] \
|
|
638
|
-
&& [[ "$merge_tree" =~ ^[0-9a-f]{40}$ ]] \
|
|
639
|
-
&& [ "$merge_tree" = "$head_tree" ]; then
|
|
640
|
-
gate_lookup "$producing_head"
|
|
641
|
-
if [ "$lookup_reason" = 'proven' ]; then
|
|
642
|
-
reason=proven
|
|
643
|
-
mode="$lookup_mode"
|
|
644
|
-
missing=''
|
|
645
|
-
source_url="$lookup_url"
|
|
646
|
-
proof_head="$producing_head"
|
|
647
|
-
merge_note="the merge result's tree is byte-identical to the tree proven at pull request head $producing_head"
|
|
648
|
-
else
|
|
649
|
-
detail="producing pull request proof not reusable ($lookup_reason)"
|
|
650
|
-
fi
|
|
651
|
-
fi
|
|
652
|
-
fi
|
|
653
|
-
|
|
654
|
-
# The binding is App-verified, but it still reaches markdown. Only a plain
|
|
708
|
+
${mergeFallbackSection(reuse)}# The binding is App-verified, but it still reaches markdown. Only a plain
|
|
655
709
|
# lowercase token is quoted back; anything else is reported as unknown.
|
|
656
710
|
case "$mode" in
|
|
657
711
|
'') ;;
|
|
@@ -770,6 +824,13 @@ export interface FactoryProofGateOptions {
|
|
|
770
824
|
* therefore requires a different coverage.
|
|
771
825
|
*/
|
|
772
826
|
readonly commands: readonly ProofReuseCommand[];
|
|
827
|
+
/**
|
|
828
|
+
* Which events may reuse proof. Defaults to
|
|
829
|
+
* `"pull-request-and-default-branch"`, the behaviour every consumer has
|
|
830
|
+
* today. `"pull-request"` makes default-branch pushes execute the full
|
|
831
|
+
* hosted suite (#873).
|
|
832
|
+
*/
|
|
833
|
+
readonly reuse?: FactoryProofGateReuse;
|
|
773
834
|
/** Names the suite in the job summary. Changes no trust decision. */
|
|
774
835
|
readonly surface: string;
|
|
775
836
|
}
|
|
@@ -788,11 +849,12 @@ export interface FactoryProofGateOptions {
|
|
|
788
849
|
*/
|
|
789
850
|
export const factoryProofGateScript = ({
|
|
790
851
|
commands,
|
|
852
|
+
reuse = "pull-request-and-default-branch",
|
|
791
853
|
surface,
|
|
792
854
|
}: FactoryProofGateOptions): string => {
|
|
793
855
|
const required = proofReuseRequiredCommands(commands);
|
|
794
856
|
return required
|
|
795
|
-
? gateScript(required, safeLabel(surface))
|
|
857
|
+
? gateScript(required, safeLabel(surface), reuse)
|
|
796
858
|
: UNUSABLE_SELECTION_SCRIPT;
|
|
797
859
|
};
|
|
798
860
|
|
|
@@ -803,7 +865,9 @@ export interface FactoryProofGateStep {
|
|
|
803
865
|
HEAD_SHA: string;
|
|
804
866
|
}>;
|
|
805
867
|
readonly id: typeof FACTORY_PROOF_GATE_STEP_ID;
|
|
806
|
-
readonly if:
|
|
868
|
+
readonly if:
|
|
869
|
+
| typeof FACTORY_PROOF_GATE_IF
|
|
870
|
+
| typeof FACTORY_PROOF_GATE_PULL_REQUEST_IF;
|
|
807
871
|
readonly name: string;
|
|
808
872
|
readonly run: string;
|
|
809
873
|
readonly shell: typeof FACTORY_PROOF_GATE_SHELL;
|
|
@@ -817,7 +881,9 @@ export interface FactoryProofGateStep {
|
|
|
817
881
|
* `checks: read`; the push-event merge fallback additionally reads the
|
|
818
882
|
* producing pull request (`pull-requests: read`) and the two commit objects
|
|
819
883
|
* whose tree ids it compares (`contents: read`). A job that grants less
|
|
820
|
-
* loses only the fallback — the failed read degrades to the full suite.
|
|
884
|
+
* loses only the fallback — the failed read degrades to the full suite. A
|
|
885
|
+
* consumer that passes `reuse: "pull-request"` emits neither the push clause
|
|
886
|
+
* nor the fallback, and needs only `checks: read` (#873).
|
|
821
887
|
*
|
|
822
888
|
* A **step, not a job**, and that is not a style preference. A separate gate
|
|
823
889
|
* job that errored would leave the guarded job `skipped`, and a summary job
|
|
@@ -841,7 +907,10 @@ export const factoryProofGateStep = (
|
|
|
841
907
|
),
|
|
842
908
|
}),
|
|
843
909
|
id: FACTORY_PROOF_GATE_STEP_ID,
|
|
844
|
-
if:
|
|
910
|
+
if:
|
|
911
|
+
options.reuse === "pull-request"
|
|
912
|
+
? FACTORY_PROOF_GATE_PULL_REQUEST_IF
|
|
913
|
+
: FACTORY_PROOF_GATE_IF,
|
|
845
914
|
name: FACTORY_PROOF_GATE_STEP_NAME,
|
|
846
915
|
run: factoryProofGateScript(options),
|
|
847
916
|
// Never omit: the runner's default `run:` shell supplies `-e`, which
|
|
@@ -1,18 +1,28 @@
|
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
/**
|
|
16
|
+
* The artifact prefix and the five step IDs are module-private. Alpha.13
|
|
17
|
+
* removed them from the public surface (#719). A caller reads a step ID from
|
|
18
|
+
* the builder's returned `steps` (`step.id`) or from `artifactName`.
|
|
19
|
+
*/
|
|
20
|
+
const FACTORY_PUSH_IDENTITY_ARTIFACT_PREFIX = "factory-push-identity";
|
|
21
|
+
const FACTORY_PUSH_IDENTITY_RECORD_STEP_ID = "factory_push_identity_record";
|
|
22
|
+
const FACTORY_PUSH_IDENTITY_LOOKUP_STEP_ID = "factory_push_identity_lookup";
|
|
23
|
+
const FACTORY_PUSH_IDENTITY_DOWNLOAD_STEP_ID = "factory_push_identity_download";
|
|
24
|
+
const FACTORY_PUSH_IDENTITY_CHECKOUT_STEP_ID = "factory_push_identity_checkout";
|
|
25
|
+
const FACTORY_PUSH_IDENTITY_VALIDATE_STEP_ID = "factory_push_identity";
|
|
16
26
|
|
|
17
27
|
/** Versioned document uploaded by a push-triggered verification run. */
|
|
18
28
|
export interface FactoryPushIdentityEnvelope {
|
|
@@ -270,7 +280,12 @@ printf 'disposition=%s\nreason=%s\nbefore=%s\nafter=%s\nprovenance=%s\n' \
|
|
|
270
280
|
} >> "$GITHUB_STEP_SUMMARY"`;
|
|
271
281
|
|
|
272
282
|
export interface FactoryPushIdentityProducerOptions {
|
|
273
|
-
/**
|
|
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
|
+
*/
|
|
274
289
|
readonly uploadArtifact: PinnedAction;
|
|
275
290
|
/** Optional consumer trigger policy combined with the required push event. */
|
|
276
291
|
readonly if?: string;
|
|
@@ -291,7 +306,7 @@ export const factoryPushIdentityProducer = (
|
|
|
291
306
|
assertPinnedAction(
|
|
292
307
|
"uploadArtifact",
|
|
293
308
|
options.uploadArtifact,
|
|
294
|
-
|
|
309
|
+
UPLOAD_ARTIFACT_REPOSITORY
|
|
295
310
|
);
|
|
296
311
|
const condition = options.if
|
|
297
312
|
? `github.event_name == 'push' && (${options.if})`
|