@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.
@@ -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?.cleanupStatus !== "failed") {
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
- /** A fail-open condition: only an explicit usable withdrawal skips work. */
40
- readonly demandedIf: (targetName: string, decisionJob?: string) => string;
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: (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
- },
108
+ demandedIf: conditions.demandedIf,
109
+ demandedIfAtStep: conditions.demandedIfAtStep,
107
110
  targetOutputs: Object.freeze(targetOutputs),
108
111
  });
109
112
  };
@@ -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 = 3;
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
- const gateScript = (required: readonly string[], surface: string): string =>
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: typeof FACTORY_PROOF_GATE_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: FACTORY_PROOF_GATE_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
- export const FACTORY_PUSH_IDENTITY_ARTIFACT_PREFIX = "factory-push-identity";
7
- export const FACTORY_PUSH_IDENTITY_RECORD_STEP_ID =
8
- "factory_push_identity_record";
9
- export const FACTORY_PUSH_IDENTITY_LOOKUP_STEP_ID =
10
- "factory_push_identity_lookup";
11
- export const FACTORY_PUSH_IDENTITY_DOWNLOAD_STEP_ID =
12
- "factory_push_identity_download";
13
- export const FACTORY_PUSH_IDENTITY_CHECKOUT_STEP_ID =
14
- "factory_push_identity_checkout";
15
- export const FACTORY_PUSH_IDENTITY_VALIDATE_STEP_ID = "factory_push_identity";
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
- /** Explicit caller-owned upload-artifact pin from its workflow artifact. */
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
- "actions/upload-artifact"
309
+ UPLOAD_ARTIFACT_REPOSITORY
295
310
  );
296
311
  const condition = options.if
297
312
  ? `github.event_name == 'push' && (${options.if})`