@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
package/dist/index.js
CHANGED
|
@@ -32,6 +32,18 @@ const NODE_PNPM_ACTION_FAMILY_NODE24 = {
|
|
|
32
32
|
uses: "pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271"
|
|
33
33
|
}
|
|
34
34
|
};
|
|
35
|
+
/**
|
|
36
|
+
* The one `actions/upload-artifact` pin every workflow generator shares
|
|
37
|
+
* (#887). Two callers pinned two different commits of the same tag family;
|
|
38
|
+
* naming the pin here makes that drift impossible the same way the Node/pnpm
|
|
39
|
+
* family does. A caller passes this through `factoryWorkflow`'s
|
|
40
|
+
* `additionalActions.uploadArtifact` explicitly — the catalog names the pin,
|
|
41
|
+
* it does not inject the upload step.
|
|
42
|
+
*/
|
|
43
|
+
const UPLOAD_ARTIFACT = {
|
|
44
|
+
tag: "v4.6.2",
|
|
45
|
+
uses: "actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02"
|
|
46
|
+
};
|
|
35
47
|
//#endregion
|
|
36
48
|
//#region src/candidate-lifecycle.ts
|
|
37
49
|
/**
|
|
@@ -251,12 +263,10 @@ const transition = (state, event) => {
|
|
|
251
263
|
evidence: [],
|
|
252
264
|
outcome: "pending"
|
|
253
265
|
},
|
|
254
|
-
cleanupStatus: "pending",
|
|
255
266
|
convergence: {
|
|
256
267
|
detail: event.convergence.detail,
|
|
257
268
|
status: "passed"
|
|
258
269
|
},
|
|
259
|
-
convergenceStatus: "passed",
|
|
260
270
|
headSha: event.candidate.headSha,
|
|
261
271
|
pr: event.candidate.pr,
|
|
262
272
|
proof: {
|
|
@@ -269,7 +279,6 @@ const transition = (state, event) => {
|
|
|
269
279
|
detail: event.smoke.detail,
|
|
270
280
|
outcome: "passed"
|
|
271
281
|
},
|
|
272
|
-
smokeStatus: "passed",
|
|
273
282
|
source: "local-self-certified",
|
|
274
283
|
stack: event.stack,
|
|
275
284
|
stage,
|
|
@@ -288,12 +297,11 @@ const transition = (state, event) => {
|
|
|
288
297
|
};
|
|
289
298
|
return replaceRegistration$1(state, {
|
|
290
299
|
...existing,
|
|
291
|
-
cleanup
|
|
292
|
-
cleanupStatus: event.outcome
|
|
300
|
+
cleanup
|
|
293
301
|
});
|
|
294
302
|
};
|
|
295
303
|
const recover = (state, event) => {
|
|
296
|
-
if (state.registrations.find((row) => row.pr === event.pr && row.stack === event.stack && row.stage === event.stage)?.
|
|
304
|
+
if (state.registrations.find((row) => row.pr === event.pr && row.stack === event.stack && row.stage === event.stage)?.cleanup.outcome !== "failed") throw new Error("Preview proof recovery re-enters cleanup only from a failed outcome.");
|
|
297
305
|
return transition(state, event);
|
|
298
306
|
};
|
|
299
307
|
const previewProofLifecycle = {
|
|
@@ -519,6 +527,22 @@ const FACTORY_PROOF_GATE_GUARD = `steps.${FACTORY_PROOF_GATE_STEP_ID}.outputs.${
|
|
|
519
527
|
*/
|
|
520
528
|
const FACTORY_PROOF_GATE_IF = "github.event_name == 'pull_request' || (github.event_name == 'push' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch))";
|
|
521
529
|
/**
|
|
530
|
+
* Pull requests only — the condition a consumer gets with
|
|
531
|
+
* `reuse: "pull-request"` (#873).
|
|
532
|
+
*
|
|
533
|
+
* A repository may want its default-branch pushes to always execute the full
|
|
534
|
+
* hosted suite, whatever proof exists: the merged commit is what the fleet
|
|
535
|
+
* deploys, so a periodic unconditional run of every command is a deliberate
|
|
536
|
+
* cost some consumers choose to pay. This condition is that choice, expressed
|
|
537
|
+
* once here rather than as a consumer-written override string. The mode also
|
|
538
|
+
* skips the push-event merge fallback, so no push path can reuse proof even if
|
|
539
|
+
* the workflow reaches the step through some other trigger.
|
|
540
|
+
*
|
|
541
|
+
* Which branches trigger the workflow at all stays repository-owned. This
|
|
542
|
+
* condition only keeps the gate from consulting proof outside a pull request.
|
|
543
|
+
*/
|
|
544
|
+
const FACTORY_PROOF_GATE_PULL_REQUEST_IF = "github.event_name == 'pull_request'";
|
|
545
|
+
/**
|
|
522
546
|
* The complete refusal vocabulary. Deliberately few, because these are the
|
|
523
547
|
* only distinctions the gate can honestly make from its Checks API reads.
|
|
524
548
|
* The merge fallback adds no words: a fallback that establishes nothing
|
|
@@ -570,7 +594,7 @@ const COMMAND_IDENTITY_MAX_LENGTH = 120;
|
|
|
570
594
|
* cannot subtract hosted work. This is intentionally fail-closed and moves in
|
|
571
595
|
* lockstep with the proof/check-payload producer.
|
|
572
596
|
*/
|
|
573
|
-
const TRUSTED_IMPACT_STAMP_VERSION =
|
|
597
|
+
const TRUSTED_IMPACT_STAMP_VERSION = 4;
|
|
574
598
|
const isProofReuseCommand = (value) => {
|
|
575
599
|
if (!(value && typeof value === "object")) return false;
|
|
576
600
|
const entry = value;
|
|
@@ -763,7 +787,9 @@ def releaseAuthorized($release; $commands; $stamp):
|
|
|
763
787
|
type == "object"
|
|
764
788
|
and (.name | nonemptyString)
|
|
765
789
|
and (.basis | nonemptyString)
|
|
766
|
-
and ((.impact == "affected") or (.impact == "not-affected"))
|
|
790
|
+
and ((.impact == "affected") or (.impact == "not-affected"))
|
|
791
|
+
and ((.subscribedPaths | type) == "array")
|
|
792
|
+
and (.subscribedPaths | all(.[]; type == "string"))))
|
|
767
793
|
and ($stamp.targets | distinctNames)
|
|
768
794
|
and (([$releases[].name] - $executed | length) == ($releases | length))
|
|
769
795
|
and ($releases | all(.[]; releaseAuthorized(.; $commands; $stamp)))
|
|
@@ -814,7 +840,92 @@ const safeLabel$1 = (surface) => {
|
|
|
814
840
|
const cleaned = (typeof surface === "string" ? surface : "").replaceAll(/[^\w -]/gu, "").trim().slice(0, 60);
|
|
815
841
|
return cleaned.length > 0 ? cleaned : "verification";
|
|
816
842
|
};
|
|
817
|
-
|
|
843
|
+
/**
|
|
844
|
+
* The push-event merge fallback (#611), emitted only for the default reuse
|
|
845
|
+
* mode. `reuse: "pull-request"` omits it: that mode's whole point is that a
|
|
846
|
+
* default-branch push executes everything, so a push path that could still
|
|
847
|
+
* reuse proof would contradict the condition above it.
|
|
848
|
+
*/
|
|
849
|
+
const MERGE_FALLBACK_BLOCK = String.raw`# Merge fallback (#611). A squash merge mints a new commit, so the direct
|
|
850
|
+
# read at a pushed merge-target head finds nothing even when the factory
|
|
851
|
+
# proved the producing pull request head. Only when the direct read found no
|
|
852
|
+
# generation at all on a push event, look up the producing pull request and
|
|
853
|
+
# reuse its head proof — and only when the merge commit's TREE id equals the
|
|
854
|
+
# proven head's tree id, which makes the pushed content byte-identical to
|
|
855
|
+
# what was verified (a clean squash of an unchanged tip). Patch identity was
|
|
856
|
+
# considered and rejected as the comparator: git patch-id normalizes
|
|
857
|
+
# whitespace and ignores base motion, so an identical patch can still
|
|
858
|
+
# produce an integrated tree that was never tested. Everything else — no
|
|
859
|
+
# unique merged producing PR, an unreadable commit, a proof that is anything
|
|
860
|
+
# but proven, or tree drift from a dirty or stale merge — leaves the direct
|
|
861
|
+
# "none" refusal standing, so the full suite runs (fail open).
|
|
862
|
+
if [ "$reason" = 'none' ] && [ "${shellExpansion$1("GITHUB_EVENT_NAME:-")}" = 'push' ]; then
|
|
863
|
+
producing_head=''
|
|
864
|
+
merge_tree=''
|
|
865
|
+
head_tree=''
|
|
866
|
+
if ! producing_pulls=$(gh api --method GET --paginate \
|
|
867
|
+
"repos/$GITHUB_REPOSITORY/commits/$HEAD_SHA/pulls" \
|
|
868
|
+
-f per_page=100 2>&1); then
|
|
869
|
+
detail="producing pull request unreadable: $producing_pulls"
|
|
870
|
+
elif ! producing_head=$(printf '%s' "$producing_pulls" | jq -r -s \
|
|
871
|
+
--arg sha "$HEAD_SHA" \
|
|
872
|
+
'[ add[]?
|
|
873
|
+
| select((.merged_at // null) != null)
|
|
874
|
+
| select((.merge_commit_sha // "") == $sha)
|
|
875
|
+
| ((.head.sha // "") | tostring) ]
|
|
876
|
+
| if length == 1 then .[0] else "" end' 2>&1); then
|
|
877
|
+
detail="producing pull request unreadable: $producing_head"
|
|
878
|
+
producing_head=''
|
|
879
|
+
elif ! [[ "$producing_head" =~ ^[0-9a-f]{40}$ ]]; then
|
|
880
|
+
detail='no single merged producing pull request at this commit'
|
|
881
|
+
producing_head=''
|
|
882
|
+
elif ! merge_commit=$(gh api --method GET \
|
|
883
|
+
"repos/$GITHUB_REPOSITORY/commits/$HEAD_SHA" 2>&1); then
|
|
884
|
+
detail="merge commit unreadable: $merge_commit"
|
|
885
|
+
elif ! merge_tree=$(printf '%s' "$merge_commit" | jq -r \
|
|
886
|
+
'(.commit.tree.sha // "") | tostring' 2>&1); then
|
|
887
|
+
detail="merge commit unreadable: $merge_tree"
|
|
888
|
+
merge_tree=''
|
|
889
|
+
elif ! [[ "$merge_tree" =~ ^[0-9a-f]{40}$ ]]; then
|
|
890
|
+
detail='merge commit carries no readable tree id'
|
|
891
|
+
merge_tree=''
|
|
892
|
+
elif ! head_commit=$(gh api --method GET \
|
|
893
|
+
"repos/$GITHUB_REPOSITORY/commits/$producing_head" 2>&1); then
|
|
894
|
+
detail="producing head commit unreadable: $head_commit"
|
|
895
|
+
elif ! head_tree=$(printf '%s' "$head_commit" | jq -r \
|
|
896
|
+
'(.commit.tree.sha // "") | tostring' 2>&1); then
|
|
897
|
+
detail="producing head commit unreadable: $head_tree"
|
|
898
|
+
head_tree=''
|
|
899
|
+
elif ! [[ "$head_tree" =~ ^[0-9a-f]{40}$ ]]; then
|
|
900
|
+
detail='producing head commit carries no readable tree id'
|
|
901
|
+
head_tree=''
|
|
902
|
+
elif [ "$merge_tree" != "$head_tree" ]; then
|
|
903
|
+
detail='tree drift: the merge result is not the proven head tree'
|
|
904
|
+
merge_tree=''
|
|
905
|
+
fi
|
|
906
|
+
if [[ "$producing_head" =~ ^[0-9a-f]{40}$ ]] \
|
|
907
|
+
&& [[ "$merge_tree" =~ ^[0-9a-f]{40}$ ]] \
|
|
908
|
+
&& [ "$merge_tree" = "$head_tree" ]; then
|
|
909
|
+
gate_lookup "$producing_head"
|
|
910
|
+
if [ "$lookup_reason" = 'proven' ]; then
|
|
911
|
+
reason=proven
|
|
912
|
+
mode="$lookup_mode"
|
|
913
|
+
missing=''
|
|
914
|
+
source_url="$lookup_url"
|
|
915
|
+
proof_head="$producing_head"
|
|
916
|
+
merge_note="the merge result's tree is byte-identical to the tree proven at pull request head $producing_head"
|
|
917
|
+
else
|
|
918
|
+
detail="producing pull request proof not reusable ($lookup_reason)"
|
|
919
|
+
fi
|
|
920
|
+
fi
|
|
921
|
+
fi`;
|
|
922
|
+
/**
|
|
923
|
+
* The fallback with the blank lines that surround it, or nothing at all. Kept
|
|
924
|
+
* as one piece so the default mode emits the exact script it emitted before
|
|
925
|
+
* this option existed.
|
|
926
|
+
*/
|
|
927
|
+
const mergeFallbackSection = (reuse) => reuse === "pull-request" ? "\n" : `\n${MERGE_FALLBACK_BLOCK}\n\n`;
|
|
928
|
+
const gateScript = (required, surface, reuse) => String.raw`
|
|
818
929
|
set -uo pipefail
|
|
819
930
|
|
|
820
931
|
CHECK_NAME=${shellSingleQuote(FACTORY_PROOF_GATE_CHECK_NAME)}
|
|
@@ -901,82 +1012,7 @@ else
|
|
|
901
1012
|
missing="$lookup_missing"
|
|
902
1013
|
source_url="$lookup_url"
|
|
903
1014
|
fi
|
|
904
|
-
|
|
905
|
-
# Merge fallback (#611). A squash merge mints a new commit, so the direct
|
|
906
|
-
# read at a pushed merge-target head finds nothing even when the factory
|
|
907
|
-
# proved the producing pull request head. Only when the direct read found no
|
|
908
|
-
# generation at all on a push event, look up the producing pull request and
|
|
909
|
-
# reuse its head proof — and only when the merge commit's TREE id equals the
|
|
910
|
-
# proven head's tree id, which makes the pushed content byte-identical to
|
|
911
|
-
# what was verified (a clean squash of an unchanged tip). Patch identity was
|
|
912
|
-
# considered and rejected as the comparator: git patch-id normalizes
|
|
913
|
-
# whitespace and ignores base motion, so an identical patch can still
|
|
914
|
-
# produce an integrated tree that was never tested. Everything else — no
|
|
915
|
-
# unique merged producing PR, an unreadable commit, a proof that is anything
|
|
916
|
-
# but proven, or tree drift from a dirty or stale merge — leaves the direct
|
|
917
|
-
# "none" refusal standing, so the full suite runs (fail open).
|
|
918
|
-
if [ "$reason" = 'none' ] && [ "${shellExpansion$1("GITHUB_EVENT_NAME:-")}" = 'push' ]; then
|
|
919
|
-
producing_head=''
|
|
920
|
-
merge_tree=''
|
|
921
|
-
head_tree=''
|
|
922
|
-
if ! producing_pulls=$(gh api --method GET --paginate \
|
|
923
|
-
"repos/$GITHUB_REPOSITORY/commits/$HEAD_SHA/pulls" \
|
|
924
|
-
-f per_page=100 2>&1); then
|
|
925
|
-
detail="producing pull request unreadable: $producing_pulls"
|
|
926
|
-
elif ! producing_head=$(printf '%s' "$producing_pulls" | jq -r -s \
|
|
927
|
-
--arg sha "$HEAD_SHA" \
|
|
928
|
-
'[ add[]?
|
|
929
|
-
| select((.merged_at // null) != null)
|
|
930
|
-
| select((.merge_commit_sha // "") == $sha)
|
|
931
|
-
| ((.head.sha // "") | tostring) ]
|
|
932
|
-
| if length == 1 then .[0] else "" end' 2>&1); then
|
|
933
|
-
detail="producing pull request unreadable: $producing_head"
|
|
934
|
-
producing_head=''
|
|
935
|
-
elif ! [[ "$producing_head" =~ ^[0-9a-f]{40}$ ]]; then
|
|
936
|
-
detail='no single merged producing pull request at this commit'
|
|
937
|
-
producing_head=''
|
|
938
|
-
elif ! merge_commit=$(gh api --method GET \
|
|
939
|
-
"repos/$GITHUB_REPOSITORY/commits/$HEAD_SHA" 2>&1); then
|
|
940
|
-
detail="merge commit unreadable: $merge_commit"
|
|
941
|
-
elif ! merge_tree=$(printf '%s' "$merge_commit" | jq -r \
|
|
942
|
-
'(.commit.tree.sha // "") | tostring' 2>&1); then
|
|
943
|
-
detail="merge commit unreadable: $merge_tree"
|
|
944
|
-
merge_tree=''
|
|
945
|
-
elif ! [[ "$merge_tree" =~ ^[0-9a-f]{40}$ ]]; then
|
|
946
|
-
detail='merge commit carries no readable tree id'
|
|
947
|
-
merge_tree=''
|
|
948
|
-
elif ! head_commit=$(gh api --method GET \
|
|
949
|
-
"repos/$GITHUB_REPOSITORY/commits/$producing_head" 2>&1); then
|
|
950
|
-
detail="producing head commit unreadable: $head_commit"
|
|
951
|
-
elif ! head_tree=$(printf '%s' "$head_commit" | jq -r \
|
|
952
|
-
'(.commit.tree.sha // "") | tostring' 2>&1); then
|
|
953
|
-
detail="producing head commit unreadable: $head_tree"
|
|
954
|
-
head_tree=''
|
|
955
|
-
elif ! [[ "$head_tree" =~ ^[0-9a-f]{40}$ ]]; then
|
|
956
|
-
detail='producing head commit carries no readable tree id'
|
|
957
|
-
head_tree=''
|
|
958
|
-
elif [ "$merge_tree" != "$head_tree" ]; then
|
|
959
|
-
detail='tree drift: the merge result is not the proven head tree'
|
|
960
|
-
merge_tree=''
|
|
961
|
-
fi
|
|
962
|
-
if [[ "$producing_head" =~ ^[0-9a-f]{40}$ ]] \
|
|
963
|
-
&& [[ "$merge_tree" =~ ^[0-9a-f]{40}$ ]] \
|
|
964
|
-
&& [ "$merge_tree" = "$head_tree" ]; then
|
|
965
|
-
gate_lookup "$producing_head"
|
|
966
|
-
if [ "$lookup_reason" = 'proven' ]; then
|
|
967
|
-
reason=proven
|
|
968
|
-
mode="$lookup_mode"
|
|
969
|
-
missing=''
|
|
970
|
-
source_url="$lookup_url"
|
|
971
|
-
proof_head="$producing_head"
|
|
972
|
-
merge_note="the merge result's tree is byte-identical to the tree proven at pull request head $producing_head"
|
|
973
|
-
else
|
|
974
|
-
detail="producing pull request proof not reusable ($lookup_reason)"
|
|
975
|
-
fi
|
|
976
|
-
fi
|
|
977
|
-
fi
|
|
978
|
-
|
|
979
|
-
# The binding is App-verified, but it still reaches markdown. Only a plain
|
|
1015
|
+
${mergeFallbackSection(reuse)}# The binding is App-verified, but it still reaches markdown. Only a plain
|
|
980
1016
|
# lowercase token is quoted back; anything else is reported as unknown.
|
|
981
1017
|
case "$mode" in
|
|
982
1018
|
'') ;;
|
|
@@ -1099,9 +1135,9 @@ fi
|
|
|
1099
1135
|
* `FACTORY_PROOF_GATE_SHELL` — so the script is also written to reach that
|
|
1100
1136
|
* write under errexit, and the tests execute it both ways.
|
|
1101
1137
|
*/
|
|
1102
|
-
const factoryProofGateScript = ({ commands, surface }) => {
|
|
1138
|
+
const factoryProofGateScript = ({ commands, reuse = "pull-request-and-default-branch", surface }) => {
|
|
1103
1139
|
const required = proofReuseRequiredCommands(commands);
|
|
1104
|
-
return required ? gateScript(required, safeLabel$1(surface)) : UNUSABLE_SELECTION_SCRIPT;
|
|
1140
|
+
return required ? gateScript(required, safeLabel$1(surface), reuse) : UNUSABLE_SELECTION_SCRIPT;
|
|
1105
1141
|
};
|
|
1106
1142
|
/**
|
|
1107
1143
|
* The step itself, structurally accepted by gagen's `step()` without adding a
|
|
@@ -1111,7 +1147,9 @@ const factoryProofGateScript = ({ commands, surface }) => {
|
|
|
1111
1147
|
* `checks: read`; the push-event merge fallback additionally reads the
|
|
1112
1148
|
* producing pull request (`pull-requests: read`) and the two commit objects
|
|
1113
1149
|
* whose tree ids it compares (`contents: read`). A job that grants less
|
|
1114
|
-
* loses only the fallback — the failed read degrades to the full suite.
|
|
1150
|
+
* loses only the fallback — the failed read degrades to the full suite. A
|
|
1151
|
+
* consumer that passes `reuse: "pull-request"` emits neither the push clause
|
|
1152
|
+
* nor the fallback, and needs only `checks: read` (#873).
|
|
1115
1153
|
*
|
|
1116
1154
|
* A **step, not a job**, and that is not a style preference. A separate gate
|
|
1117
1155
|
* job that errored would leave the guarded job `skipped`, and a summary job
|
|
@@ -1127,7 +1165,7 @@ const factoryProofGateStep = (options) => Object.freeze({
|
|
|
1127
1165
|
HEAD_SHA: githubExpression$1("github.event.pull_request.head.sha || github.sha")
|
|
1128
1166
|
}),
|
|
1129
1167
|
id: FACTORY_PROOF_GATE_STEP_ID,
|
|
1130
|
-
if: FACTORY_PROOF_GATE_IF,
|
|
1168
|
+
if: options.reuse === "pull-request" ? FACTORY_PROOF_GATE_PULL_REQUEST_IF : FACTORY_PROOF_GATE_IF,
|
|
1131
1169
|
name: FACTORY_PROOF_GATE_STEP_NAME,
|
|
1132
1170
|
run: factoryProofGateScript(options),
|
|
1133
1171
|
shell: FACTORY_PROOF_GATE_SHELL
|
|
@@ -1185,6 +1223,20 @@ const assertProofReuseCoverage = (input) => {
|
|
|
1185
1223
|
*/
|
|
1186
1224
|
/** Check-run name that carries one registration JSON payload. Not a gate. */
|
|
1187
1225
|
const PREVIEW_PROOF_INVENTORY_CHECK_NAME = "patronage-factory/preview-proof";
|
|
1226
|
+
/**
|
|
1227
|
+
* Least privilege for a job whose `GITHUB_TOKEN` calls `list`. A
|
|
1228
|
+
* `permissions` block zeroes every unlisted scope, and `list` reads five
|
|
1229
|
+
* endpoints: `pulls/{pr}`, `pulls/{pr}/commits`, and the GraphQL force-push
|
|
1230
|
+
* timeline (`pull-requests`), `compare` (`contents`), and
|
|
1231
|
+
* `commits/{sha}/check-runs` (`checks`). A job
|
|
1232
|
+
* that omits `pull-requests: read` fails with 403 on a same-repository PR
|
|
1233
|
+
* and skips every destroy matrix (#859). Give the discovery job this object.
|
|
1234
|
+
*/
|
|
1235
|
+
const PREVIEW_PROOF_INVENTORY_LIST_PERMISSIONS = Object.freeze({
|
|
1236
|
+
checks: "read",
|
|
1237
|
+
contents: "read",
|
|
1238
|
+
"pull-requests": "read"
|
|
1239
|
+
});
|
|
1188
1240
|
const INVENTORY_KIND = "patronage-factory-preview-proof";
|
|
1189
1241
|
const INVENTORY_SCHEMA_VERSION = 1;
|
|
1190
1242
|
const DEFAULT_TIMEOUT_MS = 5e3;
|
|
@@ -1238,7 +1290,7 @@ const readIdentity = (value) => {
|
|
|
1238
1290
|
const readCleanup = (value) => {
|
|
1239
1291
|
if (!isRecord$1(value.cleanup) || !isCleanupStatus(value.cleanup.outcome)) return null;
|
|
1240
1292
|
const evidence = readStringArray(value.cleanup.evidence);
|
|
1241
|
-
if (evidence === null
|
|
1293
|
+
if (evidence === null) return null;
|
|
1242
1294
|
return typeof value.cleanup.runUrl === "string" && value.cleanup.runUrl.length > 0 ? {
|
|
1243
1295
|
evidence,
|
|
1244
1296
|
outcome: value.cleanup.outcome,
|
|
@@ -1250,7 +1302,7 @@ const readCleanup = (value) => {
|
|
|
1250
1302
|
};
|
|
1251
1303
|
const readPassingChecks = (value) => {
|
|
1252
1304
|
if (!isRecord$1(value.convergence) || !isRecord$1(value.smoke)) return null;
|
|
1253
|
-
if (value.convergence.status !== "passed" ||
|
|
1305
|
+
if (value.convergence.status !== "passed" || typeof value.convergence.detail !== "string" || value.smoke.outcome !== "passed" || typeof value.smoke.detail !== "string") return null;
|
|
1254
1306
|
return {
|
|
1255
1307
|
convergence: {
|
|
1256
1308
|
detail: value.convergence.detail,
|
|
@@ -1285,14 +1337,11 @@ const readPreviewProofRegistration = (value) => {
|
|
|
1285
1337
|
if (identity === null || cleanup === null || passing === null || proof === null) return null;
|
|
1286
1338
|
return {
|
|
1287
1339
|
cleanup,
|
|
1288
|
-
cleanupStatus: cleanup.outcome,
|
|
1289
1340
|
convergence: passing.convergence,
|
|
1290
|
-
convergenceStatus: "passed",
|
|
1291
1341
|
headSha: identity.headSha,
|
|
1292
1342
|
pr: identity.pr,
|
|
1293
1343
|
proof,
|
|
1294
1344
|
smoke: passing.smoke,
|
|
1295
|
-
smokeStatus: "passed",
|
|
1296
1345
|
source: "local-self-certified",
|
|
1297
1346
|
stack: identity.stack,
|
|
1298
1347
|
stage: identity.stage,
|
|
@@ -1311,6 +1360,7 @@ const assertPersistableRegistration = (registration) => {
|
|
|
1311
1360
|
const registrationKey = (row) => `${row.stack}:${row.stage}`;
|
|
1312
1361
|
const replaceRegistration = (rows, next) => [...rows.filter((row) => registrationKey(row) !== registrationKey(next)), next];
|
|
1313
1362
|
const memoryKey = (owner, repo, pr) => `${owner}/${repo}#${pr}`;
|
|
1363
|
+
/** Reached by callers as `previewProofInventory.memoryStore`. */
|
|
1314
1364
|
const memoryStore = () => {
|
|
1315
1365
|
const byPr = /* @__PURE__ */ new Map();
|
|
1316
1366
|
return {
|
|
@@ -1449,7 +1499,7 @@ const registrationFromCheckRun = (run, expectedHeadSha) => {
|
|
|
1449
1499
|
const checkRunOutput = (registration) => {
|
|
1450
1500
|
const title = `Preview proof ${registration.stack}/${registration.stage}`.slice(0, 255);
|
|
1451
1501
|
return {
|
|
1452
|
-
summary: `cleanup ${registration.
|
|
1502
|
+
summary: `cleanup ${registration.cleanup.outcome}`,
|
|
1453
1503
|
text: JSON.stringify({
|
|
1454
1504
|
kind: INVENTORY_KIND,
|
|
1455
1505
|
registration,
|
|
@@ -1601,6 +1651,7 @@ const takeNewestRegistrations = (runsBySha, pr) => {
|
|
|
1601
1651
|
}
|
|
1602
1652
|
return [...byKey.values()];
|
|
1603
1653
|
};
|
|
1654
|
+
/** Reached by callers as `previewProofInventory.githubStore`. */
|
|
1604
1655
|
const githubStore = (transport) => {
|
|
1605
1656
|
const request = transport.fetch ?? fetch;
|
|
1606
1657
|
const timeoutMs = transport.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
@@ -1695,6 +1746,7 @@ const recordCleanup = async (input, access) => {
|
|
|
1695
1746
|
const previewProofInventory = {
|
|
1696
1747
|
githubStore,
|
|
1697
1748
|
list,
|
|
1749
|
+
listPermissions: PREVIEW_PROOF_INVENTORY_LIST_PERMISSIONS,
|
|
1698
1750
|
memoryStore,
|
|
1699
1751
|
persist,
|
|
1700
1752
|
recordCleanup
|
|
@@ -1807,9 +1859,8 @@ const factoryWorkflow = (options) => {
|
|
|
1807
1859
|
const EXECUTE_ALCHEMY_ENTRY_MAX_BUFFER = 64 * 1024 * 1024;
|
|
1808
1860
|
let spawnImplementation = spawnSync;
|
|
1809
1861
|
const resolveBundledEntry = async (options) => {
|
|
1810
|
-
if (options.bundledEntry
|
|
1811
|
-
|
|
1812
|
-
return await bundleAlchemyEntry(options.bundle);
|
|
1862
|
+
if (options.bundledEntry === void 0) return await bundleAlchemyEntry(options.bundle);
|
|
1863
|
+
return path.resolve(options.cwd ?? process.cwd(), options.bundledEntry);
|
|
1813
1864
|
};
|
|
1814
1865
|
const captured = (text, redact) => {
|
|
1815
1866
|
if (text === null || redact === void 0) return text;
|
|
@@ -1819,8 +1870,8 @@ const captured = (text, redact) => {
|
|
|
1819
1870
|
* Bundle a consumer-owned Alchemy entry, resolve that consumer's Alchemy CLI,
|
|
1820
1871
|
* run it under the current Node binary, and return only on a successful exit.
|
|
1821
1872
|
*
|
|
1822
|
-
*
|
|
1823
|
-
* path. Captured stdout and stderr (`stdio: "pipe"`) can be redacted through
|
|
1873
|
+
* Options carry exactly one entry source: `bundle` to bundle now, or
|
|
1874
|
+
* `bundledEntry` to reuse an already-bundled path. Captured stdout and stderr (`stdio: "pipe"`) can be redacted through
|
|
1824
1875
|
* `redact` before they are returned; `stdio: "inherit"` still streams the
|
|
1825
1876
|
* child unredacted.
|
|
1826
1877
|
*
|
|
@@ -1926,6 +1977,24 @@ const factoryProofReuseSummaryStep = (options) => Object.freeze({
|
|
|
1926
1977
|
run: factoryProofReuseSummaryScript(options)
|
|
1927
1978
|
});
|
|
1928
1979
|
//#endregion
|
|
1980
|
+
//#region src/impact-demand-condition.ts
|
|
1981
|
+
const impactDemandConditions = (options) => {
|
|
1982
|
+
const targetOutput = (targetName) => {
|
|
1983
|
+
const output = options.targetOutputs[targetName];
|
|
1984
|
+
if (output === void 0) throw new Error(`Unknown ${options.label} impact target "${targetName}".`);
|
|
1985
|
+
return output;
|
|
1986
|
+
};
|
|
1987
|
+
const condition = (source, outcomeKey, output) => `${source}.${outcomeKey} != 'success' || ${source}.outputs.${options.decisionOutput} != 'usable' || ${source}.outputs.${output} != 'withdrawn'`;
|
|
1988
|
+
return Object.freeze({
|
|
1989
|
+
demandedIf: (targetName, decisionJob) => {
|
|
1990
|
+
const output = targetOutput(targetName);
|
|
1991
|
+
if (decisionJob.trim().length === 0) throw new Error(`A cross-job ${options.label} impact condition requires the decision job id.`);
|
|
1992
|
+
return `always() && (${condition(`needs.${decisionJob}`, "result", output)})`;
|
|
1993
|
+
},
|
|
1994
|
+
demandedIfAtStep: (targetName) => condition(`steps.${options.stepId}`, "outcome", targetOutput(targetName))
|
|
1995
|
+
});
|
|
1996
|
+
};
|
|
1997
|
+
//#endregion
|
|
1929
1998
|
//#region src/production-impact-workflow.ts
|
|
1930
1999
|
const FACTORY_PRODUCTION_IMPACT_STEP_ID = "production_impact";
|
|
1931
2000
|
const FACTORY_PRODUCTION_IMPACT_DECISION_OUTPUT = "decision";
|
|
@@ -1962,6 +2031,12 @@ const factoryProductionImpactWorkflow = (options) => {
|
|
|
1962
2031
|
name: "Classify production impact",
|
|
1963
2032
|
run: `${cli} production:impact --before "$FACTORY_BEFORE_SHA" --after "$FACTORY_AFTER_SHA" --github-output "$GITHUB_OUTPUT" --github-summary "$GITHUB_STEP_SUMMARY"${profile}`
|
|
1964
2033
|
};
|
|
2034
|
+
const conditions = impactDemandConditions({
|
|
2035
|
+
decisionOutput: FACTORY_PRODUCTION_IMPACT_DECISION_OUTPUT,
|
|
2036
|
+
label: "production",
|
|
2037
|
+
stepId: FACTORY_PRODUCTION_IMPACT_STEP_ID,
|
|
2038
|
+
targetOutputs
|
|
2039
|
+
});
|
|
1965
2040
|
return Object.freeze({
|
|
1966
2041
|
decisionJobOutputs: Object.freeze({
|
|
1967
2042
|
basis: `\${{ steps.${FACTORY_PRODUCTION_IMPACT_STEP_ID}.outputs.${FACTORY_PRODUCTION_IMPACT_BASIS_OUTPUT} }}`,
|
|
@@ -1970,13 +2045,8 @@ const factoryProductionImpactWorkflow = (options) => {
|
|
|
1970
2045
|
...Object.fromEntries(Object.values(targetOutputs).map((output) => [output, `\${{ steps.${FACTORY_PRODUCTION_IMPACT_STEP_ID}.outputs.${output} }}`]))
|
|
1971
2046
|
}),
|
|
1972
2047
|
decisionStep: Object.freeze(decisionStep),
|
|
1973
|
-
demandedIf:
|
|
1974
|
-
|
|
1975
|
-
if (output === void 0) throw new Error(`Unknown production impact target "${targetName}".`);
|
|
1976
|
-
const source = decisionJob ? `needs.${decisionJob}` : `steps.${FACTORY_PRODUCTION_IMPACT_STEP_ID}`;
|
|
1977
|
-
const condition = `${source}.${decisionJob ? "result" : "outcome"} != 'success' || ${source}.outputs.${FACTORY_PRODUCTION_IMPACT_DECISION_OUTPUT} != 'usable' || ${source}.outputs.${output} != 'withdrawn'`;
|
|
1978
|
-
return decisionJob ? `always() && (${condition})` : condition;
|
|
1979
|
-
},
|
|
2048
|
+
demandedIf: conditions.demandedIf,
|
|
2049
|
+
demandedIfAtStep: conditions.demandedIfAtStep,
|
|
1980
2050
|
targetOutputs: Object.freeze(targetOutputs)
|
|
1981
2051
|
});
|
|
1982
2052
|
};
|
|
@@ -2019,6 +2089,12 @@ const factoryCandidateImpactWorkflow = (options) => {
|
|
|
2019
2089
|
name: "Classify candidate impact",
|
|
2020
2090
|
run: `${cli} candidate:impact --base "$FACTORY_BASE_SHA" --head "$FACTORY_HEAD_SHA" --github-output "$GITHUB_OUTPUT" --github-summary "$GITHUB_STEP_SUMMARY"${profile}`
|
|
2021
2091
|
};
|
|
2092
|
+
const conditions = impactDemandConditions({
|
|
2093
|
+
decisionOutput: FACTORY_CANDIDATE_IMPACT_DECISION_OUTPUT,
|
|
2094
|
+
label: "candidate",
|
|
2095
|
+
stepId: FACTORY_CANDIDATE_IMPACT_STEP_ID,
|
|
2096
|
+
targetOutputs
|
|
2097
|
+
});
|
|
2022
2098
|
return Object.freeze({
|
|
2023
2099
|
decisionJobOutputs: Object.freeze({
|
|
2024
2100
|
basis: `\${{ steps.${FACTORY_CANDIDATE_IMPACT_STEP_ID}.outputs.${FACTORY_CANDIDATE_IMPACT_BASIS_OUTPUT} }}`,
|
|
@@ -2028,13 +2104,8 @@ const factoryCandidateImpactWorkflow = (options) => {
|
|
|
2028
2104
|
...Object.fromEntries(Object.values(targetOutputs).map((output) => [output, `\${{ steps.${FACTORY_CANDIDATE_IMPACT_STEP_ID}.outputs.${output} }}`]))
|
|
2029
2105
|
}),
|
|
2030
2106
|
decisionStep: Object.freeze(decisionStep),
|
|
2031
|
-
demandedIf:
|
|
2032
|
-
|
|
2033
|
-
if (output === void 0) throw new Error(`Unknown candidate impact target "${targetName}".`);
|
|
2034
|
-
const source = decisionJob ? `needs.${decisionJob}` : `steps.${FACTORY_CANDIDATE_IMPACT_STEP_ID}`;
|
|
2035
|
-
const condition = `${source}.${decisionJob ? "result" : "outcome"} != 'success' || ${source}.outputs.${FACTORY_CANDIDATE_IMPACT_DECISION_OUTPUT} != 'usable' || ${source}.outputs.${output} != 'withdrawn'`;
|
|
2036
|
-
return decisionJob ? `always() && (${condition})` : condition;
|
|
2037
|
-
},
|
|
2107
|
+
demandedIf: conditions.demandedIf,
|
|
2108
|
+
demandedIfAtStep: conditions.demandedIfAtStep,
|
|
2038
2109
|
targetOutputs: Object.freeze(targetOutputs)
|
|
2039
2110
|
});
|
|
2040
2111
|
};
|
|
@@ -2106,6 +2177,180 @@ const factoryLifecycleContractLane = (declaration) => {
|
|
|
2106
2177
|
});
|
|
2107
2178
|
};
|
|
2108
2179
|
//#endregion
|
|
2180
|
+
//#region src/merge-freeze-job.ts
|
|
2181
|
+
/**
|
|
2182
|
+
* The merge-freeze writer job (#356, ADR 0016 as amended; #429 for the
|
|
2183
|
+
* merge-target namespace; #872 for this package).
|
|
2184
|
+
*
|
|
2185
|
+
* A consumer's generated merge-target-push Verify workflow is the ONLY
|
|
2186
|
+
* producer of `patronage-factory/merge-freeze` generations: a red
|
|
2187
|
+
* push-triggered Verify completes a generation active, a green one completes
|
|
2188
|
+
* it inactive, and no command, scheduled job, or second workflow writes this
|
|
2189
|
+
* check run. The operator override (`demand:waive --demand merge-freeze`)
|
|
2190
|
+
* waives the demand for one candidate during a fix-forward; it never writes
|
|
2191
|
+
* here.
|
|
2192
|
+
*
|
|
2193
|
+
* The write mirrors what the factory package's merge-freeze reader parses:
|
|
2194
|
+
* create the run `in_progress` with no `started_at` (GitHub stamps the
|
|
2195
|
+
* generation clock — the ordering rule itself is owned by the reader), then
|
|
2196
|
+
* PATCH it completed with the reader-shaped state payload in `output.text`.
|
|
2197
|
+
* The consumer's workflow test executes this exact script against a stubbed
|
|
2198
|
+
* `gh` and parses the emitted payload with `validateMergeFreezeState`, so
|
|
2199
|
+
* writer and reader cannot drift silently.
|
|
2200
|
+
*
|
|
2201
|
+
* Those two API calls are what the reader's two settling phases observe
|
|
2202
|
+
* (#890). Before the POST the merge target carries no generation at all, which
|
|
2203
|
+
* a reader reports as `awaiting-generation` while the pushed hosted verify run
|
|
2204
|
+
* is still pending. Between the POST and the PATCH the generation exists with
|
|
2205
|
+
* no `output.text`, which a reader reports as `running-generation`. Only the
|
|
2206
|
+
* PATCH writes a verdict, so neither phase is one.
|
|
2207
|
+
*
|
|
2208
|
+
* Failure posture is deliberately fail-closed and the opposite of the
|
|
2209
|
+
* proof-reuse gate's fail-open shell: this step runs under the runner's
|
|
2210
|
+
* default `bash -e {0}`, so any API refusal aborts the step, the generation
|
|
2211
|
+
* stays absent or `in_progress`, and every `pr:ready` arming-time read of
|
|
2212
|
+
* that tip (#477) refuses until a later push writes a green generation.
|
|
2213
|
+
*
|
|
2214
|
+
* **Ownership line.** This package owns the check name, the App identity, the
|
|
2215
|
+
* `create-github-app-token` inputs, the result fold, the merge-target `if`,
|
|
2216
|
+
* and the job's `permissions`. The caller owns the runner, the `needs` list
|
|
2217
|
+
* of every authoritative verification leaf, the timeout, and any extra env.
|
|
2218
|
+
*/
|
|
2219
|
+
/** The one check-run name every merge-freeze reader pins. */
|
|
2220
|
+
const FACTORY_MERGE_FREEZE_CHECK_NAME = "patronage-factory/merge-freeze";
|
|
2221
|
+
const FACTORY_MERGE_FREEZE_JOB_ID = "freeze";
|
|
2222
|
+
const FACTORY_MERGE_FREEZE_JOB_NAME = "Report the merge freeze";
|
|
2223
|
+
/** Step id the token step exposes its installation token under. */
|
|
2224
|
+
const FACTORY_MERGE_FREEZE_APP_TOKEN_STEP_ID = "factory-app-token";
|
|
2225
|
+
/**
|
|
2226
|
+
* The GitHub Actions expression that folds the needed jobs' results into the
|
|
2227
|
+
* one word the script branches on. It mirrors the `verify` summary job's
|
|
2228
|
+
* failure condition exactly: any failed or cancelled needed job is a red
|
|
2229
|
+
* merge target.
|
|
2230
|
+
*/
|
|
2231
|
+
const FACTORY_MERGE_FREEZE_VERIFY_RESULT_EXPRESSION = `\${{ (contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')) && 'failure' || 'success' }}`;
|
|
2232
|
+
/**
|
|
2233
|
+
* The literal factory merge-target namespace, as a `github.ref` predicate
|
|
2234
|
+
* (#429).
|
|
2235
|
+
*
|
|
2236
|
+
* `main` plus the repository-owned `epic/**` integration branches, and
|
|
2237
|
+
* nothing else. Deliberately literal rather than profile-declared: one
|
|
2238
|
+
* implementation per contract, no configuration machinery for a two-element
|
|
2239
|
+
* namespace whose second element is a namespace prefix the repository itself
|
|
2240
|
+
* owns. `epic/` needs the trailing slash — `startsWith` would otherwise admit
|
|
2241
|
+
* `epicness/…`, which is not a merge target.
|
|
2242
|
+
*
|
|
2243
|
+
* A caller's `push` trigger branch list names the same namespace. That list
|
|
2244
|
+
* decides which events produce a run at all; this predicate decides which of
|
|
2245
|
+
* those runs may write a generation.
|
|
2246
|
+
*/
|
|
2247
|
+
const FACTORY_MERGE_TARGET_REF_CONDITION = ["github.ref == 'refs/heads/main'", "startsWith(github.ref, 'refs/heads/epic/')"].join(" || ");
|
|
2248
|
+
/**
|
|
2249
|
+
* `always()` is load-bearing: the red path is the whole point, so the job must
|
|
2250
|
+
* run when a needed verification job failed. `github.event_name == 'push'` is
|
|
2251
|
+
* load-bearing twice over: a `pull_request` run must never write a generation
|
|
2252
|
+
* (its `github.sha` is a synthetic merge commit that no base tip is ever read
|
|
2253
|
+
* at), and a `pull_request` trigger with an `epic/**` base list would
|
|
2254
|
+
* otherwise reach this job.
|
|
2255
|
+
*/
|
|
2256
|
+
const FACTORY_MERGE_FREEZE_IF = `always() && github.event_name == 'push' && (${FACTORY_MERGE_TARGET_REF_CONDITION})`;
|
|
2257
|
+
/**
|
|
2258
|
+
* Least privilege for the writer job: no `GITHUB_TOKEN` scope at all.
|
|
2259
|
+
*
|
|
2260
|
+
* Both Checks API calls run under the App installation token minted in the
|
|
2261
|
+
* first step (`GH_TOKEN: steps.factory-app-token.outputs.token`), and the job
|
|
2262
|
+
* never checks out the repository. So the job uses no `GITHUB_TOKEN` scope,
|
|
2263
|
+
* and an empty block is the honest declaration. A `checks: write` here would
|
|
2264
|
+
* hand every adopter an unused write scope (#884).
|
|
2265
|
+
*/
|
|
2266
|
+
const FACTORY_MERGE_FREEZE_PERMISSIONS = Object.freeze({});
|
|
2267
|
+
/**
|
|
2268
|
+
* The script the freeze step runs. Environment contract:
|
|
2269
|
+
* - `GH_TOKEN`: an installation token for the pinned Patronage Factory App —
|
|
2270
|
+
* the check run must carry that App's identity or every reader rejects it.
|
|
2271
|
+
* - `VERIFY_RESULT`: `success` or `failure` (see the fold expression above).
|
|
2272
|
+
* - `GITHUB_SHA` / `GITHUB_REPOSITORY` / `GITHUB_SERVER_URL` /
|
|
2273
|
+
* `GITHUB_RUN_ID` / `GITHUB_REF_NAME`: runner-provided.
|
|
2274
|
+
*
|
|
2275
|
+
* `GITHUB_REF_NAME` is the merge target this run pushed to — `main` or an
|
|
2276
|
+
* `epic/**` integration branch (#429). The job is gated to `push` events on
|
|
2277
|
+
* exactly those refs, so the branch name is always the short name of a
|
|
2278
|
+
* repository-owned merge target and never a pull-request merge ref. It
|
|
2279
|
+
* appears in both recovery sentences because a reader of an epic-targeting
|
|
2280
|
+
* candidate must be told which branch to fix forward on: the generation lives
|
|
2281
|
+
* on that branch's tip, and only a later green push to THAT branch clears it.
|
|
2282
|
+
*/
|
|
2283
|
+
const factoryMergeFreezeScript = () => [
|
|
2284
|
+
`sha="$GITHUB_SHA"`,
|
|
2285
|
+
`branch="$GITHUB_REF_NAME"`,
|
|
2286
|
+
`repo="$GITHUB_REPOSITORY"`,
|
|
2287
|
+
`run_url="$GITHUB_SERVER_URL/$repo/actions/runs/$GITHUB_RUN_ID"`,
|
|
2288
|
+
`created=$(gh api --method POST "repos/$repo/check-runs" \\`,
|
|
2289
|
+
` -f "name=${FACTORY_MERGE_FREEZE_CHECK_NAME}" \\`,
|
|
2290
|
+
` -f "head_sha=$sha" \\`,
|
|
2291
|
+
` -f "status=in_progress" \\`,
|
|
2292
|
+
` -f "details_url=$run_url" \\`,
|
|
2293
|
+
` -f "output[title]=merge freeze generation in progress" \\`,
|
|
2294
|
+
` -f "output[summary]=This generation blocks factory merge handoffs until the merge-target Verify result is recorded.")`,
|
|
2295
|
+
`id=$(jq -r '.id' <<<"$created")`,
|
|
2296
|
+
`case "$id" in`,
|
|
2297
|
+
` ''|*[!0-9]*) echo "unusable check-run id: $id" >&2; exit 1;;`,
|
|
2298
|
+
`esac`,
|
|
2299
|
+
`if [ "$VERIFY_RESULT" = 'success' ]; then`,
|
|
2300
|
+
` active=false; outcome=inactive; conclusion=success`,
|
|
2301
|
+
` reason="Merge-target Verify passed on $branch at $sha ($run_url)."`,
|
|
2302
|
+
`else`,
|
|
2303
|
+
` active=true; outcome=active; conclusion=failure`,
|
|
2304
|
+
` reason="Merge-target Verify failed on $branch at $sha; fix forward or revert on $branch and let the next green Verify push on $branch clear this freeze ($run_url)."`,
|
|
2305
|
+
`fi`,
|
|
2306
|
+
`state=$(jq -cn --argjson active "$active" --argjson id "$id" --arg headSha "$sha" --arg outcome "$outcome" --arg reason "$reason" \\`,
|
|
2307
|
+
` '{active: $active, generationId: $id, headSha: $headSha, outcome: $outcome, reason: $reason, recordedAt: (now | todate), schemaVersion: 1}')`,
|
|
2308
|
+
`gh api --method PATCH "repos/$repo/check-runs/$id" \\`,
|
|
2309
|
+
` -f "status=completed" \\`,
|
|
2310
|
+
` -f "conclusion=$conclusion" \\`,
|
|
2311
|
+
` -f "details_url=$run_url" \\`,
|
|
2312
|
+
` -f "output[title]=merge freeze $outcome" \\`,
|
|
2313
|
+
` -f "output[summary]=$reason" \\`,
|
|
2314
|
+
` -f "output[text]=$state" >/dev/null`,
|
|
2315
|
+
`echo "merge-freeze generation $id completed $outcome on $branch at $sha"`
|
|
2316
|
+
].join("\n");
|
|
2317
|
+
/**
|
|
2318
|
+
* Build the merge-freeze writer job.
|
|
2319
|
+
*
|
|
2320
|
+
* An empty `needs` list is refused: a fold over no verification leaf is
|
|
2321
|
+
* always `success`, so such a job would write a green generation for a merge
|
|
2322
|
+
* target nothing verified — the exact failure the freeze exists to prevent.
|
|
2323
|
+
*/
|
|
2324
|
+
const factoryMergeFreezeJob = (options) => {
|
|
2325
|
+
assertPinnedAction("createGithubAppToken", options.createGithubAppToken, "actions/create-github-app-token");
|
|
2326
|
+
if (options.needs.length === 0) throw new Error("factoryMergeFreezeJob needs at least one verification job: a fold over no needed job always reports success, so the freeze would report a merge target nothing verified as green.");
|
|
2327
|
+
const mintStep = Object.freeze({
|
|
2328
|
+
id: FACTORY_MERGE_FREEZE_APP_TOKEN_STEP_ID,
|
|
2329
|
+
name: "Mint the factory App token",
|
|
2330
|
+
uses: options.createGithubAppToken.uses,
|
|
2331
|
+
with: Object.freeze({
|
|
2332
|
+
"client-id": FACTORY_PROOF_GATE_APP_ID,
|
|
2333
|
+
"private-key": `\${{ secrets.FACTORY_GITHUB_APP_PRIVATE_KEY }}`
|
|
2334
|
+
})
|
|
2335
|
+
});
|
|
2336
|
+
const reportStep = Object.freeze({
|
|
2337
|
+
env: Object.freeze({
|
|
2338
|
+
GH_TOKEN: `\${{ steps.${FACTORY_MERGE_FREEZE_APP_TOKEN_STEP_ID}.outputs.token }}`,
|
|
2339
|
+
VERIFY_RESULT: options.verifyResultExpression ?? FACTORY_MERGE_FREEZE_VERIFY_RESULT_EXPRESSION
|
|
2340
|
+
}),
|
|
2341
|
+
name: "Complete the merge-freeze generation",
|
|
2342
|
+
run: factoryMergeFreezeScript()
|
|
2343
|
+
});
|
|
2344
|
+
return Object.freeze({
|
|
2345
|
+
if: FACTORY_MERGE_FREEZE_IF,
|
|
2346
|
+
jobId: FACTORY_MERGE_FREEZE_JOB_ID,
|
|
2347
|
+
jobName: FACTORY_MERGE_FREEZE_JOB_NAME,
|
|
2348
|
+
needs: Object.freeze([...options.needs]),
|
|
2349
|
+
permissions: FACTORY_MERGE_FREEZE_PERMISSIONS,
|
|
2350
|
+
steps: Object.freeze([mintStep, reportStep])
|
|
2351
|
+
});
|
|
2352
|
+
};
|
|
2353
|
+
//#endregion
|
|
2109
2354
|
//#region src/pr-status-hud-workflow.ts
|
|
2110
2355
|
const FACTORY_PR_STATUS_HUD_JOB_ID = "status-hud";
|
|
2111
2356
|
const FACTORY_PR_STATUS_HUD_JOB_NAME = "Present PR status HUD";
|
|
@@ -2118,15 +2363,11 @@ const FACTORY_PR_STATUS_HUD_PLAN_PATH = ".factory-memory/preview-plan.json";
|
|
|
2118
2363
|
*/
|
|
2119
2364
|
const FACTORY_PR_STATUS_HUD_IF = "always() && github.event_name == 'pull_request' && github.event.pull_request.draft != true";
|
|
2120
2365
|
/**
|
|
2121
|
-
* Least privilege for the job that runs the present step.
|
|
2122
|
-
*
|
|
2123
|
-
*
|
|
2366
|
+
* Least privilege for the job that runs the present step. The present step
|
|
2367
|
+
* is an inventory `list`, so the job needs exactly the inventory's read
|
|
2368
|
+
* permissions (#825, #859).
|
|
2124
2369
|
*/
|
|
2125
|
-
const FACTORY_PR_STATUS_HUD_PERMISSIONS =
|
|
2126
|
-
checks: "read",
|
|
2127
|
-
contents: "read",
|
|
2128
|
-
"pull-requests": "read"
|
|
2129
|
-
});
|
|
2370
|
+
const FACTORY_PR_STATUS_HUD_PERMISSIONS = PREVIEW_PROOF_INVENTORY_LIST_PERMISSIONS;
|
|
2130
2371
|
const FACTORY_PR_STATUS_HUD_CONCURRENCY = Object.freeze({
|
|
2131
2372
|
cancelInProgress: false,
|
|
2132
2373
|
group: `status-hud-\${{ github.repository }}-\${{ github.event.pull_request.number }}`
|
|
@@ -2220,7 +2461,16 @@ const factoryPreviewCleanupTopology = (options) => {
|
|
|
2220
2461
|
};
|
|
2221
2462
|
//#endregion
|
|
2222
2463
|
//#region src/push-identity-workflow.ts
|
|
2464
|
+
/** `owner/repo` slice of the catalog's `UPLOAD_ARTIFACT` pin, derived rather
|
|
2465
|
+
* than repeated as a literal, so a repository rename here cannot drift from
|
|
2466
|
+
* the pin it validates against. */
|
|
2467
|
+
const UPLOAD_ARTIFACT_REPOSITORY = UPLOAD_ARTIFACT.uses.slice(0, UPLOAD_ARTIFACT.uses.indexOf("@"));
|
|
2223
2468
|
const FACTORY_PUSH_IDENTITY_SCHEMA_VERSION = 1;
|
|
2469
|
+
/**
|
|
2470
|
+
* The artifact prefix and the five step IDs are module-private. Alpha.13
|
|
2471
|
+
* removed them from the public surface (#719). A caller reads a step ID from
|
|
2472
|
+
* the builder's returned `steps` (`step.id`) or from `artifactName`.
|
|
2473
|
+
*/
|
|
2224
2474
|
const FACTORY_PUSH_IDENTITY_ARTIFACT_PREFIX = "factory-push-identity";
|
|
2225
2475
|
const FACTORY_PUSH_IDENTITY_RECORD_STEP_ID = "factory_push_identity_record";
|
|
2226
2476
|
const FACTORY_PUSH_IDENTITY_LOOKUP_STEP_ID = "factory_push_identity_lookup";
|
|
@@ -2470,7 +2720,7 @@ printf 'disposition=%s\nreason=%s\nbefore=%s\nafter=%s\nprovenance=%s\n' \
|
|
|
2470
2720
|
* result when transport is unavailable. Place these steps after verification.
|
|
2471
2721
|
*/
|
|
2472
2722
|
const factoryPushIdentityProducer = (options) => {
|
|
2473
|
-
assertPinnedAction("uploadArtifact", options.uploadArtifact,
|
|
2723
|
+
assertPinnedAction("uploadArtifact", options.uploadArtifact, UPLOAD_ARTIFACT_REPOSITORY);
|
|
2474
2724
|
const condition = options.if ? `github.event_name == 'push' && (${options.if})` : "github.event_name == 'push'";
|
|
2475
2725
|
const name = artifactName(expression("github.run_id"));
|
|
2476
2726
|
return Object.freeze({
|
|
@@ -3286,4 +3536,4 @@ const assertWorkflowShellParses = (yaml, options) => {
|
|
|
3286
3536
|
throw new Error(`${options.source} emits shell bash cannot parse; the runner would treat it as a no-op:\n${detail}`);
|
|
3287
3537
|
};
|
|
3288
3538
|
//#endregion
|
|
3289
|
-
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_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_REASONS, FACTORY_PROOF_GATE_REASON_OUTPUT, FACTORY_PROOF_GATE_SHELL, FACTORY_PROOF_GATE_SOURCE_URL_OUTPUT, FACTORY_PROOF_GATE_STEP_ID, FACTORY_PROOF_GATE_STEP_NAME, FACTORY_PR_STATUS_HUD_APP_TOKEN_STEP_ID, FACTORY_PR_STATUS_HUD_CONCURRENCY, FACTORY_PR_STATUS_HUD_IF, FACTORY_PR_STATUS_HUD_JOB_ID, FACTORY_PR_STATUS_HUD_JOB_NAME, FACTORY_PR_STATUS_HUD_PERMISSIONS, FACTORY_PR_STATUS_HUD_PLAN_PATH, FACTORY_PUSH_IDENTITY_SCHEMA_VERSION, GitHubApiError, NODE_PNPM_ACTION_FAMILY_NODE24, PREVIEW_PROOF_INVENTORY_CHECK_NAME, PreviewProofTransportError, VITEST_PROFILE_SCHEMA_VERSION, VITEST_PROFILE_TOOL, VitestProfileError, assertProofReuseCoverage, assertWorkflowShellParses, bundleAlchemyEntry, captureVitestProfileEnvironment, executeAlchemyEntry, factoryCandidateImpactWorkflow, factoryCandidateOrPushCondition, factoryLifecycleContractLane, factoryPrStatusHudWorkflow, factoryPreviewCleanupTopology, factoryProductionImpactWorkflow, factoryProofGateScript, factoryProofGateStep, factoryProofReuseSummaryStep, factoryProofTimingStartStep, factoryPushIdentityConsumer, factoryPushIdentityProducer, factoryWorkflow, githubAppJwt, isLocalPreviewStage, localPreviewStage, mintInstallationToken, normalizeVitestProfileSample, parseLocalPreviewStage, previewCleanupDestroyJobId, previewProofInventory, previewProofLifecycle, productionImpactTargetOutput, proofReuseCoverage, proofReuseRequiredCommands, readVitestProfileDocument, resolveProofReuseCommands, runVitestProfile, workflowRunBlocks, workflowShellParseFailures, writeVitestProfile };
|
|
3539
|
+
export { EXECUTE_ALCHEMY_ENTRY_MAX_BUFFER, FACTORY_CANDIDATE_IMPACT_BASIS_OUTPUT, FACTORY_CANDIDATE_IMPACT_DECISION_OUTPUT, FACTORY_CANDIDATE_IMPACT_INERT_OUTPUT, FACTORY_CANDIDATE_IMPACT_STEP_ID, FACTORY_CANDIDATE_IMPACT_UNSUBSCRIBED_OUTPUT, FACTORY_CANDIDATE_PULL_REQUEST_TYPES, FACTORY_LIFECYCLE_CONTRACT_JOB_ID, FACTORY_LIFECYCLE_CONTRACT_JOB_NAME, FACTORY_MERGE_FREEZE_APP_TOKEN_STEP_ID, FACTORY_MERGE_FREEZE_CHECK_NAME, FACTORY_MERGE_FREEZE_IF, FACTORY_MERGE_FREEZE_JOB_ID, FACTORY_MERGE_FREEZE_JOB_NAME, FACTORY_MERGE_FREEZE_PERMISSIONS, FACTORY_MERGE_FREEZE_VERIFY_RESULT_EXPRESSION, FACTORY_MERGE_TARGET_REF_CONDITION, FACTORY_PREVIEW_CLEANUP_AUDIT_JOB_ID, FACTORY_PREVIEW_CLEANUP_AUDIT_JOB_NAME, FACTORY_PRODUCTION_IMPACT_BASIS_OUTPUT, FACTORY_PRODUCTION_IMPACT_DECISION_OUTPUT, FACTORY_PRODUCTION_IMPACT_STEP_ID, FACTORY_PRODUCTION_IMPACT_UNSUBSCRIBED_OUTPUT, FACTORY_PROOF_GATE_APP_ID, FACTORY_PROOF_GATE_CHECK_NAME, FACTORY_PROOF_GATE_GUARD, FACTORY_PROOF_GATE_IF, FACTORY_PROOF_GATE_MODE_OUTPUT, FACTORY_PROOF_GATE_OUTPUT, FACTORY_PROOF_GATE_PULL_REQUEST_IF, FACTORY_PROOF_GATE_REASONS, FACTORY_PROOF_GATE_REASON_OUTPUT, FACTORY_PROOF_GATE_SHELL, FACTORY_PROOF_GATE_SOURCE_URL_OUTPUT, FACTORY_PROOF_GATE_STEP_ID, FACTORY_PROOF_GATE_STEP_NAME, FACTORY_PR_STATUS_HUD_APP_TOKEN_STEP_ID, FACTORY_PR_STATUS_HUD_CONCURRENCY, FACTORY_PR_STATUS_HUD_IF, FACTORY_PR_STATUS_HUD_JOB_ID, FACTORY_PR_STATUS_HUD_JOB_NAME, FACTORY_PR_STATUS_HUD_PERMISSIONS, FACTORY_PR_STATUS_HUD_PLAN_PATH, FACTORY_PUSH_IDENTITY_SCHEMA_VERSION, GitHubApiError, NODE_PNPM_ACTION_FAMILY_NODE24, PREVIEW_PROOF_INVENTORY_CHECK_NAME, PREVIEW_PROOF_INVENTORY_LIST_PERMISSIONS, PreviewProofTransportError, UPLOAD_ARTIFACT, VITEST_PROFILE_SCHEMA_VERSION, VITEST_PROFILE_TOOL, VitestProfileError, assertProofReuseCoverage, assertWorkflowShellParses, bundleAlchemyEntry, captureVitestProfileEnvironment, executeAlchemyEntry, factoryCandidateImpactWorkflow, factoryCandidateOrPushCondition, factoryLifecycleContractLane, factoryMergeFreezeJob, factoryMergeFreezeScript, factoryPrStatusHudWorkflow, factoryPreviewCleanupTopology, factoryProductionImpactWorkflow, factoryProofGateScript, factoryProofGateStep, factoryProofReuseSummaryStep, factoryProofTimingStartStep, factoryPushIdentityConsumer, factoryPushIdentityProducer, factoryWorkflow, githubAppJwt, isLocalPreviewStage, localPreviewStage, mintInstallationToken, normalizeVitestProfileSample, parseLocalPreviewStage, previewCleanupDestroyJobId, previewProofInventory, previewProofLifecycle, productionImpactTargetOutput, proofReuseCoverage, proofReuseRequiredCommands, readVitestProfileDocument, resolveProofReuseCommands, runVitestProfile, workflowRunBlocks, workflowShellParseFailures, writeVitestProfile };
|