@patronage/factory-ci 1.0.0-alpha.7 → 1.0.0-alpha.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -2
- package/dist/index.d.ts +109 -2
- package/dist/index.js +384 -25
- package/package.json +1 -1
- package/src/candidate-lifecycle.ts +29 -0
- package/src/factory-workflow.ts +25 -1
- package/src/index.ts +31 -0
- package/src/production-impact-workflow.ts +109 -0
- package/src/proof-reuse-gate.ts +96 -10
- package/src/proof-reuse-presentation.ts +125 -0
- package/src/vitest-profile-reader.test.ts +208 -0
- package/src/vitest-profile-reader.ts +220 -0
package/dist/index.js
CHANGED
|
@@ -32,6 +32,32 @@ const NODE_PNPM_ACTION_FAMILY_NODE24 = {
|
|
|
32
32
|
}
|
|
33
33
|
};
|
|
34
34
|
//#endregion
|
|
35
|
+
//#region src/candidate-lifecycle.ts
|
|
36
|
+
/**
|
|
37
|
+
* Pull-request events that can create or refresh a Factory candidate run.
|
|
38
|
+
*
|
|
39
|
+
* GitHub's draft boolean remains the lifecycle authority: these triggers make
|
|
40
|
+
* a run visible, while `factoryCandidateOrPushCondition` keeps substantive
|
|
41
|
+
* jobs idle until the pull request is a candidate.
|
|
42
|
+
*/
|
|
43
|
+
const FACTORY_CANDIDATE_PULL_REQUEST_TYPES = Object.freeze([
|
|
44
|
+
"opened",
|
|
45
|
+
"synchronize",
|
|
46
|
+
"reopened",
|
|
47
|
+
"ready_for_review"
|
|
48
|
+
]);
|
|
49
|
+
/**
|
|
50
|
+
* Build a GitHub Actions job condition for candidate PRs and merge-target
|
|
51
|
+
* pushes. The caller owns triggers, jobs, runners, permissions, and topology.
|
|
52
|
+
*/
|
|
53
|
+
const factoryCandidateOrPushCondition = (candidateCondition) => {
|
|
54
|
+
return ["github.event_name == 'push'", `(${[
|
|
55
|
+
"github.event_name == 'pull_request'",
|
|
56
|
+
"github.event.pull_request.draft != true",
|
|
57
|
+
...candidateCondition ? [candidateCondition] : []
|
|
58
|
+
].join(" && ")})`].join(" || ");
|
|
59
|
+
};
|
|
60
|
+
//#endregion
|
|
35
61
|
//#region src/bundle-alchemy-entry.ts
|
|
36
62
|
/**
|
|
37
63
|
* Alchemy and Effect must resolve from the *consumer's* node_modules at run
|
|
@@ -208,15 +234,26 @@ const assertAdditionalActions = (actions) => {
|
|
|
208
234
|
assertPinnedAction(name, action);
|
|
209
235
|
}
|
|
210
236
|
};
|
|
237
|
+
const checkoutInputs = (checkout) => {
|
|
238
|
+
if (checkout === void 0) return;
|
|
239
|
+
const inputs = {};
|
|
240
|
+
if (checkout.fetchDepth !== void 0) {
|
|
241
|
+
if (!Number.isSafeInteger(checkout.fetchDepth) || checkout.fetchDepth < 0) throw new Error("checkout.fetchDepth must be a non-negative integer.");
|
|
242
|
+
inputs["fetch-depth"] = String(checkout.fetchDepth);
|
|
243
|
+
}
|
|
244
|
+
if (checkout.ref) inputs.ref = checkout.ref;
|
|
245
|
+
return Object.keys(inputs).length > 0 ? inputs : void 0;
|
|
246
|
+
};
|
|
211
247
|
const setupSteps = (family, setup = {}) => {
|
|
212
248
|
const installRun = setup.install?.run ?? "pnpm install --frozen-lockfile";
|
|
213
249
|
if (/(?:^|\s)--ignore-scripts(?:\s|$)/u.test(installRun)) throw new Error("The install step must not use --ignore-scripts; workspace prepare scripts build required package artifacts.");
|
|
214
250
|
const { checkout, setupNode } = setup;
|
|
251
|
+
const checkoutWith = checkoutInputs(checkout);
|
|
215
252
|
return Object.freeze([
|
|
216
253
|
{
|
|
217
254
|
name: checkout?.name ?? "Checkout",
|
|
218
255
|
uses: family.checkout.uses,
|
|
219
|
-
...
|
|
256
|
+
...checkoutWith ? { with: checkoutWith } : {}
|
|
220
257
|
},
|
|
221
258
|
usesStep("Setup pnpm", family.setupPnpm),
|
|
222
259
|
{
|
|
@@ -403,9 +440,9 @@ const executeAlchemyEntry = async (options) => {
|
|
|
403
440
|
* Escaped so the emitted text carries a GitHub Actions expression rather than
|
|
404
441
|
* this file carrying a JavaScript template hole.
|
|
405
442
|
*/
|
|
406
|
-
const githubExpression = (expression) => `\${{ ${expression} }}`;
|
|
443
|
+
const githubExpression$1 = (expression) => `\${{ ${expression} }}`;
|
|
407
444
|
/** A braced shell expansion that has to survive TypeScript interpolation. */
|
|
408
|
-
const shellExpansion = (expression) => `\${${expression}}`;
|
|
445
|
+
const shellExpansion$1 = (expression) => `\${${expression}}`;
|
|
409
446
|
/**
|
|
410
447
|
* Single-quote a value for the emitted script. Every non-literal value that
|
|
411
448
|
* reaches the script goes through this.
|
|
@@ -435,6 +472,13 @@ const FACTORY_PROOF_GATE_APP_ID = "4314840";
|
|
|
435
472
|
/** Step id the guard condition refers to. */
|
|
436
473
|
const FACTORY_PROOF_GATE_STEP_ID = "factory-proof";
|
|
437
474
|
/**
|
|
475
|
+
* Human-visible name of the gate step as the Actions jobs API serves it. A
|
|
476
|
+
* read-only run analyzer (`psf ci:analyze`, #647) matches on this name to
|
|
477
|
+
* classify a run as proof-reuse versus full fallback, so it is exported from
|
|
478
|
+
* exactly the module that writes it — matching on a re-typed copy would drift.
|
|
479
|
+
*/
|
|
480
|
+
const FACTORY_PROOF_GATE_STEP_NAME = "Check for factory proof of this head";
|
|
481
|
+
/**
|
|
438
482
|
* The shell the gate runs under, and it is a correctness requirement rather
|
|
439
483
|
* than a preference.
|
|
440
484
|
*
|
|
@@ -471,6 +515,8 @@ const FACTORY_PROOF_GATE_REASON_OUTPUT = "reason";
|
|
|
471
515
|
* proof that executed every command the surface requires is reusable.
|
|
472
516
|
*/
|
|
473
517
|
const FACTORY_PROOF_GATE_MODE_OUTPUT = "mode";
|
|
518
|
+
/** Exact Checks API URL of the proof generation selected by the gate. */
|
|
519
|
+
const FACTORY_PROOF_GATE_SOURCE_URL_OUTPUT = "source-check-url";
|
|
474
520
|
/**
|
|
475
521
|
* Guard for every step the gate protects. Deliberately `!= 'true'` and not
|
|
476
522
|
* `== 'false'`: an unset, empty, or garbled output must run the suite.
|
|
@@ -491,7 +537,8 @@ const FACTORY_PROOF_GATE_IF = "github.event_name == 'pull_request'";
|
|
|
491
537
|
* - `pending` the newest generation had not completed when this job read it
|
|
492
538
|
* - `failed` the newest generation records no pass
|
|
493
539
|
* - `unreadable` it passed but carries no binding for this repository and head
|
|
494
|
-
* - `incomplete`
|
|
540
|
+
* - `incomplete` its executed plus stamp-authorized released commands do not
|
|
541
|
+
* cover every required command
|
|
495
542
|
* - `ambiguous` two newest generations share the greatest start time
|
|
496
543
|
* - `error` the gate could not reach a decision (fail open)
|
|
497
544
|
*
|
|
@@ -525,6 +572,13 @@ const FACTORY_PROOF_GATE_REASONS = [
|
|
|
525
572
|
*/
|
|
526
573
|
const COMMAND_IDENTITY = /^\w[\w.:@/-]*$/u;
|
|
527
574
|
const COMMAND_IDENTITY_MAX_LENGTH = 120;
|
|
575
|
+
/**
|
|
576
|
+
* The impact-stamp interpretation the paired factory packages currently
|
|
577
|
+
* share. A release recorded under any other version is an unmodelled input and
|
|
578
|
+
* cannot subtract hosted work. This is intentionally fail-closed and moves in
|
|
579
|
+
* lockstep with the proof/check-payload producer.
|
|
580
|
+
*/
|
|
581
|
+
const TRUSTED_IMPACT_STAMP_VERSION = 3;
|
|
528
582
|
const isProofReuseCommand = (value) => {
|
|
529
583
|
if (!(value && typeof value === "object")) return false;
|
|
530
584
|
const entry = value;
|
|
@@ -592,7 +646,7 @@ const resolveProofReuseCommands = (catalog, names, selectionLabel) => names.map(
|
|
|
592
646
|
});
|
|
593
647
|
/**
|
|
594
648
|
* jq program: every page of the Checks API result in, three sanitized lines
|
|
595
|
-
* (`reason`, `mode`,
|
|
649
|
+
* (`reason`, `mode`, uncovered commands) out.
|
|
596
650
|
*
|
|
597
651
|
* The input is what `gh api --paginate` actually writes: the pages
|
|
598
652
|
* *concatenated* as a stream of top-level response objects, not merged into
|
|
@@ -649,6 +703,20 @@ def startedAt: (.started_at // "") | tostring;
|
|
|
649
703
|
def rankable:
|
|
650
704
|
test("^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?Z$");
|
|
651
705
|
|
|
706
|
+
def nonemptyString:
|
|
707
|
+
type == "string" and length > 0;
|
|
708
|
+
|
|
709
|
+
def distinctNames:
|
|
710
|
+
(map(.name) | length) == (map(.name) | unique | length);
|
|
711
|
+
|
|
712
|
+
def releaseAuthorized($release; $commands; $stamp):
|
|
713
|
+
([ $commands[] | select(.name == $release.name) ]) as $commandRows
|
|
714
|
+
| ([ $stamp.targets[] | select(.name == $release.impactTarget) ]) as $targetRows
|
|
715
|
+
| ($commandRows | length) == 1
|
|
716
|
+
and $commandRows[0].impactTarget == $release.impactTarget
|
|
717
|
+
and ($targetRows | length) == 1
|
|
718
|
+
and $targetRows[0].impact == "not-affected";
|
|
719
|
+
|
|
652
720
|
[ .[]
|
|
653
721
|
| (.check_runs // [])[]
|
|
654
722
|
| select(.name == $name)
|
|
@@ -656,20 +724,60 @@ def rankable:
|
|
|
656
724
|
| { started: startedAt,
|
|
657
725
|
status: (.status // ""),
|
|
658
726
|
conclusion: (.conclusion // ""),
|
|
727
|
+
url: ((.html_url // "") | tostring),
|
|
659
728
|
binding: binding }
|
|
660
729
|
] as $runs
|
|
661
730
|
| ($runs | map(.started | rankable) | all) as $orderable
|
|
662
731
|
| ($runs | map(.started | sub("\\.[0-9]+Z$"; "Z")) | max) as $newest
|
|
663
732
|
| [ $runs[] | select((.started | sub("\\.[0-9]+Z$"; "Z")) == $newest) ] as $generation
|
|
664
|
-
| (if ($runs | length) == 0 then ["none", "", ""]
|
|
665
|
-
elif ($orderable | not) then ["ambiguous", "", ""]
|
|
666
|
-
elif ($generation | length) != 1 then ["ambiguous", "", ""]
|
|
733
|
+
| (if ($runs | length) == 0 then ["none", "", "", ""]
|
|
734
|
+
elif ($orderable | not) then ["ambiguous", "", "", ""]
|
|
735
|
+
elif ($generation | length) != 1 then ["ambiguous", "", "", ""]
|
|
667
736
|
else
|
|
668
737
|
$generation[0] as $run
|
|
669
738
|
| (if ($run.binding | type) == "object" then $run.binding else {} end) as $proof
|
|
670
739
|
| (($proof.mode // "") | tostring) as $mode
|
|
671
740
|
| (($proof.executedCommands // []) | map(select(type == "string"))) as $executed
|
|
672
|
-
| ($
|
|
741
|
+
| ($proof | has("notRequiredCommands")) as $hasReleases
|
|
742
|
+
| (if $hasReleases then $proof.notRequiredCommands else [] end) as $releases
|
|
743
|
+
| ($proof.verificationCommands // null) as $commands
|
|
744
|
+
| ($proof.impactStamp // null) as $stamp
|
|
745
|
+
| (if ($hasReleases | not) then true
|
|
746
|
+
else
|
|
747
|
+
($proof.proofSchemaVersion == 4)
|
|
748
|
+
and ($releases | type) == "array"
|
|
749
|
+
and ($releases | length) > 0
|
|
750
|
+
and ($releases | all(.[];
|
|
751
|
+
type == "object"
|
|
752
|
+
and (.name | nonemptyString)
|
|
753
|
+
and (.impactTarget | nonemptyString)
|
|
754
|
+
and (.basis | nonemptyString)))
|
|
755
|
+
and ($releases | distinctNames)
|
|
756
|
+
and ($commands | type) == "array"
|
|
757
|
+
and ($commands | all(.[];
|
|
758
|
+
type == "object"
|
|
759
|
+
and (.name | nonemptyString)
|
|
760
|
+
and ((has("impactTarget") | not) or (.impactTarget | nonemptyString))))
|
|
761
|
+
and ($commands | distinctNames)
|
|
762
|
+
and ($stamp | type) == "object"
|
|
763
|
+
and ($stamp.stampVersion == ${TRUSTED_IMPACT_STAMP_VERSION})
|
|
764
|
+
and ($stamp.basis == "target-scoped")
|
|
765
|
+
and (($stamp.reasons | type) == "array")
|
|
766
|
+
and ($stamp.reasons | all(.[]; type == "string"))
|
|
767
|
+
and (($stamp.unsubscribedPaths | type) == "array")
|
|
768
|
+
and ($stamp.unsubscribedPaths | all(.[]; type == "string"))
|
|
769
|
+
and (($stamp.targets | type) == "array")
|
|
770
|
+
and ($stamp.targets | all(.[];
|
|
771
|
+
type == "object"
|
|
772
|
+
and (.name | nonemptyString)
|
|
773
|
+
and (.basis | nonemptyString)
|
|
774
|
+
and ((.impact == "affected") or (.impact == "not-affected"))))
|
|
775
|
+
and ($stamp.targets | distinctNames)
|
|
776
|
+
and (([$releases[].name] - $executed | length) == ($releases | length))
|
|
777
|
+
and ($releases | all(.[]; releaseAuthorized(.; $commands; $stamp)))
|
|
778
|
+
end) as $releasesValid
|
|
779
|
+
| (if $releasesValid then [$releases[].name] else [] end) as $released
|
|
780
|
+
| ($required - $executed - $released) as $missing
|
|
673
781
|
| (if $run.status != "completed" then "pending"
|
|
674
782
|
elif $run.conclusion != "success" then "failed"
|
|
675
783
|
elif (($run.binding | type) != "object")
|
|
@@ -679,10 +787,11 @@ def rankable:
|
|
|
679
787
|
or ($proof.repository != $repository) then "unreadable"
|
|
680
788
|
elif $proof.outcome != "passed" then "failed"
|
|
681
789
|
elif ($required | length) == 0 then "incomplete"
|
|
790
|
+
elif ($releasesValid | not) then "incomplete"
|
|
682
791
|
elif ($missing | length) != 0 then "incomplete"
|
|
683
792
|
else "proven"
|
|
684
793
|
end) as $reason
|
|
685
|
-
| [$reason, $mode, ($missing | join(", "))]
|
|
794
|
+
| [$reason, $mode, ($missing | join(", ")), $run.url]
|
|
686
795
|
end)
|
|
687
796
|
| map(gsub("[\\r\\n\\t]"; " "))
|
|
688
797
|
| join("\n")
|
|
@@ -707,9 +816,9 @@ const CORRECTIVE_LINE = "Run `psf pr:verify` before publishing to reuse local pr
|
|
|
707
816
|
const UNUSABLE_SELECTION_SCRIPT = String.raw`{
|
|
708
817
|
printf '${FACTORY_PROOF_GATE_OUTPUT}=false\n'
|
|
709
818
|
printf '${FACTORY_PROOF_GATE_REASON_OUTPUT}=error\n'
|
|
710
|
-
} >> "${shellExpansion("GITHUB_OUTPUT:-/dev/null")}"`;
|
|
819
|
+
} >> "${shellExpansion$1("GITHUB_OUTPUT:-/dev/null")}"`;
|
|
711
820
|
/** Surface labels reach markdown, so only a plain, bounded label survives. */
|
|
712
|
-
const safeLabel = (surface) => {
|
|
821
|
+
const safeLabel$1 = (surface) => {
|
|
713
822
|
const cleaned = (typeof surface === "string" ? surface : "").replaceAll(/[^\w -]/gu, "").trim().slice(0, 60);
|
|
714
823
|
return cleaned.length > 0 ? cleaned : "verification";
|
|
715
824
|
};
|
|
@@ -725,14 +834,15 @@ reason=error
|
|
|
725
834
|
detail=''
|
|
726
835
|
mode=''
|
|
727
836
|
missing=''
|
|
837
|
+
source_url=''
|
|
728
838
|
|
|
729
839
|
# filter=all with full pagination is load-bearing (ADR 0022). GitHub's
|
|
730
840
|
# default "latest" filter is ordered by completion, so a newer generation
|
|
731
841
|
# that is still running can be hidden behind an older completed one — the
|
|
732
842
|
# gate would then read a stale pass as current.
|
|
733
|
-
if [ -z "${shellExpansion("HEAD_SHA:-")}" ]; then
|
|
843
|
+
if [ -z "${shellExpansion$1("HEAD_SHA:-")}" ]; then
|
|
734
844
|
detail='no pull request head SHA'
|
|
735
|
-
elif [ -z "${shellExpansion("GITHUB_REPOSITORY:-")}" ]; then
|
|
845
|
+
elif [ -z "${shellExpansion$1("GITHUB_REPOSITORY:-")}" ]; then
|
|
736
846
|
detail='no repository name'
|
|
737
847
|
elif ! response=$(gh api --method GET --paginate \
|
|
738
848
|
"repos/$GITHUB_REPOSITORY/commits/$HEAD_SHA/check-runs" \
|
|
@@ -763,6 +873,7 @@ else
|
|
|
763
873
|
IFS= read -r reason || :
|
|
764
874
|
IFS= read -r mode || :
|
|
765
875
|
IFS= read -r missing || :
|
|
876
|
+
IFS= read -r source_url || :
|
|
766
877
|
} <<< "$finding"
|
|
767
878
|
case "$reason" in
|
|
768
879
|
proven | none | pending | failed | unreadable | incomplete | ambiguous) ;;
|
|
@@ -789,8 +900,11 @@ fi
|
|
|
789
900
|
# line so it cannot restructure the summary it is written into.
|
|
790
901
|
detail=$(printf '%s' "$detail" | tr '\n\r\t' ' ' | cut -c1-240)
|
|
791
902
|
missing=$(printf '%s' "$missing" | tr '\n\r\t' ' ' | cut -c1-240)
|
|
903
|
+
if ! [[ "$source_url" =~ ^https://github\.com/[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+/runs/[0-9]+$ ]]; then
|
|
904
|
+
source_url=''
|
|
905
|
+
fi
|
|
792
906
|
|
|
793
|
-
SUMMARY="${shellExpansion("GITHUB_STEP_SUMMARY:-/dev/null")}"
|
|
907
|
+
SUMMARY="${shellExpansion$1("GITHUB_STEP_SUMMARY:-/dev/null")}"
|
|
794
908
|
say() { printf '%s\n' "$1" >> "$SUMMARY"; }
|
|
795
909
|
|
|
796
910
|
say "## Proof reuse: $SURFACE"
|
|
@@ -801,7 +915,11 @@ case "$reason" in
|
|
|
801
915
|
echo "Factory proof: reusing local verification of $HEAD_SHA; skipping the $SURFACE suite."
|
|
802
916
|
say "Skipped. The factory already verified this exact commit, so this job did not run the $SURFACE suite a second time."
|
|
803
917
|
say ''
|
|
804
|
-
|
|
918
|
+
if [ -n "$source_url" ]; then
|
|
919
|
+
say "- Reused proof: [\`$CHECK_NAME\`]($source_url), published by the pinned factory GitHub App."
|
|
920
|
+
else
|
|
921
|
+
say "- Reused proof: the \`$CHECK_NAME\` check run published by the pinned factory GitHub App."
|
|
922
|
+
fi
|
|
805
923
|
say "- Covers head: \`$HEAD_SHA\`"
|
|
806
924
|
say "- Recorded mode: \`$mode\` (diagnostic only), outcome \`passed\`."
|
|
807
925
|
;;
|
|
@@ -836,7 +954,7 @@ case "$reason" in
|
|
|
836
954
|
incomplete)
|
|
837
955
|
say "Ran the full suite. The factory proof for this commit does not cover every command the $SURFACE surface requires."
|
|
838
956
|
say ''
|
|
839
|
-
say "The newest \`$CHECK_NAME\` check run at head \`$HEAD_SHA\` passed, but its \`
|
|
957
|
+
say "The newest \`$CHECK_NAME\` check run at head \`$HEAD_SHA\` passed, but its executed commands plus stamp-authorized \`notRequiredCommands\` do not cover this surface. Missing: \`$missing\`"
|
|
840
958
|
say ''
|
|
841
959
|
say '${CORRECTIVE_LINE}'
|
|
842
960
|
;;
|
|
@@ -858,7 +976,7 @@ esac
|
|
|
858
976
|
say ''
|
|
859
977
|
|
|
860
978
|
if [ "$verdict" != 'true' ]; then
|
|
861
|
-
echo "No reusable factory proof for ${shellExpansion("HEAD_SHA:-<unknown>")}; running the $SURFACE suite ($reason). $detail"
|
|
979
|
+
echo "No reusable factory proof for ${shellExpansion$1("HEAD_SHA:-<unknown>")}; running the $SURFACE suite ($reason). $detail"
|
|
862
980
|
fi
|
|
863
981
|
|
|
864
982
|
# The only write. A crash before this line leaves the output unset, the guard
|
|
@@ -867,7 +985,8 @@ fi
|
|
|
867
985
|
printf '${FACTORY_PROOF_GATE_OUTPUT}=%s\n' "$verdict"
|
|
868
986
|
printf '${FACTORY_PROOF_GATE_REASON_OUTPUT}=%s\n' "$reason"
|
|
869
987
|
printf '${FACTORY_PROOF_GATE_MODE_OUTPUT}=%s\n' "$mode"
|
|
870
|
-
}
|
|
988
|
+
printf '${FACTORY_PROOF_GATE_SOURCE_URL_OUTPUT}=%s\n' "$source_url"
|
|
989
|
+
} >> "${shellExpansion$1("GITHUB_OUTPUT:-/dev/null")}"
|
|
871
990
|
`.trim();
|
|
872
991
|
/**
|
|
873
992
|
* The gate script. Exported so it can be executed directly under test against
|
|
@@ -883,7 +1002,7 @@ fi
|
|
|
883
1002
|
*/
|
|
884
1003
|
const factoryProofGateScript = ({ commands, surface }) => {
|
|
885
1004
|
const required = proofReuseRequiredCommands(commands);
|
|
886
|
-
return required ? gateScript(required, safeLabel(surface)) : UNUSABLE_SELECTION_SCRIPT;
|
|
1005
|
+
return required ? gateScript(required, safeLabel$1(surface)) : UNUSABLE_SELECTION_SCRIPT;
|
|
887
1006
|
};
|
|
888
1007
|
/**
|
|
889
1008
|
* The step itself, structurally accepted by gagen's `step()` without adding a
|
|
@@ -901,12 +1020,12 @@ const factoryProofGateScript = ({ commands, surface }) => {
|
|
|
901
1020
|
const factoryProofGateStep = (options) => Object.freeze({
|
|
902
1021
|
continueOnError: true,
|
|
903
1022
|
env: Object.freeze({
|
|
904
|
-
GH_TOKEN: githubExpression("secrets.GITHUB_TOKEN"),
|
|
905
|
-
HEAD_SHA: githubExpression("github.event.pull_request.head.sha")
|
|
1023
|
+
GH_TOKEN: githubExpression$1("secrets.GITHUB_TOKEN"),
|
|
1024
|
+
HEAD_SHA: githubExpression$1("github.event.pull_request.head.sha")
|
|
906
1025
|
}),
|
|
907
1026
|
id: FACTORY_PROOF_GATE_STEP_ID,
|
|
908
1027
|
if: FACTORY_PROOF_GATE_IF,
|
|
909
|
-
name:
|
|
1028
|
+
name: FACTORY_PROOF_GATE_STEP_NAME,
|
|
910
1029
|
run: factoryProofGateScript(options),
|
|
911
1030
|
shell: FACTORY_PROOF_GATE_SHELL
|
|
912
1031
|
});
|
|
@@ -934,11 +1053,137 @@ const proofReuseCoverage = ({ commands, skipped }) => {
|
|
|
934
1053
|
const assertProofReuseCoverage = (input) => {
|
|
935
1054
|
const report = proofReuseCoverage(input);
|
|
936
1055
|
if (report.covered) return report;
|
|
937
|
-
const surface = safeLabel(input.surface ?? "");
|
|
1056
|
+
const surface = safeLabel$1(input.surface ?? "");
|
|
938
1057
|
const problem = report.requiredCommands.length === 0 ? "selects no usable profile commands, so any passing proof would trivially cover it" : `skips work no selected profile command runs: ${report.uncovered.join(", ")}`;
|
|
939
1058
|
throw new Error(`Proof-reuse coverage failed: the ${surface} surface ${problem}. Add the command to software-factory.profile.json (and to this surface's selection), or stop skipping it.`);
|
|
940
1059
|
};
|
|
941
1060
|
//#endregion
|
|
1061
|
+
//#region src/proof-reuse-presentation.ts
|
|
1062
|
+
/** Generic timing and presentation steps around the proof-reuse gate (#652). */
|
|
1063
|
+
const githubExpression = (expression) => `\${{ ${expression} }}`;
|
|
1064
|
+
const shellExpansion = (expression) => `\${${expression}}`;
|
|
1065
|
+
const safeLabel = (surface) => {
|
|
1066
|
+
const cleaned = (typeof surface === "string" ? surface : "").replaceAll(/[^\w -]/gu, "").trim().slice(0, 60);
|
|
1067
|
+
return cleaned.length > 0 ? cleaned : "verification";
|
|
1068
|
+
};
|
|
1069
|
+
const FACTORY_PROOF_TIMING_STEP_ID = "ci-timing";
|
|
1070
|
+
const FACTORY_PROOF_TIMING_START_STEP_NAME = "Start CI timing";
|
|
1071
|
+
const FACTORY_PROOF_TIMING_SUMMARY_STEP_NAME = "Record proof-reuse timing";
|
|
1072
|
+
const factoryProofTimingStartStep = () => Object.freeze({
|
|
1073
|
+
continueOnError: true,
|
|
1074
|
+
id: FACTORY_PROOF_TIMING_STEP_ID,
|
|
1075
|
+
name: FACTORY_PROOF_TIMING_START_STEP_NAME,
|
|
1076
|
+
run: String.raw`started_ms="$(node -e 'process.stdout.write(String(Date.now()))')"
|
|
1077
|
+
printf 'started_ms=%s\n' "$started_ms" >> "$GITHUB_OUTPUT"`
|
|
1078
|
+
});
|
|
1079
|
+
const factoryProofReuseSummaryScript = ({ surface }) => {
|
|
1080
|
+
const label = safeLabel(surface);
|
|
1081
|
+
return String.raw`now_ms="$(node -e 'process.stdout.write(String(Date.now()))')"
|
|
1082
|
+
ci_started_ms="${shellExpansion("CI_STARTED_MS:-")}"
|
|
1083
|
+
|
|
1084
|
+
SUMMARY="${shellExpansion("GITHUB_STEP_SUMMARY:-/dev/null")}"
|
|
1085
|
+
say() { printf '%s\n' "$1" >> "$SUMMARY"; }
|
|
1086
|
+
|
|
1087
|
+
say '## ${label} timing'
|
|
1088
|
+
say ''
|
|
1089
|
+
|
|
1090
|
+
if [ "${shellExpansion("GITHUB_EVENT_NAME:-")}" = 'push' ]; then
|
|
1091
|
+
say '- Path: merge-target full execution'
|
|
1092
|
+
say '- Proof-reuse gate: not applicable on merge-target runs; the full suite ran.'
|
|
1093
|
+
say "- Verified head: \`${shellExpansion("GITHUB_SHA:-unknown")}\`"
|
|
1094
|
+
echo "::notice title=Factory proof reuse::Proof reuse is not applicable on merge-target runs; the full ${label} suite executed."
|
|
1095
|
+
elif [ "${shellExpansion("PROOF_REUSED:-")}" = 'true' ]; then
|
|
1096
|
+
say '- Path: trusted local proof reused'
|
|
1097
|
+
if [ -n "${shellExpansion("PROOF_SOURCE_URL:-")}" ]; then
|
|
1098
|
+
say "- Reused proof: [exact source check](${shellExpansion("PROOF_SOURCE_URL")})"
|
|
1099
|
+
fi
|
|
1100
|
+
say "- Bound head: \`${shellExpansion("PROOF_HEAD_SHA:-unknown")}\`"
|
|
1101
|
+
say "- Factory verification mode: ${shellExpansion("PROOF_MODE:-unavailable")} (diagnostic only)"
|
|
1102
|
+
echo "::notice title=Factory proof reuse::Reused trusted local proof for ${label} at ${shellExpansion("PROOF_HEAD_SHA:-unknown")}."
|
|
1103
|
+
else
|
|
1104
|
+
say "- Path: full GitHub CI fallback (${shellExpansion("PROOF_REASON:-error")})"
|
|
1105
|
+
say "- Bound head: \`${shellExpansion("PROOF_HEAD_SHA:-unknown")}\`"
|
|
1106
|
+
say "- Factory verification mode: ${shellExpansion("PROOF_MODE:-unavailable")} (diagnostic only)"
|
|
1107
|
+
echo "::notice title=Factory proof reuse::No reusable proof for ${label}; the full hosted suite executed (${shellExpansion("PROOF_REASON:-error")})."
|
|
1108
|
+
fi
|
|
1109
|
+
|
|
1110
|
+
if [[ "$ci_started_ms" =~ ^[0-9]+$ ]]; then
|
|
1111
|
+
say "- GitHub workflow execution after the proof gate: $((now_ms - ci_started_ms)) ms"
|
|
1112
|
+
else
|
|
1113
|
+
say '- GitHub workflow execution after the proof gate: unavailable'
|
|
1114
|
+
fi
|
|
1115
|
+
say ''`;
|
|
1116
|
+
};
|
|
1117
|
+
const factoryProofReuseSummaryStep = (options) => Object.freeze({
|
|
1118
|
+
continueOnError: true,
|
|
1119
|
+
env: Object.freeze({
|
|
1120
|
+
CI_STARTED_MS: githubExpression(`steps.${FACTORY_PROOF_TIMING_STEP_ID}.outputs.started_ms`),
|
|
1121
|
+
PROOF_HEAD_SHA: githubExpression("github.event.pull_request.head.sha"),
|
|
1122
|
+
PROOF_MODE: githubExpression(`steps.${FACTORY_PROOF_GATE_STEP_ID}.outputs.${FACTORY_PROOF_GATE_MODE_OUTPUT}`),
|
|
1123
|
+
PROOF_REASON: githubExpression(`steps.${FACTORY_PROOF_GATE_STEP_ID}.outputs.${FACTORY_PROOF_GATE_REASON_OUTPUT}`),
|
|
1124
|
+
PROOF_REUSED: githubExpression(`steps.${FACTORY_PROOF_GATE_STEP_ID}.outputs.${FACTORY_PROOF_GATE_OUTPUT}`),
|
|
1125
|
+
PROOF_SOURCE_URL: githubExpression(`steps.${FACTORY_PROOF_GATE_STEP_ID}.outputs.${FACTORY_PROOF_GATE_SOURCE_URL_OUTPUT}`)
|
|
1126
|
+
}),
|
|
1127
|
+
if: "always()",
|
|
1128
|
+
name: FACTORY_PROOF_TIMING_SUMMARY_STEP_NAME,
|
|
1129
|
+
run: factoryProofReuseSummaryScript(options)
|
|
1130
|
+
});
|
|
1131
|
+
//#endregion
|
|
1132
|
+
//#region src/production-impact-workflow.ts
|
|
1133
|
+
const FACTORY_PRODUCTION_IMPACT_STEP_ID = "production_impact";
|
|
1134
|
+
const FACTORY_PRODUCTION_IMPACT_DECISION_OUTPUT = "decision";
|
|
1135
|
+
const FACTORY_PRODUCTION_IMPACT_BASIS_OUTPUT = "basis";
|
|
1136
|
+
const FACTORY_PRODUCTION_IMPACT_UNSUBSCRIBED_OUTPUT = "unsubscribed_paths";
|
|
1137
|
+
const OUTPUT_NAME_PATTERN = /[^a-z0-9_]+/gu;
|
|
1138
|
+
const shellQuote = (value) => `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
1139
|
+
/** Stable GitHub-output key for one declared target. */
|
|
1140
|
+
const productionImpactTargetOutput = (targetName) => {
|
|
1141
|
+
const normalized = targetName.toLowerCase().replaceAll(OUTPUT_NAME_PATTERN, "_").replaceAll(/^_+|_+$/gu, "");
|
|
1142
|
+
if (normalized.length === 0) throw new Error(`Impact target name "${targetName}" has no output-safe characters.`);
|
|
1143
|
+
return `target_${normalized}`;
|
|
1144
|
+
};
|
|
1145
|
+
/**
|
|
1146
|
+
* Generate the small factory-owned decision seam for a consumer production
|
|
1147
|
+
* workflow. Consumers retain jobs, deploy commands, credentials, topology,
|
|
1148
|
+
* and convergence checks; this artifact supplies only the decision step and
|
|
1149
|
+
* per-target withdrawal conditions.
|
|
1150
|
+
*/
|
|
1151
|
+
const factoryProductionImpactWorkflow = (options) => {
|
|
1152
|
+
const targetOutputs = Object.fromEntries(options.targets.map((target) => [target, productionImpactTargetOutput(target)]));
|
|
1153
|
+
if (new Set(Object.values(targetOutputs)).size !== options.targets.length) throw new Error("Impact target names must map to distinct GitHub output keys.");
|
|
1154
|
+
const before = options.before ?? `\${{ github.event.before }}`;
|
|
1155
|
+
const after = options.after ?? `\${{ github.sha }}`;
|
|
1156
|
+
const cli = options.cli ?? "pnpm exec psf";
|
|
1157
|
+
const profile = options.profilePath ? ` --profile ${shellQuote(options.profilePath)}` : "";
|
|
1158
|
+
const decisionStep = {
|
|
1159
|
+
continueOnError: true,
|
|
1160
|
+
env: {
|
|
1161
|
+
FACTORY_AFTER_SHA: after,
|
|
1162
|
+
FACTORY_BEFORE_SHA: before
|
|
1163
|
+
},
|
|
1164
|
+
id: FACTORY_PRODUCTION_IMPACT_STEP_ID,
|
|
1165
|
+
name: "Classify production impact",
|
|
1166
|
+
run: `${cli} production:impact --before "$FACTORY_BEFORE_SHA" --after "$FACTORY_AFTER_SHA" --github-output "$GITHUB_OUTPUT" --github-summary "$GITHUB_STEP_SUMMARY"${profile}`
|
|
1167
|
+
};
|
|
1168
|
+
return Object.freeze({
|
|
1169
|
+
decisionJobOutputs: Object.freeze({
|
|
1170
|
+
basis: `\${{ steps.${FACTORY_PRODUCTION_IMPACT_STEP_ID}.outputs.${FACTORY_PRODUCTION_IMPACT_BASIS_OUTPUT} }}`,
|
|
1171
|
+
decision: `\${{ steps.${FACTORY_PRODUCTION_IMPACT_STEP_ID}.outputs.${FACTORY_PRODUCTION_IMPACT_DECISION_OUTPUT} }}`,
|
|
1172
|
+
unsubscribed_paths: `\${{ steps.${FACTORY_PRODUCTION_IMPACT_STEP_ID}.outputs.${FACTORY_PRODUCTION_IMPACT_UNSUBSCRIBED_OUTPUT} }}`,
|
|
1173
|
+
...Object.fromEntries(Object.values(targetOutputs).map((output) => [output, `\${{ steps.${FACTORY_PRODUCTION_IMPACT_STEP_ID}.outputs.${output} }}`]))
|
|
1174
|
+
}),
|
|
1175
|
+
decisionStep: Object.freeze(decisionStep),
|
|
1176
|
+
demandedIf: (targetName, decisionJob) => {
|
|
1177
|
+
const output = targetOutputs[targetName];
|
|
1178
|
+
if (output === void 0) throw new Error(`Unknown production impact target "${targetName}".`);
|
|
1179
|
+
const source = decisionJob ? `needs.${decisionJob}` : `steps.${FACTORY_PRODUCTION_IMPACT_STEP_ID}`;
|
|
1180
|
+
const condition = `${source}.${decisionJob ? "result" : "outcome"} != 'success' || ${source}.outputs.${FACTORY_PRODUCTION_IMPACT_DECISION_OUTPUT} != 'usable' || ${source}.outputs.${output} != 'withdrawn'`;
|
|
1181
|
+
return decisionJob ? `always() && (${condition})` : condition;
|
|
1182
|
+
},
|
|
1183
|
+
targetOutputs: Object.freeze(targetOutputs)
|
|
1184
|
+
});
|
|
1185
|
+
};
|
|
1186
|
+
//#endregion
|
|
942
1187
|
//#region src/vitest-profile.ts
|
|
943
1188
|
/**
|
|
944
1189
|
* Vitest suite profiling — the measurement mechanics behind a CI runner
|
|
@@ -1261,6 +1506,120 @@ const runVitestProfile = async (options, dependencies = {}) => {
|
|
|
1261
1506
|
return profile;
|
|
1262
1507
|
};
|
|
1263
1508
|
//#endregion
|
|
1509
|
+
//#region src/vitest-profile-reader.ts
|
|
1510
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1511
|
+
const isFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value);
|
|
1512
|
+
const durationSummaryFailure = (value, where) => {
|
|
1513
|
+
if (!isRecord(value)) return `${where} is not an object`;
|
|
1514
|
+
const missing = [
|
|
1515
|
+
"maximum",
|
|
1516
|
+
"mean",
|
|
1517
|
+
"median",
|
|
1518
|
+
"minimum"
|
|
1519
|
+
].find((field) => !isFiniteNumber(value[field]));
|
|
1520
|
+
return missing === void 0 ? void 0 : `${where}.${missing} is not a finite number`;
|
|
1521
|
+
};
|
|
1522
|
+
const environmentFailure = (value) => {
|
|
1523
|
+
if (!isRecord(value)) return "environment is not an object";
|
|
1524
|
+
if (!(typeof value.cpuModel === "string" || value.cpuModel === null)) return "environment.cpuModel is neither a string nor null";
|
|
1525
|
+
if (!isFiniteNumber(value.cpuCount)) return "environment.cpuCount is not a finite number";
|
|
1526
|
+
};
|
|
1527
|
+
/** The count fields the writer emits; every one must be a finite number. */
|
|
1528
|
+
const COUNT_FIELDS = [
|
|
1529
|
+
"failed",
|
|
1530
|
+
"passed",
|
|
1531
|
+
"pending",
|
|
1532
|
+
"suites",
|
|
1533
|
+
"tests",
|
|
1534
|
+
"todo"
|
|
1535
|
+
];
|
|
1536
|
+
const runFailure = (value, index) => {
|
|
1537
|
+
if (!isRecord(value)) return `runs[${index}] is not an object`;
|
|
1538
|
+
if (!isFiniteNumber(value.durationMs)) return `runs[${index}].durationMs is not a finite number`;
|
|
1539
|
+
if (value.counts === null) return;
|
|
1540
|
+
if (!isRecord(value.counts)) return `runs[${index}].counts is neither an object nor null`;
|
|
1541
|
+
const { counts } = value;
|
|
1542
|
+
const missing = COUNT_FIELDS.find((field) => !isFiniteNumber(counts[field]));
|
|
1543
|
+
return missing === void 0 ? void 0 : `runs[${index}].counts.${missing} is not a finite number`;
|
|
1544
|
+
};
|
|
1545
|
+
const slowEntryFailure = (value, where, labelField) => {
|
|
1546
|
+
if (!isRecord(value)) return `${where} is not an object`;
|
|
1547
|
+
if (typeof value[labelField] !== "string") return `${where}.${labelField} is not a string`;
|
|
1548
|
+
if (labelField === "name" && typeof value.file !== "string") return `${where}.file is not a string`;
|
|
1549
|
+
return durationSummaryFailure(value.durationMs, `${where}.durationMs`);
|
|
1550
|
+
};
|
|
1551
|
+
const summaryFailure = (value) => {
|
|
1552
|
+
if (!isRecord(value)) return "summary is not an object";
|
|
1553
|
+
const durations = durationSummaryFailure(value.durationMs, "summary.durationMs");
|
|
1554
|
+
if (durations !== void 0) return durations;
|
|
1555
|
+
if (!Array.isArray(value.slowFiles)) return "summary.slowFiles is not an array";
|
|
1556
|
+
for (const [index, entry] of value.slowFiles.entries()) {
|
|
1557
|
+
const failure = slowEntryFailure(entry, `summary.slowFiles[${index}]`, "path");
|
|
1558
|
+
if (failure !== void 0) return failure;
|
|
1559
|
+
}
|
|
1560
|
+
if (!Array.isArray(value.slowTests)) return "summary.slowTests is not an array";
|
|
1561
|
+
for (const [index, entry] of value.slowTests.entries()) {
|
|
1562
|
+
const failure = slowEntryFailure(entry, `summary.slowTests[${index}]`, "name");
|
|
1563
|
+
if (failure !== void 0) return failure;
|
|
1564
|
+
}
|
|
1565
|
+
};
|
|
1566
|
+
/**
|
|
1567
|
+
* Check one parsed JSON document against the profile contract the writer
|
|
1568
|
+
* emits. Returns the typed profile on success and the first mismatch reason
|
|
1569
|
+
* otherwise — never a partially-usable value.
|
|
1570
|
+
*/
|
|
1571
|
+
const readVitestProfileDocument = (value) => {
|
|
1572
|
+
if (!isRecord(value)) return {
|
|
1573
|
+
kind: "unrecognized",
|
|
1574
|
+
reason: "the document is not an object"
|
|
1575
|
+
};
|
|
1576
|
+
if (value.tool !== "factory-ci-vitest-profile") return {
|
|
1577
|
+
kind: "unrecognized",
|
|
1578
|
+
reason: `tool is ${JSON.stringify(value.tool)} rather than "${VITEST_PROFILE_TOOL}"`
|
|
1579
|
+
};
|
|
1580
|
+
if (value.schemaVersion !== 1) return {
|
|
1581
|
+
kind: "unrecognized",
|
|
1582
|
+
reason: `schemaVersion is ${JSON.stringify(value.schemaVersion)} rather than 1`
|
|
1583
|
+
};
|
|
1584
|
+
if (!(Array.isArray(value.command) && value.command.every((part) => typeof part === "string"))) return {
|
|
1585
|
+
kind: "unrecognized",
|
|
1586
|
+
reason: "command is not an array of strings"
|
|
1587
|
+
};
|
|
1588
|
+
const environment = environmentFailure(value.environment);
|
|
1589
|
+
if (environment !== void 0) return {
|
|
1590
|
+
kind: "unrecognized",
|
|
1591
|
+
reason: environment
|
|
1592
|
+
};
|
|
1593
|
+
if (!isRecord(value.options)) return {
|
|
1594
|
+
kind: "unrecognized",
|
|
1595
|
+
reason: "options is not an object"
|
|
1596
|
+
};
|
|
1597
|
+
if (!isFiniteNumber(value.options.maxWorkers)) return {
|
|
1598
|
+
kind: "unrecognized",
|
|
1599
|
+
reason: "options.maxWorkers is not a finite number"
|
|
1600
|
+
};
|
|
1601
|
+
if (!Array.isArray(value.runs)) return {
|
|
1602
|
+
kind: "unrecognized",
|
|
1603
|
+
reason: "runs is not an array"
|
|
1604
|
+
};
|
|
1605
|
+
for (const [index, run] of value.runs.entries()) {
|
|
1606
|
+
const failure = runFailure(run, index);
|
|
1607
|
+
if (failure !== void 0) return {
|
|
1608
|
+
kind: "unrecognized",
|
|
1609
|
+
reason: failure
|
|
1610
|
+
};
|
|
1611
|
+
}
|
|
1612
|
+
const summary = summaryFailure(value.summary);
|
|
1613
|
+
if (summary !== void 0) return {
|
|
1614
|
+
kind: "unrecognized",
|
|
1615
|
+
reason: summary
|
|
1616
|
+
};
|
|
1617
|
+
return {
|
|
1618
|
+
kind: "profile",
|
|
1619
|
+
profile: value
|
|
1620
|
+
};
|
|
1621
|
+
};
|
|
1622
|
+
//#endregion
|
|
1264
1623
|
//#region src/workflow-shell-lint.ts
|
|
1265
1624
|
const RUN_KEY = /^(?<indent>\s*)(?:-\s+)?run:(?<inline>.*)$/u;
|
|
1266
1625
|
/**
|
|
@@ -1525,4 +1884,4 @@ const assertWorkflowShellParses = (yaml, options) => {
|
|
|
1525
1884
|
throw new Error(`${options.source} emits shell bash cannot parse; the runner would treat it as a no-op:\n${detail}`);
|
|
1526
1885
|
};
|
|
1527
1886
|
//#endregion
|
|
1528
|
-
export { 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_STEP_ID, GitHubApiError, NODE_PNPM_ACTION_FAMILY_NODE24, VITEST_PROFILE_SCHEMA_VERSION, VITEST_PROFILE_TOOL, VitestProfileError, assertProofReuseCoverage, assertWorkflowShellParses, bundleAlchemyEntry, captureVitestProfileEnvironment, executeAlchemyEntry, factoryProofGateScript, factoryProofGateStep, factoryWorkflow, githubAppJwt, isLocalPreviewStage, localPreviewStage, mintInstallationToken, normalizeVitestProfileSample, parseLocalPreviewStage, proofReuseCoverage, proofReuseRequiredCommands, resolveProofReuseCommands, runVitestProfile, workflowRunBlocks, workflowShellParseFailures, writeVitestProfile };
|
|
1887
|
+
export { FACTORY_CANDIDATE_PULL_REQUEST_TYPES, 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_PROOF_TIMING_START_STEP_NAME, FACTORY_PROOF_TIMING_STEP_ID, FACTORY_PROOF_TIMING_SUMMARY_STEP_NAME, GitHubApiError, NODE_PNPM_ACTION_FAMILY_NODE24, VITEST_PROFILE_SCHEMA_VERSION, VITEST_PROFILE_TOOL, VitestProfileError, assertProofReuseCoverage, assertWorkflowShellParses, bundleAlchemyEntry, captureVitestProfileEnvironment, executeAlchemyEntry, factoryCandidateOrPushCondition, factoryProductionImpactWorkflow, factoryProofGateScript, factoryProofGateStep, factoryProofReuseSummaryScript, factoryProofReuseSummaryStep, factoryProofTimingStartStep, factoryWorkflow, githubAppJwt, isLocalPreviewStage, localPreviewStage, mintInstallationToken, normalizeVitestProfileSample, parseLocalPreviewStage, productionImpactTargetOutput, proofReuseCoverage, proofReuseRequiredCommands, readVitestProfileDocument, resolveProofReuseCommands, runVitestProfile, workflowRunBlocks, workflowShellParseFailures, writeVitestProfile };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@patronage/factory-ci",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.9",
|
|
4
4
|
"description": "Deep CI and deploy building blocks for Patronage factory projects: workflow source artifacts, hosted diff classification, Alchemy entry execution, and disposable-stage semantics",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"alchemy",
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pull-request events that can create or refresh a Factory candidate run.
|
|
3
|
+
*
|
|
4
|
+
* GitHub's draft boolean remains the lifecycle authority: these triggers make
|
|
5
|
+
* a run visible, while `factoryCandidateOrPushCondition` keeps substantive
|
|
6
|
+
* jobs idle until the pull request is a candidate.
|
|
7
|
+
*/
|
|
8
|
+
export const FACTORY_CANDIDATE_PULL_REQUEST_TYPES = Object.freeze([
|
|
9
|
+
"opened",
|
|
10
|
+
"synchronize",
|
|
11
|
+
"reopened",
|
|
12
|
+
"ready_for_review",
|
|
13
|
+
] as const);
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Build a GitHub Actions job condition for candidate PRs and merge-target
|
|
17
|
+
* pushes. The caller owns triggers, jobs, runners, permissions, and topology.
|
|
18
|
+
*/
|
|
19
|
+
export const factoryCandidateOrPushCondition = (
|
|
20
|
+
candidateCondition?: string
|
|
21
|
+
): string => {
|
|
22
|
+
const candidate = [
|
|
23
|
+
"github.event_name == 'pull_request'",
|
|
24
|
+
"github.event.pull_request.draft != true",
|
|
25
|
+
...(candidateCondition ? [candidateCondition] : []),
|
|
26
|
+
].join(" && ");
|
|
27
|
+
|
|
28
|
+
return ["github.event_name == 'push'", `(${candidate})`].join(" || ");
|
|
29
|
+
};
|