@patronage/factory-ci 1.0.0-alpha.26 → 1.0.0-alpha.28
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 +7 -8
- package/dist/.build-fingerprint.json +1 -1
- package/dist/alchemy/index.d.ts +14 -2
- package/dist/alchemy/index.js +37 -8
- package/dist/index.d.ts +73 -81
- package/dist/index.js +98 -172
- package/package.json +1 -1
- package/src/alchemy/credential-preflight.ts +8 -2
- package/src/alchemy/secret-name-policy.ts +6 -0
- package/src/alchemy/stage-policy.ts +7 -6
- package/src/alchemy/url-implies-auth.ts +20 -1
- package/src/alchemy-baseline.ts +19 -6
- package/src/candidate-impact-workflow.ts +2 -0
- package/src/cursor-oidc.ts +33 -0
- package/src/github-app-token.ts +40 -2
- package/src/index.ts +10 -2
- package/src/pr-status-hud-workflow.ts +2 -0
- package/src/production-impact-workflow.ts +2 -0
- package/src/proof-reuse-gate.ts +23 -213
- package/src/proof-reuse-presentation.ts +4 -8
- package/src/runnable-cli.ts +27 -0
package/dist/index.js
CHANGED
|
@@ -27,19 +27,28 @@ const ALCHEMY_BASELINE = {
|
|
|
27
27
|
const BASELINE_PACKAGE_NAMES = Object.keys(ALCHEMY_BASELINE);
|
|
28
28
|
/**
|
|
29
29
|
* Fails when the consumer's `alchemy` or `effect` pin differs from
|
|
30
|
-
* {@link ALCHEMY_BASELINE}
|
|
31
|
-
*
|
|
32
|
-
*
|
|
30
|
+
* {@link ALCHEMY_BASELINE}, or when only one of the pair is present.
|
|
31
|
+
* Alchemy peers on Effect, so an unpinned auto-installed Effect can drift
|
|
32
|
+
* outside the baseline while a lone `alchemy` pin would otherwise pass.
|
|
33
|
+
* Both packages remaining absent is not drift — they are optional peers of
|
|
34
|
+
* the `./alchemy` subpath, so a consumer that never touches it carries
|
|
35
|
+
* neither.
|
|
33
36
|
*
|
|
34
37
|
* Reads only the `packageJson` object it is handed; it never walks the
|
|
35
38
|
* filesystem.
|
|
36
39
|
*/
|
|
37
40
|
const assertAlchemyBaseline = ({ packageJson }) => {
|
|
38
41
|
const drifted = [];
|
|
42
|
+
const pinOf = (name) => packageJson.dependencies?.[name] ?? packageJson.devDependencies?.[name];
|
|
43
|
+
const pairPresent = BASELINE_PACKAGE_NAMES.some((name) => pinOf(name) !== void 0);
|
|
39
44
|
for (const name of BASELINE_PACKAGE_NAMES) {
|
|
40
|
-
const pinned =
|
|
45
|
+
const pinned = pinOf(name);
|
|
41
46
|
const baseline = ALCHEMY_BASELINE[name];
|
|
42
|
-
if (pinned
|
|
47
|
+
if (pinned === void 0) {
|
|
48
|
+
if (pairPresent) drifted.push(`missing ${name} (baseline: ${name}@${baseline})`);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (pinned !== baseline) drifted.push(`${name}@${pinned} (baseline: ${name}@${baseline})`);
|
|
43
52
|
}
|
|
44
53
|
if (drifted.length > 0) throw new Error(`Alchemy fleet baseline drift: ${drifted.join(", ")}. Bump to match ALCHEMY_BASELINE in @patronage/factory-ci, or wait for the next lockstep release.`);
|
|
45
54
|
};
|
|
@@ -433,6 +442,8 @@ var GitHubApiError = class extends Error {
|
|
|
433
442
|
this.status = status;
|
|
434
443
|
}
|
|
435
444
|
};
|
|
445
|
+
/** The Factory App broker's token: repo-scoped Checks write, nothing else. */
|
|
446
|
+
const FACTORY_APP_BROKER_TOKEN_PERMISSIONS = { checks: "write" };
|
|
436
447
|
const base64url = (value) => Buffer.from(value).toString("base64url");
|
|
437
448
|
/**
|
|
438
449
|
* The signed app JWT GitHub accepts as `Authorization: Bearer` for the App
|
|
@@ -461,6 +472,7 @@ const githubAppJwt = (credentials, options = {}) => {
|
|
|
461
472
|
};
|
|
462
473
|
const githubAppJson = (request, url, jwt, method, budget) => githubJson$1({
|
|
463
474
|
attempts: budget.attempts,
|
|
475
|
+
...budget.body === void 0 ? {} : { body: budget.body },
|
|
464
476
|
closeConnection: true,
|
|
465
477
|
fetch: request,
|
|
466
478
|
method,
|
|
@@ -493,7 +505,14 @@ const mintInstallationToken = async (input, options = {}) => {
|
|
|
493
505
|
if (typeof installation.id !== "number") throw new TypeError("GitHub App installation response omitted id");
|
|
494
506
|
installationId = installation.id;
|
|
495
507
|
}
|
|
496
|
-
const
|
|
508
|
+
const restriction = input.permissions === void 0 && input.repositories === void 0 ? void 0 : JSON.stringify({
|
|
509
|
+
...input.permissions === void 0 ? {} : { permissions: input.permissions },
|
|
510
|
+
...input.repositories === void 0 ? {} : { repositories: [...input.repositories] }
|
|
511
|
+
});
|
|
512
|
+
const minted = await githubAppJson(request, `https://api.github.com/app/installations/${installationId}/access_tokens`, jwt, "POST", restriction === void 0 ? budget : {
|
|
513
|
+
...budget,
|
|
514
|
+
body: restriction
|
|
515
|
+
});
|
|
497
516
|
if (typeof minted.token !== "string") throw new TypeError("GitHub App token response omitted token");
|
|
498
517
|
return minted.token;
|
|
499
518
|
};
|
|
@@ -504,13 +523,9 @@ const mintInstallationToken = async (input, options = {}) => {
|
|
|
504
523
|
*
|
|
505
524
|
* It answers one question, before checkout and before install: may this job
|
|
506
525
|
* reuse the local verification the factory already published for *this exact
|
|
507
|
-
* head*?
|
|
508
|
-
*
|
|
509
|
-
*
|
|
510
|
-
* extends "this exact head" to "this exact tree": a squash merge whose result
|
|
511
|
-
* tree is byte-identical to the producing pull request's proven head tree
|
|
512
|
-
* reuses that head's proof (#611). Refusing runs hosted CI; it never fails
|
|
513
|
-
* the candidate. Three
|
|
526
|
+
* head*? It asks that only on a pull request, so every default-branch push
|
|
527
|
+
* executes the full hosted suite (#863). Refusing runs hosted CI; it never
|
|
528
|
+
* fails the candidate. Three
|
|
514
529
|
* repositories had independently grown their own answer to that question and
|
|
515
530
|
* had already drifted apart, so the answer lives here once, with the App
|
|
516
531
|
* identity, check name, step identity, output names, guard, and every trust
|
|
@@ -607,54 +622,24 @@ const FACTORY_PROOF_GATE_SOURCE_URL_OUTPUT = "source-check-url";
|
|
|
607
622
|
*/
|
|
608
623
|
const FACTORY_PROOF_GATE_GUARD = `steps.${FACTORY_PROOF_GATE_STEP_ID}.outputs.${FACTORY_PROOF_GATE_OUTPUT} != 'true'`;
|
|
609
624
|
/**
|
|
610
|
-
* Pull requests
|
|
611
|
-
*
|
|
612
|
-
*
|
|
613
|
-
* It buys runner time on the merge target and pays for it in coverage. No
|
|
614
|
-
* local proof is ever written for a merged SHA, so the direct read finds
|
|
615
|
-
* nothing after a squash merge. The push path therefore exists only together
|
|
616
|
-
* with the merge fallback below: when no generation exists at the merged SHA,
|
|
617
|
-
* the gate may look up the producing pull request and reuse its head proof,
|
|
618
|
-
* but only when the merge result's tree is exactly the proven head's tree.
|
|
619
|
-
* Deploy gates keep waiting for a green required check at the merged SHA; a
|
|
620
|
-
* reused proof turns that check green through the same guarded steps a pull
|
|
621
|
-
* request uses.
|
|
622
|
-
*
|
|
623
|
-
* The trade this mode accepts: a candidate's proof may be impact-scoped, and
|
|
624
|
-
* an impact-scoped proof is a sound gate for the candidate and an unsound
|
|
625
|
-
* gate for the merge target. #931 is that defect observed — a `main` push
|
|
626
|
-
* reused a candidate proof, executed no command, and left a red package
|
|
627
|
-
* unnoticed for three days. Choose this mode only when the consumer accepts
|
|
628
|
-
* that risk for speed.
|
|
629
|
-
*
|
|
630
|
-
* The push side is deliberately the merge target only — the repository's
|
|
631
|
-
* default branch — not every pushed branch. Which branches trigger a
|
|
632
|
-
* consumer's workflow at all stays repository-owned; this condition only
|
|
633
|
-
* keeps the gate from consulting proof on pushes that are not merges into
|
|
634
|
-
* the default branch.
|
|
635
|
-
*/
|
|
636
|
-
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))";
|
|
637
|
-
/**
|
|
638
|
-
* Pull requests only — the **default** condition, and the condition a
|
|
639
|
-
* consumer gets with `reuse: "pull-request"` (#873, default since #863).
|
|
625
|
+
* Pull requests only — the gate's single condition (#873, default since #863,
|
|
626
|
+
* the only mode since the merge-target opt-out was deleted).
|
|
640
627
|
*
|
|
641
628
|
* A merge target's guarantee is about the whole tree, not the diff. A
|
|
642
629
|
* candidate proof only ever binds the diff the candidate was scoped to, so
|
|
643
630
|
* reusing it on the merged commit proves less than the merge target needs.
|
|
644
|
-
*
|
|
645
|
-
*
|
|
646
|
-
*
|
|
647
|
-
*
|
|
631
|
+
* #931 is that defect observed — a `main` push reused a candidate proof,
|
|
632
|
+
* executed no command, and left a red package unnoticed for three days. Every
|
|
633
|
+
* default-branch push therefore executes the full hosted suite, whatever
|
|
634
|
+
* proof exists.
|
|
648
635
|
*
|
|
649
636
|
* Which branches trigger the workflow at all stays repository-owned. This
|
|
650
637
|
* condition only keeps the gate from consulting proof outside a pull request.
|
|
651
638
|
*/
|
|
652
|
-
const
|
|
639
|
+
const FACTORY_PROOF_GATE_IF = "github.event_name == 'pull_request'";
|
|
653
640
|
/**
|
|
654
641
|
* The complete refusal vocabulary. Deliberately few, because these are the
|
|
655
642
|
* only distinctions the gate can honestly make from its Checks API reads.
|
|
656
|
-
* The merge fallback adds no words: a fallback that establishes nothing
|
|
657
|
-
* leaves the direct read's refusal standing, with the specifics in `detail`.
|
|
658
643
|
*
|
|
659
644
|
* - `proven` a covering, passing, unambiguous proof for this exact head
|
|
660
645
|
* - `none` no App-verified factory check run at this commit
|
|
@@ -948,100 +933,7 @@ const safeLabel$1 = (surface) => {
|
|
|
948
933
|
const cleaned = (typeof surface === "string" ? surface : "").replaceAll(/[^\w -]/gu, "").trim().slice(0, 60);
|
|
949
934
|
return cleaned.length > 0 ? cleaned : "verification";
|
|
950
935
|
};
|
|
951
|
-
|
|
952
|
-
* The push-event merge fallback (#611), emitted only for
|
|
953
|
-
* `reuse: "pull-request-and-default-branch"`. The default `"pull-request"`
|
|
954
|
-
* mode omits it: that mode's whole point is that a default-branch push
|
|
955
|
-
* executes everything, so a push path that could still reuse proof would
|
|
956
|
-
* contradict the condition above it.
|
|
957
|
-
*/
|
|
958
|
-
const MERGE_FALLBACK_BLOCK = String.raw`# Merge fallback (#611). A squash merge mints a new commit, so the direct
|
|
959
|
-
# read at a pushed merge-target head finds nothing even when the factory
|
|
960
|
-
# proved the producing pull request head. Only when the direct read found no
|
|
961
|
-
# generation at all on a push event, look up the producing pull request and
|
|
962
|
-
# reuse its head proof — and only when the merge commit's TREE id equals the
|
|
963
|
-
# proven head's tree id, which makes the pushed content byte-identical to
|
|
964
|
-
# what was verified (a clean squash of an unchanged tip). Patch identity was
|
|
965
|
-
# considered and rejected as the comparator: git patch-id normalizes
|
|
966
|
-
# whitespace and ignores base motion, so an identical patch can still
|
|
967
|
-
# produce an integrated tree that was never tested. Everything else — no
|
|
968
|
-
# unique merged producing PR, an unreadable commit, a proof that is anything
|
|
969
|
-
# but proven, or tree drift from a dirty or stale merge — leaves the direct
|
|
970
|
-
# "none" refusal standing, so the full suite runs (fail open).
|
|
971
|
-
if [ "$reason" = 'none' ] && [ "${shellExpansion$1("GITHUB_EVENT_NAME:-")}" = 'push' ]; then
|
|
972
|
-
producing_head=''
|
|
973
|
-
merge_tree=''
|
|
974
|
-
head_tree=''
|
|
975
|
-
if ! producing_pulls=$(gh api --method GET --paginate \
|
|
976
|
-
"repos/$GITHUB_REPOSITORY/commits/$HEAD_SHA/pulls" \
|
|
977
|
-
-f per_page=100 2>&1); then
|
|
978
|
-
detail="producing pull request unreadable: $producing_pulls"
|
|
979
|
-
elif ! producing_head=$(printf '%s' "$producing_pulls" | jq -r -s \
|
|
980
|
-
--arg sha "$HEAD_SHA" \
|
|
981
|
-
'[ add[]?
|
|
982
|
-
| select((.merged_at // null) != null)
|
|
983
|
-
| select((.merge_commit_sha // "") == $sha)
|
|
984
|
-
| ((.head.sha // "") | tostring) ]
|
|
985
|
-
| if length == 1 then .[0] else "" end' 2>&1); then
|
|
986
|
-
detail="producing pull request unreadable: $producing_head"
|
|
987
|
-
producing_head=''
|
|
988
|
-
elif ! [[ "$producing_head" =~ ^[0-9a-f]{40}$ ]]; then
|
|
989
|
-
detail='no single merged producing pull request at this commit'
|
|
990
|
-
producing_head=''
|
|
991
|
-
elif ! merge_commit=$(gh api --method GET \
|
|
992
|
-
"repos/$GITHUB_REPOSITORY/commits/$HEAD_SHA" 2>&1); then
|
|
993
|
-
detail="merge commit unreadable: $merge_commit"
|
|
994
|
-
elif ! merge_tree=$(printf '%s' "$merge_commit" | jq -r \
|
|
995
|
-
'(.commit.tree.sha // "") | tostring' 2>&1); then
|
|
996
|
-
detail="merge commit unreadable: $merge_tree"
|
|
997
|
-
merge_tree=''
|
|
998
|
-
elif ! [[ "$merge_tree" =~ ^[0-9a-f]{40}$ ]]; then
|
|
999
|
-
detail='merge commit carries no readable tree id'
|
|
1000
|
-
merge_tree=''
|
|
1001
|
-
elif ! head_commit=$(gh api --method GET \
|
|
1002
|
-
"repos/$GITHUB_REPOSITORY/commits/$producing_head" 2>&1); then
|
|
1003
|
-
detail="producing head commit unreadable: $head_commit"
|
|
1004
|
-
elif ! head_tree=$(printf '%s' "$head_commit" | jq -r \
|
|
1005
|
-
'(.commit.tree.sha // "") | tostring' 2>&1); then
|
|
1006
|
-
detail="producing head commit unreadable: $head_tree"
|
|
1007
|
-
head_tree=''
|
|
1008
|
-
elif ! [[ "$head_tree" =~ ^[0-9a-f]{40}$ ]]; then
|
|
1009
|
-
detail='producing head commit carries no readable tree id'
|
|
1010
|
-
head_tree=''
|
|
1011
|
-
elif [ "$merge_tree" != "$head_tree" ]; then
|
|
1012
|
-
detail='tree drift: the merge result is not the proven head tree'
|
|
1013
|
-
merge_tree=''
|
|
1014
|
-
fi
|
|
1015
|
-
if [[ "$producing_head" =~ ^[0-9a-f]{40}$ ]] \
|
|
1016
|
-
&& [[ "$merge_tree" =~ ^[0-9a-f]{40}$ ]] \
|
|
1017
|
-
&& [ "$merge_tree" = "$head_tree" ]; then
|
|
1018
|
-
gate_lookup "$producing_head"
|
|
1019
|
-
if [ "$lookup_reason" = 'proven' ]; then
|
|
1020
|
-
reason=proven
|
|
1021
|
-
mode="$lookup_mode"
|
|
1022
|
-
missing=''
|
|
1023
|
-
source_url="$lookup_url"
|
|
1024
|
-
proof_head="$producing_head"
|
|
1025
|
-
merge_note="the merge result's tree is byte-identical to the tree proven at pull request head $producing_head"
|
|
1026
|
-
else
|
|
1027
|
-
detail="producing pull request proof not reusable ($lookup_reason)"
|
|
1028
|
-
fi
|
|
1029
|
-
fi
|
|
1030
|
-
fi`;
|
|
1031
|
-
/**
|
|
1032
|
-
* The fallback with the blank lines that surround it, or nothing at all. Kept
|
|
1033
|
-
* as one piece so `pull-request-and-default-branch` emits the exact script
|
|
1034
|
-
* that mode emitted before the option existed.
|
|
1035
|
-
*
|
|
1036
|
-
* Tests for the opt-out value, matching `factoryProofGateStep`'s condition
|
|
1037
|
-
* ternary. The polarity is the point: the condition and the script are two
|
|
1038
|
-
* halves of one decision, so an unrecognized value — an untyped JavaScript
|
|
1039
|
-
* consumer's typo — must resolve to the safe default at both sites. Testing
|
|
1040
|
-
* for `"pull-request"` here would pair the PR-only condition with a script
|
|
1041
|
-
* that still carries the fallback, a combination no mode means.
|
|
1042
|
-
*/
|
|
1043
|
-
const mergeFallbackSection = (reuse) => reuse === "pull-request-and-default-branch" ? `\n${MERGE_FALLBACK_BLOCK}\n\n` : "\n";
|
|
1044
|
-
const gateScript = (required, surface, reuse) => String.raw`
|
|
936
|
+
const gateScript = (required, surface) => String.raw`
|
|
1045
937
|
set -uo pipefail
|
|
1046
938
|
|
|
1047
939
|
CHECK_NAME=${shellSingleQuote(FACTORY_PROOF_GATE_CHECK_NAME)}
|
|
@@ -1054,8 +946,6 @@ detail=''
|
|
|
1054
946
|
mode=''
|
|
1055
947
|
missing=''
|
|
1056
948
|
source_url=''
|
|
1057
|
-
proof_head="${shellExpansion$1("HEAD_SHA:-")}"
|
|
1058
|
-
merge_note=''
|
|
1059
949
|
|
|
1060
950
|
# One proof lookup at one commit. Sets lookup_reason / lookup_mode /
|
|
1061
951
|
# lookup_missing / lookup_url, or lookup_detail when the read itself
|
|
@@ -1128,7 +1018,8 @@ else
|
|
|
1128
1018
|
missing="$lookup_missing"
|
|
1129
1019
|
source_url="$lookup_url"
|
|
1130
1020
|
fi
|
|
1131
|
-
|
|
1021
|
+
|
|
1022
|
+
# The binding is App-verified, but it still reaches markdown. Only a plain
|
|
1132
1023
|
# lowercase token is quoted back; anything else is reported as unknown.
|
|
1133
1024
|
case "$mode" in
|
|
1134
1025
|
'') ;;
|
|
@@ -1156,7 +1047,7 @@ say ''
|
|
|
1156
1047
|
|
|
1157
1048
|
case "$reason" in
|
|
1158
1049
|
proven)
|
|
1159
|
-
echo "Factory proof: reusing local verification of $
|
|
1050
|
+
echo "Factory proof: reusing local verification of $HEAD_SHA; skipping the $SURFACE suite."
|
|
1160
1051
|
say "Skipped. The factory already verified this exact content, so this job did not run the $SURFACE suite a second time."
|
|
1161
1052
|
say ''
|
|
1162
1053
|
if [ -n "$source_url" ]; then
|
|
@@ -1164,20 +1055,13 @@ case "$reason" in
|
|
|
1164
1055
|
else
|
|
1165
1056
|
say "- Reused proof: the \`$CHECK_NAME\` check run published by the pinned factory GitHub App."
|
|
1166
1057
|
fi
|
|
1167
|
-
say "- Covers head: \`$
|
|
1168
|
-
if [ -n "$merge_note" ]; then
|
|
1169
|
-
say "- Merge-target reuse: $merge_note."
|
|
1170
|
-
fi
|
|
1058
|
+
say "- Covers head: \`$HEAD_SHA\`"
|
|
1171
1059
|
say "- Recorded mode: \`$mode\` (diagnostic only), outcome \`passed\`."
|
|
1172
1060
|
;;
|
|
1173
1061
|
none)
|
|
1174
1062
|
say 'Ran the full suite. No trusted factory proof covers this commit.'
|
|
1175
1063
|
say ''
|
|
1176
1064
|
say "The factory GitHub App has published no \`$CHECK_NAME\` check run at head \`$HEAD_SHA\`. Proof is keyed to the commit, so proof written for any other commit is not visible here. A pull request opened by hand carries no factory proof at all, which is normal."
|
|
1177
|
-
if [ -n "$detail" ]; then
|
|
1178
|
-
say ''
|
|
1179
|
-
say "- Merge fallback: $detail"
|
|
1180
|
-
fi
|
|
1181
1065
|
say ''
|
|
1182
1066
|
say '${CORRECTIVE_LINE}'
|
|
1183
1067
|
;;
|
|
@@ -1251,22 +1135,16 @@ fi
|
|
|
1251
1135
|
* `FACTORY_PROOF_GATE_SHELL` — so the script is also written to reach that
|
|
1252
1136
|
* write under errexit, and the tests execute it both ways.
|
|
1253
1137
|
*/
|
|
1254
|
-
const factoryProofGateScript = ({ commands,
|
|
1138
|
+
const factoryProofGateScript = ({ commands, surface }) => {
|
|
1255
1139
|
const required = proofReuseRequiredCommands(commands);
|
|
1256
|
-
return required ? gateScript(required, safeLabel$1(surface)
|
|
1140
|
+
return required ? gateScript(required, safeLabel$1(surface)) : UNUSABLE_SELECTION_SCRIPT;
|
|
1257
1141
|
};
|
|
1258
1142
|
/**
|
|
1259
1143
|
* The step itself, structurally accepted by gagen's `step()` without adding a
|
|
1260
1144
|
* gagen runtime dependency. It belongs first in the job it guards: a few API
|
|
1261
1145
|
* reads with the default `GITHUB_TOKEN`, no checkout, no install, so a proven
|
|
1262
|
-
* head costs a runner nothing beyond job startup. The
|
|
1263
|
-
*
|
|
1264
|
-
* needs only `checks: read` (#873, #863). A consumer that opts into
|
|
1265
|
-
* `reuse: "pull-request-and-default-branch"` also runs the push-event merge
|
|
1266
|
-
* fallback, which reads the producing pull request (`pull-requests: read`)
|
|
1267
|
-
* and the two commit objects whose tree ids it compares (`contents: read`).
|
|
1268
|
-
* A job that grants less loses only the fallback — the failed read degrades
|
|
1269
|
-
* to the full suite.
|
|
1146
|
+
* head costs a runner nothing beyond job startup. The step runs on pull
|
|
1147
|
+
* requests only, so it needs only `checks: read` (#873, #863).
|
|
1270
1148
|
*
|
|
1271
1149
|
* A **step, not a job**, and that is not a style preference. A separate gate
|
|
1272
1150
|
* job that errored would leave the guarded job `skipped`, and a summary job
|
|
@@ -1282,7 +1160,7 @@ const factoryProofGateStep = (options) => Object.freeze({
|
|
|
1282
1160
|
HEAD_SHA: githubExpression$1("github.event.pull_request.head.sha || github.sha")
|
|
1283
1161
|
}),
|
|
1284
1162
|
id: FACTORY_PROOF_GATE_STEP_ID,
|
|
1285
|
-
if:
|
|
1163
|
+
if: FACTORY_PROOF_GATE_IF,
|
|
1286
1164
|
name: FACTORY_PROOF_GATE_STEP_NAME,
|
|
1287
1165
|
run: factoryProofGateScript(options),
|
|
1288
1166
|
shell: FACTORY_PROOF_GATE_SHELL
|
|
@@ -1908,6 +1786,35 @@ const factoryWorkflow = (options) => {
|
|
|
1908
1786
|
});
|
|
1909
1787
|
};
|
|
1910
1788
|
//#endregion
|
|
1789
|
+
//#region src/cursor-oidc.ts
|
|
1790
|
+
/**
|
|
1791
|
+
* Pinned Cursor OIDC trust predicates for the Factory App broker (#949).
|
|
1792
|
+
*
|
|
1793
|
+
* The broker verifies a Cursor-managed Cloud Agent JWT and mints a
|
|
1794
|
+
* repo-scoped Factory App installation token. Audience, issuer, and JWKS are
|
|
1795
|
+
* not consumer settings: a knob that pointed them elsewhere would be the
|
|
1796
|
+
* whole mechanism undone, the same reason {@link FACTORY_PROOF_GATE_APP_ID}
|
|
1797
|
+
* is pinned.
|
|
1798
|
+
*/
|
|
1799
|
+
/** Cursor's OIDC issuer. Tokens minted on the agent VM carry this `iss`. */
|
|
1800
|
+
const FACTORY_CURSOR_OIDC_ISSUER = "https://api.cursor.com";
|
|
1801
|
+
/** JWKS the broker uses to verify RS256 signatures. */
|
|
1802
|
+
const FACTORY_CURSOR_OIDC_JWKS_URL = "https://api.cursor.com/keys";
|
|
1803
|
+
/**
|
|
1804
|
+
* Audience a Cursor-managed VM must mint with. The CLI and the broker share
|
|
1805
|
+
* this string; Cursor does not allowlist audiences.
|
|
1806
|
+
*/
|
|
1807
|
+
const FACTORY_CURSOR_OIDC_AUDIENCE = "Factory";
|
|
1808
|
+
/**
|
|
1809
|
+
* Runtime claim a Cursor-managed Cloud Agent VM always presents. Self-hosted
|
|
1810
|
+
* workers are not this broker's callers.
|
|
1811
|
+
*/
|
|
1812
|
+
const FACTORY_CURSOR_OIDC_MANAGED_RUNTIME = "managed";
|
|
1813
|
+
/** HQ route that exchanges a verified Cursor OIDC JWT for an App token. */
|
|
1814
|
+
const FACTORY_APP_BROKER_PATH = "/api/factory-app/installation-token";
|
|
1815
|
+
/** Production broker origin. The CLI pins this; it is not profile-declared. */
|
|
1816
|
+
const FACTORY_APP_BROKER_ORIGIN = "https://hq.patronage.com";
|
|
1817
|
+
//#endregion
|
|
1911
1818
|
//#region src/execute-alchemy-entry.ts
|
|
1912
1819
|
/** Node's spawnSync default is 1 MiB; Alchemy plans routinely exceed that. */
|
|
1913
1820
|
const EXECUTE_ALCHEMY_ENTRY_MAX_BUFFER = 64 * 1024 * 1024;
|
|
@@ -1987,10 +1894,8 @@ say ''
|
|
|
1987
1894
|
|
|
1988
1895
|
if [ "${shellExpansion("GITHUB_EVENT_NAME:-")}" = 'push' ] && [ -z "${shellExpansion("PROOF_REASON:-")}" ]; then
|
|
1989
1896
|
# A push run with no recorded reason is one where the gate step's condition
|
|
1990
|
-
# kept it off entirely, so the full suite ran with no reuse decision.
|
|
1991
|
-
#
|
|
1992
|
-
# 'pull-request-and-default-branch' reaches it only on a ref outside its
|
|
1993
|
-
# merge target.
|
|
1897
|
+
# kept it off entirely, so the full suite ran with no reuse decision. The
|
|
1898
|
+
# gate runs on pull requests only (#863), so that is every push.
|
|
1994
1899
|
say '- Path: full execution, no proof consulted'
|
|
1995
1900
|
say '- Proof-reuse gate: did not run on this ref; the full suite ran.'
|
|
1996
1901
|
say "- Verified head: \`${shellExpansion("GITHUB_SHA:-unknown")}\`"
|
|
@@ -2050,6 +1955,24 @@ const impactDemandConditions = (options) => {
|
|
|
2050
1955
|
});
|
|
2051
1956
|
};
|
|
2052
1957
|
//#endregion
|
|
1958
|
+
//#region src/runnable-cli.ts
|
|
1959
|
+
/**
|
|
1960
|
+
* `@patronage/software-factory` publishes two dist entries. `dist/index.js` is
|
|
1961
|
+
* the package export: consumers import it, so it runs no command. `dist/cli.js`
|
|
1962
|
+
* is the bin, and it is the only entry that runs one (software-factory#386).
|
|
1963
|
+
*
|
|
1964
|
+
* A workflow step that invokes `dist/index.js` therefore loads a library and
|
|
1965
|
+
* exits 0 — a green step that did nothing. This guard makes a generator refuse
|
|
1966
|
+
* that `cli` at generation time, where the failure is loud, instead of letting
|
|
1967
|
+
* it reach CI where the failure is silent. Invoke the bin by name (`psf`,
|
|
1968
|
+
* `pnpm exec psf`, `pnpm dlx psf`) or by the `dist/cli.js` path.
|
|
1969
|
+
*/
|
|
1970
|
+
const INERT_ENTRY_PATTERN = /(?:^|[/\\])dist[/\\]index\.js(?:["'\s]|$)/u;
|
|
1971
|
+
/** Shared validator for every `cli` string a factory-ci generator emits. */
|
|
1972
|
+
const assertRunnableCli = (name, cli) => {
|
|
1973
|
+
if (INERT_ENTRY_PATTERN.test(cli)) throw new Error(`${name} must invoke the software-factory bin, not the package entry dist/index.js, which imports as a library and exits 0 without running the command. Use the bin name (for example "pnpm exec psf") or dist/cli.js. Got "${cli}".`);
|
|
1974
|
+
};
|
|
1975
|
+
//#endregion
|
|
2053
1976
|
//#region src/production-impact-workflow.ts
|
|
2054
1977
|
const FACTORY_PRODUCTION_IMPACT_STEP_ID = "production_impact";
|
|
2055
1978
|
const FACTORY_PRODUCTION_IMPACT_DECISION_OUTPUT = "decision";
|
|
@@ -2075,6 +1998,7 @@ const factoryProductionImpactWorkflow = (options) => {
|
|
|
2075
1998
|
const before = options.before ?? `\${{ github.event.before }}`;
|
|
2076
1999
|
const after = options.after ?? `\${{ github.sha }}`;
|
|
2077
2000
|
const cli = options.cli ?? "pnpm exec psf";
|
|
2001
|
+
assertRunnableCli("cli", cli);
|
|
2078
2002
|
const profile = options.profilePath ? ` --profile ${shellQuote$1(options.profilePath)}` : "";
|
|
2079
2003
|
const decisionStep = {
|
|
2080
2004
|
continueOnError: true,
|
|
@@ -2133,6 +2057,7 @@ const factoryCandidateImpactWorkflow = (options) => {
|
|
|
2133
2057
|
const base = options.base ?? `\${{ github.event.pull_request.base.sha }}`;
|
|
2134
2058
|
const head = options.head ?? `\${{ github.event.pull_request.head.sha }}`;
|
|
2135
2059
|
const cli = options.cli ?? "pnpm exec psf";
|
|
2060
|
+
assertRunnableCli("cli", cli);
|
|
2136
2061
|
const profile = options.profilePath ? ` --profile ${shellQuote(options.profilePath)}` : "";
|
|
2137
2062
|
const decisionStep = {
|
|
2138
2063
|
continueOnError: true,
|
|
@@ -2431,6 +2356,7 @@ const defaultCli = "psf";
|
|
|
2431
2356
|
const factoryPrStatusHudWorkflow = (options) => {
|
|
2432
2357
|
assertPinnedAction("createGithubAppToken", options.createGithubAppToken, "actions/create-github-app-token");
|
|
2433
2358
|
const cli = options.cli || defaultCli;
|
|
2359
|
+
assertRunnableCli("cli", cli);
|
|
2434
2360
|
const mintStep = Object.freeze({
|
|
2435
2361
|
id: FACTORY_PR_STATUS_HUD_APP_TOKEN_STEP_ID,
|
|
2436
2362
|
name: "Mint the factory App token",
|
|
@@ -3708,4 +3634,4 @@ const assertWorkflowShellParses = (yaml, options) => {
|
|
|
3708
3634
|
throw new Error(`${options.source} emits shell bash cannot parse; the runner would treat it as a no-op:\n${detail}`);
|
|
3709
3635
|
};
|
|
3710
3636
|
//#endregion
|
|
3711
|
-
export { ALCHEMY_BASELINE, 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,
|
|
3637
|
+
export { ALCHEMY_BASELINE, EXECUTE_ALCHEMY_ENTRY_MAX_BUFFER, FACTORY_APP_BROKER_ORIGIN, FACTORY_APP_BROKER_PATH, FACTORY_APP_BROKER_TOKEN_PERMISSIONS, 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_CURSOR_OIDC_AUDIENCE, FACTORY_CURSOR_OIDC_ISSUER, FACTORY_CURSOR_OIDC_JWKS_URL, FACTORY_CURSOR_OIDC_MANAGED_RUNTIME, 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_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, GitHubAppTokenTransportError, 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, assertAlchemyBaseline, assertProofReuseCoverage, assertVerifyRunnerPolicy, assertWorkflowShellParses, bundleAlchemyEntry, captureVitestProfileEnvironment, executeAlchemyEntry, factoryCandidateImpactWorkflow, factoryCandidateOrPushCondition, factoryLifecycleContractLane, factoryMergeFreezeJob, factoryMergeFreezeScript, factoryPrStatusHudWorkflow, factoryPreviewCleanupTopology, factoryProductionImpactWorkflow, factoryProofGateScript, factoryProofGateStep, factoryProofReuseSummaryStep, factoryProofTimingStartStep, factoryPushIdentityConsumer, factoryPushIdentityProducer, factoryVerifyRunner, factoryWorkflow, githubAppJwt, isLocalPreviewStage, localPreviewStage, mintInstallationToken, normalizeVitestProfileSample, parseLocalPreviewStage, previewCleanupDestroyJobId, previewProofInventory, previewProofLifecycle, productionImpactTargetOutput, proofReuseCoverage, proofReuseRequiredCommands, readVitestProfileDocument, resolveProofReuseCommands, runVitestProfile, verifyRunnerPolicy, 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.28",
|
|
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",
|
|
@@ -324,8 +324,8 @@ const resolveResolvedCredential = async (
|
|
|
324
324
|
};
|
|
325
325
|
|
|
326
326
|
/**
|
|
327
|
-
* Resolves the deploy's Cloudflare credential posture and
|
|
328
|
-
*
|
|
327
|
+
* Resolves the deploy's Cloudflare credential posture and checks the token
|
|
328
|
+
* against the reviewed policy document.
|
|
329
329
|
*
|
|
330
330
|
* The check is: the environment matches the declared source, the credential
|
|
331
331
|
* resolves, Cloudflare reports the token active, the reviewed policy names
|
|
@@ -333,6 +333,12 @@ const resolveResolvedCredential = async (
|
|
|
333
333
|
* the project expects, and the permission groups match the required list with
|
|
334
334
|
* no group missing, extra, or repeated.
|
|
335
335
|
*
|
|
336
|
+
* The permission groups come from the reviewed policy document, not from
|
|
337
|
+
* Cloudflare. This function never reads live permissions from the Cloudflare
|
|
338
|
+
* API. A token whose permissions changed after the review still passes, as
|
|
339
|
+
* long as Cloudflare reports it active and the document still matches its id.
|
|
340
|
+
* See issue #1010.
|
|
341
|
+
*
|
|
336
342
|
* Throws on any failure. {@link CloudflareCredentialUnavailableError} means
|
|
337
343
|
* Cloudflare could not be asked; every other error means the credential is
|
|
338
344
|
* wrong. Nothing it throws, and nothing it returns, carries a secret value.
|
|
@@ -42,6 +42,12 @@ export interface SecretNamePolicyOptions {
|
|
|
42
42
|
* Longer values are replaced first, so a secret that contains another secret
|
|
43
43
|
* leaves no fragment behind.
|
|
44
44
|
*
|
|
45
|
+
* The policy replaces verbatim substrings of each named value only. A value
|
|
46
|
+
* that the output re-encodes passes through unredacted. URL-encoded,
|
|
47
|
+
* base64, and JSON-escaped forms are all such re-encodings. A project that
|
|
48
|
+
* prints a secret in one of those forms must not rely on this policy for it.
|
|
49
|
+
* See issue #1011.
|
|
50
|
+
*
|
|
45
51
|
* An empty `names` list yields a pass-through function only when `values` is
|
|
46
52
|
* also empty: a project with no secrets in its Alchemy output declares none,
|
|
47
53
|
* and gets no redaction. Empty names beside a populated `values` map is
|
|
@@ -33,15 +33,16 @@ export const LOCAL_PREVIEW_STAGES: StageMatcher = (stage: string) =>
|
|
|
33
33
|
*
|
|
34
34
|
* A `RegExp` with `g` or `y` advances `lastIndex` on every match, so the same
|
|
35
35
|
* pattern answers "yes" and then "no" for the same stage. A policy that
|
|
36
|
-
* alternates is worse than one that always refuses, so
|
|
37
|
-
*
|
|
38
|
-
*
|
|
36
|
+
* alternates is worse than one that always refuses, so a flagged pattern is
|
|
37
|
+
* tested through a fresh copy whose `lastIndex` starts at zero. The copy
|
|
38
|
+
* carries the complete flags: `y` anchors the match at `lastIndex`, so
|
|
39
|
+
* dropping it would let `/prod/y` accept a stage named `pre-prod` that the
|
|
40
|
+
* project's own pattern rejects. The caller's own `RegExp` is never written
|
|
41
|
+
* to, so a frozen one still works.
|
|
39
42
|
*/
|
|
40
43
|
const statelessTest = (pattern: RegExp, stage: string): boolean =>
|
|
41
44
|
pattern.global || pattern.sticky
|
|
42
|
-
? new RegExp(pattern.source, pattern.flags
|
|
43
|
-
stage
|
|
44
|
-
)
|
|
45
|
+
? new RegExp(pattern.source, pattern.flags).test(stage)
|
|
45
46
|
: pattern.test(stage);
|
|
46
47
|
|
|
47
48
|
const stageMatches = (matcher: StageMatcher, stage: string): boolean => {
|
|
@@ -129,10 +129,29 @@ const publiclyReachable = ({ props }: EvaluatedResource): boolean =>
|
|
|
129
129
|
domainReachable(props.domain) ||
|
|
130
130
|
routesReachable(props.routes);
|
|
131
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Test a pattern without carrying state between Workers.
|
|
134
|
+
*
|
|
135
|
+
* A `RegExp` with `g` or `y` advances `lastIndex` on every match, so the same
|
|
136
|
+
* pattern answers "yes" for the first Worker and "no" for the second Worker
|
|
137
|
+
* with the same binding name. A guard that alternates is worse than one that
|
|
138
|
+
* always refuses, so a flagged pattern is tested through a fresh copy whose
|
|
139
|
+
* `lastIndex` starts at zero. The copy carries the complete flags: `y`
|
|
140
|
+
* anchors the match at `lastIndex`, so dropping it would let `/TOKEN/y`
|
|
141
|
+
* credit a binding named `X_TOKEN` that the caller's pattern rejects. The
|
|
142
|
+
* caller's own `RegExp` is never written to, so a frozen one still works.
|
|
143
|
+
* Same isolation `stage-policy` already applies to stage names.
|
|
144
|
+
*/
|
|
145
|
+
const statelessTest = (pattern: RegExp, name: string): boolean =>
|
|
146
|
+
pattern.global || pattern.sticky
|
|
147
|
+
? new RegExp(pattern.source, pattern.flags).test(name)
|
|
148
|
+
: pattern.test(name);
|
|
149
|
+
|
|
132
150
|
const matchesAuthBindingName = (
|
|
133
151
|
matcher: AuthBindingNameMatcher,
|
|
134
152
|
name: string
|
|
135
|
-
): boolean =>
|
|
153
|
+
): boolean =>
|
|
154
|
+
matcher instanceof RegExp ? statelessTest(matcher, name) : matcher(name);
|
|
136
155
|
|
|
137
156
|
/**
|
|
138
157
|
* The Workers a Cloudflare Access application gates in production.
|
package/src/alchemy-baseline.ts
CHANGED
|
@@ -33,9 +33,12 @@ const BASELINE_PACKAGE_NAMES = Object.keys(
|
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
35
|
* Fails when the consumer's `alchemy` or `effect` pin differs from
|
|
36
|
-
* {@link ALCHEMY_BASELINE}
|
|
37
|
-
*
|
|
38
|
-
*
|
|
36
|
+
* {@link ALCHEMY_BASELINE}, or when only one of the pair is present.
|
|
37
|
+
* Alchemy peers on Effect, so an unpinned auto-installed Effect can drift
|
|
38
|
+
* outside the baseline while a lone `alchemy` pin would otherwise pass.
|
|
39
|
+
* Both packages remaining absent is not drift — they are optional peers of
|
|
40
|
+
* the `./alchemy` subpath, so a consumer that never touches it carries
|
|
41
|
+
* neither.
|
|
39
42
|
*
|
|
40
43
|
* Reads only the `packageJson` object it is handed; it never walks the
|
|
41
44
|
* filesystem.
|
|
@@ -44,12 +47,22 @@ export const assertAlchemyBaseline = ({
|
|
|
44
47
|
packageJson,
|
|
45
48
|
}: AssertAlchemyBaselineInput): void => {
|
|
46
49
|
const drifted: string[] = [];
|
|
50
|
+
const pinOf = (name: keyof typeof ALCHEMY_BASELINE): string | undefined =>
|
|
51
|
+
packageJson.dependencies?.[name] ?? packageJson.devDependencies?.[name];
|
|
52
|
+
const pairPresent = BASELINE_PACKAGE_NAMES.some(
|
|
53
|
+
(name) => pinOf(name) !== undefined
|
|
54
|
+
);
|
|
47
55
|
|
|
48
56
|
for (const name of BASELINE_PACKAGE_NAMES) {
|
|
49
|
-
const pinned =
|
|
50
|
-
packageJson.dependencies?.[name] ?? packageJson.devDependencies?.[name];
|
|
57
|
+
const pinned = pinOf(name);
|
|
51
58
|
const baseline = ALCHEMY_BASELINE[name];
|
|
52
|
-
if (pinned
|
|
59
|
+
if (pinned === undefined) {
|
|
60
|
+
if (pairPresent) {
|
|
61
|
+
drifted.push(`missing ${name} (baseline: ${name}@${baseline})`);
|
|
62
|
+
}
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (pinned !== baseline) {
|
|
53
66
|
drifted.push(`${name}@${pinned} (baseline: ${name}@${baseline})`);
|
|
54
67
|
}
|
|
55
68
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { WorkflowStep } from "./factory-workflow.ts";
|
|
2
2
|
import { impactDemandConditions } from "./impact-demand-condition.ts";
|
|
3
3
|
import { productionImpactTargetOutput } from "./production-impact-workflow.ts";
|
|
4
|
+
import { assertRunnableCli } from "./runnable-cli.ts";
|
|
4
5
|
|
|
5
6
|
export const FACTORY_CANDIDATE_IMPACT_STEP_ID = "candidate_impact";
|
|
6
7
|
export const FACTORY_CANDIDATE_IMPACT_DECISION_OUTPUT = "decision";
|
|
@@ -66,6 +67,7 @@ export const factoryCandidateImpactWorkflow = (
|
|
|
66
67
|
const base = options.base ?? `\${{ github.event.pull_request.base.sha }}`;
|
|
67
68
|
const head = options.head ?? `\${{ github.event.pull_request.head.sha }}`;
|
|
68
69
|
const cli = options.cli ?? "pnpm exec psf";
|
|
70
|
+
assertRunnableCli("cli", cli);
|
|
69
71
|
const profile = options.profilePath
|
|
70
72
|
? ` --profile ${shellQuote(options.profilePath)}`
|
|
71
73
|
: "";
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pinned Cursor OIDC trust predicates for the Factory App broker (#949).
|
|
3
|
+
*
|
|
4
|
+
* The broker verifies a Cursor-managed Cloud Agent JWT and mints a
|
|
5
|
+
* repo-scoped Factory App installation token. Audience, issuer, and JWKS are
|
|
6
|
+
* not consumer settings: a knob that pointed them elsewhere would be the
|
|
7
|
+
* whole mechanism undone, the same reason {@link FACTORY_PROOF_GATE_APP_ID}
|
|
8
|
+
* is pinned.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** Cursor's OIDC issuer. Tokens minted on the agent VM carry this `iss`. */
|
|
12
|
+
export const FACTORY_CURSOR_OIDC_ISSUER = "https://api.cursor.com";
|
|
13
|
+
|
|
14
|
+
/** JWKS the broker uses to verify RS256 signatures. */
|
|
15
|
+
export const FACTORY_CURSOR_OIDC_JWKS_URL = "https://api.cursor.com/keys";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Audience a Cursor-managed VM must mint with. The CLI and the broker share
|
|
19
|
+
* this string; Cursor does not allowlist audiences.
|
|
20
|
+
*/
|
|
21
|
+
export const FACTORY_CURSOR_OIDC_AUDIENCE = "Factory";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Runtime claim a Cursor-managed Cloud Agent VM always presents. Self-hosted
|
|
25
|
+
* workers are not this broker's callers.
|
|
26
|
+
*/
|
|
27
|
+
export const FACTORY_CURSOR_OIDC_MANAGED_RUNTIME = "managed";
|
|
28
|
+
|
|
29
|
+
/** HQ route that exchanges a verified Cursor OIDC JWT for an App token. */
|
|
30
|
+
export const FACTORY_APP_BROKER_PATH = "/api/factory-app/installation-token";
|
|
31
|
+
|
|
32
|
+
/** Production broker origin. The CLI pins this; it is not profile-declared. */
|
|
33
|
+
export const FACTORY_APP_BROKER_ORIGIN = "https://hq.patronage.com";
|