@patronage/software-factory 1.0.0-alpha.42 → 1.0.0-alpha.44
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/dist/index.d.ts +37 -14
- package/dist/index.js +248 -19
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -239,8 +239,8 @@ declare const boundaryReviewProofBaseSchema: z.ZodObject<{
|
|
|
239
239
|
mergedSha: z.ZodOptional<z.ZodString>;
|
|
240
240
|
pr: z.ZodNumber;
|
|
241
241
|
state: z.ZodEnum<{
|
|
242
|
-
open: "open";
|
|
243
242
|
merged: "merged";
|
|
243
|
+
open: "open";
|
|
244
244
|
}>;
|
|
245
245
|
wave: z.ZodOptional<z.ZodString>;
|
|
246
246
|
}, z.core.$loose>>;
|
|
@@ -283,8 +283,8 @@ declare const boundaryReviewProofSchema: z.ZodObject<{
|
|
|
283
283
|
mergedSha: z.ZodOptional<z.ZodString>;
|
|
284
284
|
pr: z.ZodNumber;
|
|
285
285
|
state: z.ZodEnum<{
|
|
286
|
-
open: "open";
|
|
287
286
|
merged: "merged";
|
|
287
|
+
open: "open";
|
|
288
288
|
}>;
|
|
289
289
|
wave: z.ZodOptional<z.ZodString>;
|
|
290
290
|
}, z.core.$loose>>;
|
|
@@ -1752,8 +1752,8 @@ declare const mergeFreezeStateSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
1752
1752
|
generationId: z.ZodNumber;
|
|
1753
1753
|
headSha: z.ZodString;
|
|
1754
1754
|
outcome: z.ZodEnum<{
|
|
1755
|
-
active: "active";
|
|
1756
1755
|
stale: "stale";
|
|
1756
|
+
active: "active";
|
|
1757
1757
|
}>;
|
|
1758
1758
|
reason: z.ZodString;
|
|
1759
1759
|
recordedAt: z.ZodISODateTime;
|
|
@@ -2040,10 +2040,10 @@ declare const managedReadinessLedgerSchema: z.ZodObject<{
|
|
|
2040
2040
|
reviewedHeadSha: z.ZodOptional<z.ZodString>;
|
|
2041
2041
|
reviewedPatchId: z.ZodOptional<z.ZodString>;
|
|
2042
2042
|
status: z.ZodEnum<{
|
|
2043
|
-
stale: "stale";
|
|
2044
2043
|
blocked: "blocked";
|
|
2045
2044
|
"not-required": "not-required";
|
|
2046
2045
|
current: "current";
|
|
2046
|
+
stale: "stale";
|
|
2047
2047
|
missing: "missing";
|
|
2048
2048
|
}>;
|
|
2049
2049
|
}, z.core.$strip>;
|
|
@@ -2052,10 +2052,10 @@ declare const managedReadinessLedgerSchema: z.ZodObject<{
|
|
|
2052
2052
|
reviewedHeadSha: z.ZodOptional<z.ZodString>;
|
|
2053
2053
|
reviewedPatchId: z.ZodOptional<z.ZodString>;
|
|
2054
2054
|
status: z.ZodEnum<{
|
|
2055
|
-
stale: "stale";
|
|
2056
2055
|
blocked: "blocked";
|
|
2057
2056
|
"not-required": "not-required";
|
|
2058
2057
|
current: "current";
|
|
2058
|
+
stale: "stale";
|
|
2059
2059
|
missing: "missing";
|
|
2060
2060
|
}>;
|
|
2061
2061
|
}, z.core.$strip>>;
|
|
@@ -2089,7 +2089,7 @@ type ReadinessStatus = "ready" | "blocked";
|
|
|
2089
2089
|
type ManagedReadinessLedger = z.infer<typeof managedReadinessLedgerSchema>;
|
|
2090
2090
|
declare function validateManagedReadinessLedger(value: unknown): ManagedReadinessLedger;
|
|
2091
2091
|
declare namespace status_check_rollup_d_exports {
|
|
2092
|
-
export { CheckState, HOSTED_VERIFY_CHECK_NAME, HOSTED_VERIFY_DRAFT_CHECK_NAME, StatusCheckRollup, hostedVerifyCheckState, isDraftHostedVerifyCheck, isExcludedFromAdmissionChecks, isFactoryReadyCheck, isHostedVerifyCheck, isSupersededHostedVerifyCheck, pendingCheckNames, statusCheckState };
|
|
2092
|
+
export { CheckState, HOSTED_VERIFY_CHECK_NAME, HOSTED_VERIFY_DRAFT_CHECK_NAME, StatusCheckRollup, hostedVerifyCheckState, isCancelledPrStatusHudCheck, isDraftHostedVerifyCheck, isExcludedFromAdmissionChecks, isFactoryReadyCheck, isHostedVerifyCheck, isSupersededHostedVerifyCheck, pendingCheckNames, statusCheckState };
|
|
2093
2093
|
}
|
|
2094
2094
|
type CheckState = "passed" | "failed" | "pending" | "none" | "unknown";
|
|
2095
2095
|
interface StatusCheckRollup {
|
|
@@ -2150,10 +2150,27 @@ declare const HOSTED_VERIFY_DRAFT_CHECK_NAME = "verify (draft)";
|
|
|
2150
2150
|
* `verify` row already answers for, so admission does not judge it.
|
|
2151
2151
|
*/
|
|
2152
2152
|
declare const isDraftHostedVerifyCheck: (check: StatusCheckRollup) => boolean;
|
|
2153
|
+
/**
|
|
2154
|
+
* Is this a cancelled run of the PR status HUD job (#1056)?
|
|
2155
|
+
*
|
|
2156
|
+
* The HUD job keeps one stable name across Verify generations, so a superseded
|
|
2157
|
+
* run and its replacement share a context and `effectiveChecks` keeps only the
|
|
2158
|
+
* newer. But the Verify workflow cancels an in-flight pull-request run when the
|
|
2159
|
+
* next one starts (`ready_for_review` is the next one), and until the
|
|
2160
|
+
* replacement's HUD job appears, the rollup carries only the CANCELLED row. A
|
|
2161
|
+
* readiness read in that window would report a failed required check that
|
|
2162
|
+
* names a job nobody broke.
|
|
2163
|
+
*
|
|
2164
|
+
* The HUD only presents. It verifies nothing and never gates, so its
|
|
2165
|
+
* cancellation says nothing about the candidate. Only a cancellation is
|
|
2166
|
+
* excluded: a HUD run that failed still reads as failed, and every
|
|
2167
|
+
* verification and deploy check keeps its own state.
|
|
2168
|
+
*/
|
|
2169
|
+
declare const isCancelledPrStatusHudCheck: (check: StatusCheckRollup) => boolean;
|
|
2153
2170
|
/**
|
|
2154
2171
|
* The rollup rows an admission read must not judge: the factory's own branded
|
|
2155
|
-
* checks (it would be judging itself)
|
|
2156
|
-
*
|
|
2172
|
+
* checks (it would be judging itself), the draft-head summary residue above,
|
|
2173
|
+
* and a cancelled PR status HUD run.
|
|
2157
2174
|
*/
|
|
2158
2175
|
declare const isExcludedFromAdmissionChecks: (check: StatusCheckRollup) => boolean;
|
|
2159
2176
|
/**
|
|
@@ -2512,6 +2529,12 @@ interface CompletedFactoryCheckSnapshot {
|
|
|
2512
2529
|
* generation (#1158). Absent when the newest generation stands.
|
|
2513
2530
|
*/
|
|
2514
2531
|
readonly readPastRunIds?: readonly number[];
|
|
2532
|
+
/**
|
|
2533
|
+
* Merge freeze only (#1503): why readiness refuses to read the freeze at
|
|
2534
|
+
* this tip, when it does. The row then presents that refusal, never the
|
|
2535
|
+
* run's own conclusion.
|
|
2536
|
+
*/
|
|
2537
|
+
readonly refusal?: string;
|
|
2515
2538
|
}
|
|
2516
2539
|
interface FailedPreviewSnapshot {
|
|
2517
2540
|
readonly completedAt: string;
|
|
@@ -3131,10 +3154,10 @@ declare const loadEvidenceEnvelopes: (cwd: string) => LoadedEvidenceEnvelope[];
|
|
|
3131
3154
|
//#region src/review-proof-applicability.d.ts
|
|
3132
3155
|
declare const REVIEW_STATUS_VALUES: readonly ["not-required", "current", "stale", "missing", "blocked"];
|
|
3133
3156
|
declare const reviewStatusSchema: z.ZodEnum<{
|
|
3134
|
-
stale: "stale";
|
|
3135
3157
|
blocked: "blocked";
|
|
3136
3158
|
"not-required": "not-required";
|
|
3137
3159
|
current: "current";
|
|
3160
|
+
stale: "stale";
|
|
3138
3161
|
missing: "missing";
|
|
3139
3162
|
}>;
|
|
3140
3163
|
type ReviewStatus = z.infer<typeof reviewStatusSchema>;
|
|
@@ -3242,8 +3265,8 @@ declare const boundaryCheckProofSchema: z.ZodObject<{
|
|
|
3242
3265
|
pr: z.ZodNumber;
|
|
3243
3266
|
sha: z.ZodString;
|
|
3244
3267
|
state: z.ZodEnum<{
|
|
3245
|
-
open: "open";
|
|
3246
3268
|
merged: "merged";
|
|
3269
|
+
open: "open";
|
|
3247
3270
|
}>;
|
|
3248
3271
|
wave: z.ZodString;
|
|
3249
3272
|
}, z.core.$strip>>;
|
|
@@ -4680,13 +4703,13 @@ declare const evaluateReadiness: (input: EvaluationInput) => {
|
|
|
4680
4703
|
reviews: {
|
|
4681
4704
|
correctness: {
|
|
4682
4705
|
required: boolean;
|
|
4683
|
-
status: "
|
|
4706
|
+
status: "blocked" | "not-required" | "current" | "stale" | "missing";
|
|
4684
4707
|
reviewedHeadSha?: string | undefined;
|
|
4685
4708
|
reviewedPatchId?: string | undefined;
|
|
4686
4709
|
};
|
|
4687
4710
|
security?: {
|
|
4688
4711
|
required: boolean;
|
|
4689
|
-
status: "
|
|
4712
|
+
status: "blocked" | "not-required" | "current" | "stale" | "missing";
|
|
4690
4713
|
reviewedHeadSha?: string | undefined;
|
|
4691
4714
|
reviewedPatchId?: string | undefined;
|
|
4692
4715
|
} | undefined;
|
|
@@ -4799,13 +4822,13 @@ declare const evaluateReadiness: (input: EvaluationInput) => {
|
|
|
4799
4822
|
reviews: {
|
|
4800
4823
|
correctness: {
|
|
4801
4824
|
required: boolean;
|
|
4802
|
-
status: "
|
|
4825
|
+
status: "blocked" | "not-required" | "current" | "stale" | "missing";
|
|
4803
4826
|
reviewedHeadSha?: string | undefined;
|
|
4804
4827
|
reviewedPatchId?: string | undefined;
|
|
4805
4828
|
};
|
|
4806
4829
|
security?: {
|
|
4807
4830
|
required: boolean;
|
|
4808
|
-
status: "
|
|
4831
|
+
status: "blocked" | "not-required" | "current" | "stale" | "missing";
|
|
4809
4832
|
reviewedHeadSha?: string | undefined;
|
|
4810
4833
|
reviewedPatchId?: string | undefined;
|
|
4811
4834
|
} | undefined;
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { execFile, execFileSync, spawn, spawnSync } from "node:child_process";
|
|
|
5
5
|
import { EventEmitter, once } from "node:events";
|
|
6
6
|
import * as nodeFs from "node:fs";
|
|
7
7
|
import { accessSync, appendFileSync, closeSync, constants, cpSync, existsSync, fsyncSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
8
|
-
import { FACTORY_APP_BROKER_ORIGIN, FACTORY_APP_BROKER_PATH, FACTORY_CURSOR_OIDC_AUDIENCE, FACTORY_PROOF_GATE_APP_ID, FACTORY_PROOF_GATE_STEP_NAME, GitHubApiError, mintInstallationToken, previewProofInventory, productionImpactTargetOutput, readVitestProfileDocument } from "@patronage/factory-ci";
|
|
8
|
+
import { FACTORY_APP_BROKER_ORIGIN, FACTORY_APP_BROKER_PATH, FACTORY_CURSOR_OIDC_AUDIENCE, FACTORY_PROOF_GATE_APP_ID, FACTORY_PROOF_GATE_STEP_NAME, FACTORY_PR_STATUS_HUD_JOB_NAME, GitHubApiError, assertPreviewCleanupStackPolicy, assertPreviewCleanupTargetsAbsent, discoverOrphanedPreviewCleanupTargets, discoverPreviewCleanupTargets, mintInstallationToken, parsePreviewCleanupResidue, parsePreviewCleanupTargets, previewCleanupGithubOutput, previewCleanupResiduePullRequests, previewProofInventory, productionImpactTargetOutput, readPullRequestStates, readVitestProfileDocument, recordPreviewCleanupOutcome } from "@patronage/factory-ci";
|
|
9
9
|
import http from "node:http";
|
|
10
10
|
import { isDeepStrictEqual, promisify } from "node:util";
|
|
11
11
|
import os, { homedir, tmpdir } from "node:os";
|
|
@@ -22,7 +22,7 @@ import { AsyncLocalStorage } from "node:async_hooks";
|
|
|
22
22
|
import { pathToFileURL } from "node:url";
|
|
23
23
|
//#region package.json
|
|
24
24
|
var name = "@patronage/software-factory";
|
|
25
|
-
var version = "1.0.0-alpha.
|
|
25
|
+
var version = "1.0.0-alpha.44";
|
|
26
26
|
//#endregion
|
|
27
27
|
//#region src/git-globals-parse.ts
|
|
28
28
|
const VALUE_TAKING_GIT_OPTIONS = new Set([
|
|
@@ -9784,6 +9784,15 @@ const verifyProofGenerations = (runs, bound) => {
|
|
|
9784
9784
|
* created no earlier than its verification began, so `started_at` is an
|
|
9785
9785
|
* upper bound on that start. Ranking there can only make such a run newer
|
|
9786
9786
|
* than it is, so it vetoes more, never less.
|
|
9787
|
+
* - A pass with no usable typed proof is trusted only when it is the only run
|
|
9788
|
+
* at this head (`startUnknownBesideAnotherRun`, #1514). Its verification
|
|
9789
|
+
* started at some unknown instant no later than its `started_at`, so
|
|
9790
|
+
* another run posted before that `started_at` may still have been posted
|
|
9791
|
+
* after the pass began: a pass verified before a failure but posted after
|
|
9792
|
+
* it would otherwise outrank the failure. Ranked at its post, such a pass
|
|
9793
|
+
* can only veto more as an older run; as the newest, nothing is trusted
|
|
9794
|
+
* beside any other run. Proofs over the text cap are rare, so refusing
|
|
9795
|
+
* costs one fresh verification.
|
|
9787
9796
|
* - Neither `endedAt` nor a passing run's `started_at` is its start. Every
|
|
9788
9797
|
* writer posts its `pr-verify` run only once verification has finished
|
|
9789
9798
|
* (`pr:verify` on success or failure, `pr:publish` on re-post), so both say
|
|
@@ -9834,7 +9843,7 @@ const verifyProofGenerations = (runs, bound) => {
|
|
|
9834
9843
|
* reimplement this reader's payload parser. On payloads the factory's
|
|
9835
9844
|
* writer emits, both readers select the same pass or both refuse: the
|
|
9836
9845
|
* differential suite, `pr-verify-reuse-differential.test.ts`, asserts it
|
|
9837
|
-
* over
|
|
9846
|
+
* over 10,561 generated run sets. On App-signed text the writer never emits,
|
|
9838
9847
|
* the two can differ in either direction. The known instances where the
|
|
9839
9848
|
* hosted gate reuses and this reader refuses are: (a) a reused run whose
|
|
9840
9849
|
* typed proof fails `validatePrVerifyProof`; (b) a newest pass that repeats
|
|
@@ -9873,8 +9882,10 @@ const newestVerifyProofGeneration = (runs, bound) => {
|
|
|
9873
9882
|
if (selected.kind !== "newest") return;
|
|
9874
9883
|
const newest = selected.generation;
|
|
9875
9884
|
const repeatsAnotherRun = !newest.recorded && newest.payload !== void 0 && generations.some((generation) => generation !== newest && sameVerdict(generation, newest));
|
|
9876
|
-
const
|
|
9877
|
-
|
|
9885
|
+
const passing = isPassingRun(newest.run, newest.payload);
|
|
9886
|
+
const startUnknownBesideAnotherRun = passing && !newest.recorded && posts.some((post) => post.generation !== newest);
|
|
9887
|
+
const postedSinceItStarted = passing && posts.some((post) => post.generation !== newest && post.postedMs >= newest.orderMs);
|
|
9888
|
+
return repeatsAnotherRun || startUnknownBesideAnotherRun || postedSinceItStarted ? void 0 : newest;
|
|
9878
9889
|
};
|
|
9879
9890
|
/**
|
|
9880
9891
|
* Read the machine-readable `pr:verify` binding for one commit (#247).
|
|
@@ -17738,6 +17749,7 @@ var status_check_rollup_exports = /* @__PURE__ */ __exportAll({
|
|
|
17738
17749
|
HOSTED_VERIFY_CHECK_NAME: () => HOSTED_VERIFY_CHECK_NAME,
|
|
17739
17750
|
HOSTED_VERIFY_DRAFT_CHECK_NAME: () => HOSTED_VERIFY_DRAFT_CHECK_NAME,
|
|
17740
17751
|
hostedVerifyCheckState: () => hostedVerifyCheckState,
|
|
17752
|
+
isCancelledPrStatusHudCheck: () => isCancelledPrStatusHudCheck,
|
|
17741
17753
|
isDraftHostedVerifyCheck: () => isDraftHostedVerifyCheck,
|
|
17742
17754
|
isExcludedFromAdmissionChecks: () => isExcludedFromAdmissionChecks,
|
|
17743
17755
|
isFactoryReadyCheck: () => isFactoryReadyCheck,
|
|
@@ -17812,11 +17824,28 @@ const HOSTED_VERIFY_DRAFT_CHECK_NAME = `${HOSTED_VERIFY_CHECK_NAME} (draft)`;
|
|
|
17812
17824
|
*/
|
|
17813
17825
|
const isDraftHostedVerifyCheck = (check) => check.name === HOSTED_VERIFY_DRAFT_CHECK_NAME && Boolean(check.workflowName);
|
|
17814
17826
|
/**
|
|
17827
|
+
* Is this a cancelled run of the PR status HUD job (#1056)?
|
|
17828
|
+
*
|
|
17829
|
+
* The HUD job keeps one stable name across Verify generations, so a superseded
|
|
17830
|
+
* run and its replacement share a context and `effectiveChecks` keeps only the
|
|
17831
|
+
* newer. But the Verify workflow cancels an in-flight pull-request run when the
|
|
17832
|
+
* next one starts (`ready_for_review` is the next one), and until the
|
|
17833
|
+
* replacement's HUD job appears, the rollup carries only the CANCELLED row. A
|
|
17834
|
+
* readiness read in that window would report a failed required check that
|
|
17835
|
+
* names a job nobody broke.
|
|
17836
|
+
*
|
|
17837
|
+
* The HUD only presents. It verifies nothing and never gates, so its
|
|
17838
|
+
* cancellation says nothing about the candidate. Only a cancellation is
|
|
17839
|
+
* excluded: a HUD run that failed still reads as failed, and every
|
|
17840
|
+
* verification and deploy check keeps its own state.
|
|
17841
|
+
*/
|
|
17842
|
+
const isCancelledPrStatusHudCheck = (check) => check.name === FACTORY_PR_STATUS_HUD_JOB_NAME && Boolean(check.workflowName) && (check.conclusion ?? check.state) === "CANCELLED";
|
|
17843
|
+
/**
|
|
17815
17844
|
* The rollup rows an admission read must not judge: the factory's own branded
|
|
17816
|
-
* checks (it would be judging itself)
|
|
17817
|
-
*
|
|
17845
|
+
* checks (it would be judging itself), the draft-head summary residue above,
|
|
17846
|
+
* and a cancelled PR status HUD run.
|
|
17818
17847
|
*/
|
|
17819
|
-
const isExcludedFromAdmissionChecks = (check) => isFactoryReadyCheck(check) || isDraftHostedVerifyCheck(check);
|
|
17848
|
+
const isExcludedFromAdmissionChecks = (check) => isFactoryReadyCheck(check) || isDraftHostedVerifyCheck(check) || isCancelledPrStatusHudCheck(check);
|
|
17820
17849
|
/**
|
|
17821
17850
|
* The hosted `verify` gate's state on this head, read by presence (#477).
|
|
17822
17851
|
*
|
|
@@ -17882,6 +17911,15 @@ const effectiveChecks = (rollup) => {
|
|
|
17882
17911
|
}
|
|
17883
17912
|
return currentChecks;
|
|
17884
17913
|
};
|
|
17914
|
+
/**
|
|
17915
|
+
* The current generation of each check, less the excluded rows.
|
|
17916
|
+
*
|
|
17917
|
+
* Generations collapse before the exclusion runs. An exclusion that depends on
|
|
17918
|
+
* a run's state, like the cancelled HUD run (#1056), must judge only the
|
|
17919
|
+
* newest generation: dropping it first would let an older, superseded run of
|
|
17920
|
+
* the same job stand in as current (#1550).
|
|
17921
|
+
*/
|
|
17922
|
+
const currentIncludedChecks = (rollup, exclude) => effectiveChecks(rollup ?? []).filter((check) => !exclude?.(check));
|
|
17885
17923
|
const terminalState = (check) => check.conclusion ?? check.state ?? "";
|
|
17886
17924
|
const progressState = (check) => check.status ?? check.state ?? "";
|
|
17887
17925
|
/**
|
|
@@ -17894,11 +17932,10 @@ const progressState = (check) => check.status ?? check.state ?? "";
|
|
|
17894
17932
|
* rather than dropped: a wait must not look settled because one row was
|
|
17895
17933
|
* unprintable.
|
|
17896
17934
|
*/
|
|
17897
|
-
const pendingCheckNames = (rollup, exclude) =>
|
|
17935
|
+
const pendingCheckNames = (rollup, exclude) => currentIncludedChecks(rollup, exclude).filter((check) => PENDING_STATES.has(progressState(check))).map((check) => checkContext(check) ?? check.name ?? "unnamed check");
|
|
17898
17936
|
const statusCheckState = (rollup, exclude) => {
|
|
17899
|
-
const
|
|
17900
|
-
if (
|
|
17901
|
-
const currentChecks = effectiveChecks(included);
|
|
17937
|
+
const currentChecks = currentIncludedChecks(rollup, exclude);
|
|
17938
|
+
if (currentChecks.length === 0) return "none";
|
|
17902
17939
|
if (currentChecks.every((check) => PASSED_STATES.has(terminalState(check)))) return "passed";
|
|
17903
17940
|
if (currentChecks.some((check) => FAILED_STATES.has(terminalState(check)))) return "failed";
|
|
17904
17941
|
if (currentChecks.some((check) => PENDING_STATES.has(progressState(check)))) return "pending";
|
|
@@ -18092,6 +18129,13 @@ const mergeFreezeCheckRunListItemSchema = z.object({
|
|
|
18092
18129
|
"queued"
|
|
18093
18130
|
])
|
|
18094
18131
|
});
|
|
18132
|
+
/**
|
|
18133
|
+
* Whether the reader can parse one listed run (#1550). The reader parses every
|
|
18134
|
+
* listed run before it selects one, so a single run it cannot parse refuses
|
|
18135
|
+
* the whole read, even behind a newer green generation. The PR status HUD
|
|
18136
|
+
* applies the same rule through this export rather than a copy of the schema.
|
|
18137
|
+
*/
|
|
18138
|
+
const isReadableMergeFreezeListedRun = (run) => mergeFreezeCheckRunListItemSchema.safeParse(run).success;
|
|
18095
18139
|
const completedMergeFreezeCheckRunSchema = mergeFreezeCheckRunListItemSchema.extend({
|
|
18096
18140
|
conclusion: z.enum([
|
|
18097
18141
|
"failure",
|
|
@@ -19601,6 +19645,13 @@ const checkRow = (target, snapshot, renderedAt) => {
|
|
|
19601
19645
|
target,
|
|
19602
19646
|
timestamp: utcTimestamp(renderedAt)
|
|
19603
19647
|
};
|
|
19648
|
+
if (snapshot.refusal) return {
|
|
19649
|
+
detail: snapshot.refusal,
|
|
19650
|
+
link: safeMarkdownLink("Details", snapshot.detailsUrl),
|
|
19651
|
+
status: "Failed",
|
|
19652
|
+
target: `${target} (${snapshot.refusal})`,
|
|
19653
|
+
timestamp: utcTimestamp(snapshot.completedAt)
|
|
19654
|
+
};
|
|
19604
19655
|
return {
|
|
19605
19656
|
detail: `Completed snapshot: ${snapshot.conclusion}.`,
|
|
19606
19657
|
link: safeMarkdownLink("Details", snapshot.detailsUrl),
|
|
@@ -19790,9 +19841,10 @@ const listFactoryCheckRuns = async (input, name, sha, dependencies) => {
|
|
|
19790
19841
|
}
|
|
19791
19842
|
throw new Error(`${name} pagination limit was exhausted before GitHub returned a final page.`);
|
|
19792
19843
|
};
|
|
19844
|
+
const writtenByFactoryApp = (run) => String(run.app?.id ?? "") === FACTORY_PROOF_GATE_APP_ID;
|
|
19793
19845
|
/** The Factory App's generations of one check at one SHA, each orderable. */
|
|
19794
19846
|
const factoryGenerations = (name, sha, runs) => {
|
|
19795
|
-
const candidates = runs.filter((run) =>
|
|
19847
|
+
const candidates = runs.filter((run) => writtenByFactoryApp(run) && run.head_sha === sha && run.name === name);
|
|
19796
19848
|
if (candidates.some((run) => !Number.isFinite(generationOrderMs(run.started_at)))) throw new Error(`${name} presentation snapshot has a generation with no usable started_at.`);
|
|
19797
19849
|
return candidates;
|
|
19798
19850
|
};
|
|
@@ -19824,25 +19876,52 @@ const MERGE_FREEZE_CHECK = "patronage-factory/merge-freeze";
|
|
|
19824
19876
|
* tip (#1158). The newest completed run alone would show that cancellation as
|
|
19825
19877
|
* a freeze readiness does not apply, so the row names the runs it read past.
|
|
19826
19878
|
*
|
|
19879
|
+
* Where readiness refuses to read the freeze at all, the row says so rather
|
|
19880
|
+
* than showing a conclusion readiness never trusts (#1503). A listed run the
|
|
19881
|
+
* pinned Factory App did not write refuses the whole read, as it does in the
|
|
19882
|
+
* reader (#1393): filtering it out could let an older generation win. So does
|
|
19883
|
+
* any listed run the reader cannot parse, such as an older one concluded
|
|
19884
|
+
* `cancelled` (#1550). A newest payload the selection cannot parse, or one
|
|
19885
|
+
* that contradicts its own check run, is unreadable.
|
|
19886
|
+
*
|
|
19827
19887
|
* Otherwise the row is the newest completed snapshot, as for every other check:
|
|
19828
|
-
* running, missing and ambiguous generations present as before
|
|
19829
|
-
*
|
|
19830
|
-
*
|
|
19831
|
-
* is advisory and never gates.
|
|
19888
|
+
* running, missing and ambiguous generations present as before. Every case
|
|
19889
|
+
* where the freeze stays fail-closed keeps the row red. The HUD is advisory and
|
|
19890
|
+
* never gates.
|
|
19832
19891
|
*/
|
|
19833
19892
|
const mergeFreezeSnapshot = (sha, runs) => {
|
|
19893
|
+
const foreign = runs.find((run) => !writtenByFactoryApp(run));
|
|
19894
|
+
const unparseable = runs.find((run) => !isReadableMergeFreezeListedRun(run));
|
|
19895
|
+
const refusing = foreign ?? unparseable;
|
|
19896
|
+
if (refusing) {
|
|
19897
|
+
const at = refusing.completed_at ?? refusing.started_at;
|
|
19898
|
+
if (!at) throw new Error(`${MERGE_FREEZE_CHECK} presentation snapshot has a refused run with no usable timestamp.`);
|
|
19899
|
+
return {
|
|
19900
|
+
completedAt: at,
|
|
19901
|
+
conclusion: refusing.conclusion ?? refusing.status,
|
|
19902
|
+
...refusing.details_url ? { detailsUrl: refusing.details_url } : {},
|
|
19903
|
+
headSha: sha,
|
|
19904
|
+
name: MERGE_FREEZE_CHECK,
|
|
19905
|
+
refusal: foreign ? `refused: run ${String(foreign.id)} was not written by the Factory App` : `refused: run ${String(refusing.id)} cannot be read`
|
|
19906
|
+
};
|
|
19907
|
+
}
|
|
19834
19908
|
const generations = factoryGenerations(MERGE_FREEZE_CHECK, sha, runs);
|
|
19835
19909
|
const newest = newestSnapshot(MERGE_FREEZE_CHECK, generations);
|
|
19836
19910
|
if (!newest) return newest;
|
|
19911
|
+
const unreadable = {
|
|
19912
|
+
...newest,
|
|
19913
|
+
refusal: "unreadable newest payload"
|
|
19914
|
+
};
|
|
19837
19915
|
try {
|
|
19838
|
-
const { readPast, run } = selectMergeFreezeRun(generations, sha);
|
|
19916
|
+
const { generation, readPast, run } = selectMergeFreezeRun(generations, sha);
|
|
19917
|
+
if (generation.kind === "unreadable") return unreadable;
|
|
19839
19918
|
const selected = run && readPast.length > 0 ? runSnapshot(MERGE_FREEZE_CHECK, run) : void 0;
|
|
19840
19919
|
return selected ? {
|
|
19841
19920
|
...selected,
|
|
19842
19921
|
readPastRunIds: readPast.map((past) => past.id)
|
|
19843
19922
|
} : newest;
|
|
19844
19923
|
} catch {
|
|
19845
|
-
return
|
|
19924
|
+
return unreadable;
|
|
19846
19925
|
}
|
|
19847
19926
|
};
|
|
19848
19927
|
const FAILED_PREVIEW_CONCLUSIONS = new Set([
|
|
@@ -24807,6 +24886,155 @@ function createPrStatusHudCommand(output, action = runPrStatusHud, env = process
|
|
|
24807
24886
|
}));
|
|
24808
24887
|
}
|
|
24809
24888
|
//#endregion
|
|
24889
|
+
//#region src/preview-cleanup-command.ts
|
|
24890
|
+
const ACTIONS = new Set([
|
|
24891
|
+
"audit",
|
|
24892
|
+
"discover",
|
|
24893
|
+
"record",
|
|
24894
|
+
"sweep"
|
|
24895
|
+
]);
|
|
24896
|
+
const collect = (value, previous = []) => [...previous, value];
|
|
24897
|
+
const parseAction = (value) => {
|
|
24898
|
+
if (!ACTIONS.has(value)) throw new InvalidArgumentError("must be discover, sweep, record, or audit");
|
|
24899
|
+
return value;
|
|
24900
|
+
};
|
|
24901
|
+
const policyFrom = (options) => {
|
|
24902
|
+
const stackAliases = {};
|
|
24903
|
+
for (const entry of options.stackAlias) {
|
|
24904
|
+
const separator = entry.indexOf("=");
|
|
24905
|
+
if (separator <= 0 || separator === entry.length - 1) throw new Error(`--stack-alias must be <alias>=<stack>, got "${entry}".`);
|
|
24906
|
+
stackAliases[entry.slice(0, separator)] = entry.slice(separator + 1);
|
|
24907
|
+
}
|
|
24908
|
+
return {
|
|
24909
|
+
...options.hostedStagePrefix === void 0 ? {} : { hostedStagePrefix: options.hostedStagePrefix },
|
|
24910
|
+
stackAliases,
|
|
24911
|
+
stacks: options.stack
|
|
24912
|
+
};
|
|
24913
|
+
};
|
|
24914
|
+
const repositoryFrom = (options, env) => {
|
|
24915
|
+
const [owner, repo, extra] = (options.repository ?? env.GITHUB_REPOSITORY ?? "").split("/");
|
|
24916
|
+
if (!(owner && repo) || extra !== void 0) throw new Error("preview:cleanup requires --repository <owner/repo> or GITHUB_REPOSITORY.");
|
|
24917
|
+
return {
|
|
24918
|
+
owner,
|
|
24919
|
+
repo
|
|
24920
|
+
};
|
|
24921
|
+
};
|
|
24922
|
+
const requireValue = (name, value) => {
|
|
24923
|
+
if (value === void 0 || value === "") throw new Error(`preview:cleanup requires ${name} for this action.`);
|
|
24924
|
+
return value;
|
|
24925
|
+
};
|
|
24926
|
+
const requireToken = (env, name) => {
|
|
24927
|
+
const token = env[name]?.trim();
|
|
24928
|
+
if (!token) throw new Error(`preview:cleanup requires ${name} for this action.`);
|
|
24929
|
+
return token;
|
|
24930
|
+
};
|
|
24931
|
+
const describeTargets = (count, label, deferred = 0) => `${label}: ${count} cleanup target${count === 1 ? "" : "s"}${deferred > 0 ? `, ${deferred} deferred to the next sweep` : ""}\n`;
|
|
24932
|
+
const recordOutcome = async (options, policy, deps) => {
|
|
24933
|
+
const { env, readFile } = deps;
|
|
24934
|
+
const outcome = requireValue("--outcome", options.outcome);
|
|
24935
|
+
if (outcome !== "passed" && outcome !== "failed") throw new Error("--outcome must be passed or failed.");
|
|
24936
|
+
const targetStack = requireValue("--target-stack", options.targetStack);
|
|
24937
|
+
assertPreviewCleanupStackPolicy(policy);
|
|
24938
|
+
if (!policy.stacks.includes(targetStack)) throw new Error(`--target-stack "${targetStack}" is not a declared --stack.`);
|
|
24939
|
+
const { owner, repo } = repositoryFrom(options, env);
|
|
24940
|
+
const evidence = readFile(requireValue("--evidence-file", options.evidenceFile)).split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
|
|
24941
|
+
const server = env.GITHUB_SERVER_URL?.trim();
|
|
24942
|
+
const runId = env.GITHUB_RUN_ID?.trim();
|
|
24943
|
+
const transport = deps.fetch === void 0 ? {} : { fetch: deps.fetch };
|
|
24944
|
+
const result = await recordPreviewCleanupOutcome({
|
|
24945
|
+
evidence,
|
|
24946
|
+
...options.headSha ? { headSha: options.headSha } : {},
|
|
24947
|
+
outcome,
|
|
24948
|
+
owner,
|
|
24949
|
+
pr: requireValue("--pr", options.pr),
|
|
24950
|
+
repo,
|
|
24951
|
+
...server && runId ? { runUrl: `${server}/${owner}/${repo}/actions/runs/${runId}` } : {},
|
|
24952
|
+
stack: targetStack,
|
|
24953
|
+
stackAliases: policy.stackAliases ?? {},
|
|
24954
|
+
stage: requireValue("--target-stage", options.targetStage)
|
|
24955
|
+
}, {
|
|
24956
|
+
read: {
|
|
24957
|
+
...transport,
|
|
24958
|
+
token: requireToken(env, "GITHUB_TOKEN")
|
|
24959
|
+
},
|
|
24960
|
+
write: {
|
|
24961
|
+
...transport,
|
|
24962
|
+
token: requireToken(env, "FACTORY_GITHUB_APP_TOKEN")
|
|
24963
|
+
}
|
|
24964
|
+
});
|
|
24965
|
+
return `Cleanup ${outcome} for ${targetStack}/${options.targetStage}: ${result.kind}\n`;
|
|
24966
|
+
};
|
|
24967
|
+
async function runPreviewCleanup(options, deps = {}) {
|
|
24968
|
+
const env = deps.env ?? process.env;
|
|
24969
|
+
const readFile = deps.readFile ?? ((path) => readFileSync(path, "utf-8"));
|
|
24970
|
+
const writeOutput = deps.writeOutput ?? appendFileSync;
|
|
24971
|
+
const policy = policyFrom(options);
|
|
24972
|
+
const readResidue = () => parsePreviewCleanupResidue(JSON.parse(readFile(requireValue("--residue", options.residue))));
|
|
24973
|
+
const publish = (text) => {
|
|
24974
|
+
if (options.githubOutput !== void 0) writeOutput(options.githubOutput, text);
|
|
24975
|
+
};
|
|
24976
|
+
if (options.action === "discover") {
|
|
24977
|
+
const pr = requireValue("--pr", options.pr);
|
|
24978
|
+
const { owner, repo } = repositoryFrom(options, env);
|
|
24979
|
+
const registrations = await (deps.listRegistrations === void 0 ? previewProofInventory.list({
|
|
24980
|
+
owner,
|
|
24981
|
+
pr,
|
|
24982
|
+
repo
|
|
24983
|
+
}, {
|
|
24984
|
+
...deps.fetch === void 0 ? {} : { fetch: deps.fetch },
|
|
24985
|
+
token: requireToken(env, "GITHUB_TOKEN")
|
|
24986
|
+
}) : deps.listRegistrations({
|
|
24987
|
+
owner,
|
|
24988
|
+
pr,
|
|
24989
|
+
repo
|
|
24990
|
+
}));
|
|
24991
|
+
const targets = discoverPreviewCleanupTargets({
|
|
24992
|
+
...policy,
|
|
24993
|
+
pr,
|
|
24994
|
+
registrations,
|
|
24995
|
+
residue: readResidue()
|
|
24996
|
+
});
|
|
24997
|
+
const text = previewCleanupGithubOutput(policy, targets);
|
|
24998
|
+
publish(text);
|
|
24999
|
+
return `${describeTargets(targets.length, `PR #${pr}`)}${text}`;
|
|
25000
|
+
}
|
|
25001
|
+
if (options.action === "sweep") {
|
|
25002
|
+
const { owner, repo } = repositoryFrom(options, env);
|
|
25003
|
+
const residue = readResidue();
|
|
25004
|
+
const pullRequestStates = await readPullRequestStates({
|
|
25005
|
+
owner,
|
|
25006
|
+
prs: previewCleanupResiduePullRequests(policy, residue),
|
|
25007
|
+
repo
|
|
25008
|
+
}, {
|
|
25009
|
+
...deps.fetch === void 0 ? {} : { fetch: deps.fetch },
|
|
25010
|
+
token: requireToken(env, "GITHUB_TOKEN")
|
|
25011
|
+
});
|
|
25012
|
+
const sweep = discoverOrphanedPreviewCleanupTargets({
|
|
25013
|
+
...policy,
|
|
25014
|
+
...options.maxTargets === void 0 ? {} : { maxTargets: options.maxTargets },
|
|
25015
|
+
pullRequestStates,
|
|
25016
|
+
residue
|
|
25017
|
+
});
|
|
25018
|
+
const text = previewCleanupGithubOutput(policy, sweep.targets);
|
|
25019
|
+
publish(text);
|
|
25020
|
+
return `${describeTargets(sweep.targets.length, "Sweep", sweep.deferred)}${text}`;
|
|
25021
|
+
}
|
|
25022
|
+
if (options.action === "record") return recordOutcome(options, policy, {
|
|
25023
|
+
...deps,
|
|
25024
|
+
env,
|
|
25025
|
+
readFile
|
|
25026
|
+
});
|
|
25027
|
+
const targets = parsePreviewCleanupTargets(policy, JSON.parse(requireValue("--targets", options.targets)));
|
|
25028
|
+
assertPreviewCleanupTargetsAbsent(targets, readResidue());
|
|
25029
|
+
return `No residue remains for ${targets.length} cleanup target${targets.length === 1 ? "" : "s"}\n`;
|
|
25030
|
+
}
|
|
25031
|
+
function createPreviewCleanupCommand(output, deps = {}) {
|
|
25032
|
+
return new Command("preview:cleanup").description("Discover, sweep, record, or audit disposable preview stage cleanup for factory-ci's generated cleanup workflow").requiredOption("--action <action>", "discover (one closed PR), sweep (closed PRs' residue), record, or audit", parseAction).option("--stack <name>", "declared cleanup stack; repeat for each stack", collect, []).option("--stack-alias <alias=stack>", "another name for a declared stack; repeat as needed", collect, []).option("--hosted-stage-prefix <prefix>", "the hosted per-PR stage prefix, such as pr-").option("--repository <owner/repo>", "repository; defaults to GITHUB_REPOSITORY").option("--residue <path>", "residue JSON: an array of stack and stage rows").option("--pr <number>", "pull request number", positiveInteger$1("--pr")).option("--max-targets <count>", "sweep targets per run", positiveInteger$1("--max-targets")).option("--target-stack <stack>", "the destroyed stack, for record").option("--target-stage <stage>", "the destroyed stage, for record").option("--outcome <outcome>", "passed or failed, for record").option("--evidence-file <path>", "evidence lines, for record").option("--head-sha <sha>", "registered head SHA, for record").option("--targets <json>", "the discover job's targets output, for audit").option("--github-output <path>", "append discover or sweep outputs to the Actions output file").action(async (options) => {
|
|
25033
|
+
if (options.stack.length === 0) throw new Error("preview:cleanup requires at least one --stack.");
|
|
25034
|
+
output.stdout.write(await runPreviewCleanup(options, deps));
|
|
25035
|
+
});
|
|
25036
|
+
}
|
|
25037
|
+
//#endregion
|
|
24810
25038
|
//#region src/preview-reaper.ts
|
|
24811
25039
|
const previewBindingReferenceSchema = z.object({
|
|
24812
25040
|
binding: z.string().min(1),
|
|
@@ -25619,6 +25847,7 @@ function createProgram(options = {}) {
|
|
|
25619
25847
|
program.addCommand(createProductionImpactCommand(output));
|
|
25620
25848
|
program.addCommand(createPreviewReceiptCommand(output));
|
|
25621
25849
|
program.addCommand(createPreviewReapCommand(output));
|
|
25850
|
+
program.addCommand(createPreviewCleanupCommand(output));
|
|
25622
25851
|
program.addCommand(createCandidateImpactCommand(output));
|
|
25623
25852
|
program.addCommand(createAffectedCommand(output));
|
|
25624
25853
|
program.addCommand(createAppTokenExecCommand());
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@patronage/software-factory",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.44",
|
|
4
4
|
"description": "Shared Patronage software factory CLI and project-profile validation tools",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"picomatch": "^4.0.5",
|
|
38
38
|
"yaml": "^2.9.0",
|
|
39
39
|
"zod": "4.4.3",
|
|
40
|
-
"@patronage/factory-ci": "1.0.0-alpha.
|
|
40
|
+
"@patronage/factory-ci": "1.0.0-alpha.44"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/node": "24.13.3",
|