@deftai/directive-core 0.79.2 → 0.79.3
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/check/orchestrator.d.ts +4 -0
- package/dist/check/orchestrator.js +9 -1
- package/dist/platform/resolve-version.d.ts +2 -6
- package/dist/platform/resolve-version.js +3 -129
- package/dist/pr-merge-readiness/gh.js +13 -5
- package/dist/pr-merge-readiness/test-gh-fixtures.helpers.d.ts +18 -0
- package/dist/pr-merge-readiness/test-gh-fixtures.helpers.js +76 -0
- package/dist/pr-monitor/monitor.js +10 -1
- package/dist/pr-watch/constants.d.ts +5 -0
- package/dist/pr-watch/constants.js +27 -0
- package/dist/pr-watch/index.d.ts +1 -1
- package/dist/pr-watch/index.js +1 -1
- package/dist/pr-watch/main.d.ts +3 -0
- package/dist/pr-watch/main.js +29 -3
- package/dist/release/constants.d.ts +5 -0
- package/dist/release/constants.js +14 -2
- package/dist/release/flags.js +16 -0
- package/dist/release/main.js +7 -0
- package/dist/release/pipeline.js +7 -3
- package/dist/release/preflight.js +8 -1
- package/dist/release/skip-ci-incident.d.ts +22 -0
- package/dist/release/skip-ci-incident.js +68 -0
- package/dist/release/types.d.ts +2 -0
- package/dist/release/version.js +68 -32
- package/dist/release-e2e/entrypoint-worker.js +1 -0
- package/dist/release-e2e/entrypoint.js +13 -0
- package/dist/review-monitor/constants.d.ts +14 -0
- package/dist/review-monitor/constants.js +55 -0
- package/dist/review-monitor/index.d.ts +5 -0
- package/dist/review-monitor/index.js +5 -0
- package/dist/review-monitor/record.d.ts +50 -0
- package/dist/review-monitor/record.js +178 -0
- package/dist/review-monitor/tier-detection.d.ts +15 -0
- package/dist/review-monitor/tier-detection.js +61 -0
- package/dist/review-monitor/verify.d.ts +32 -0
- package/dist/review-monitor/verify.js +171 -0
- package/package.json +7 -3
|
@@ -20,12 +20,16 @@ export interface CheckOrchestratorSeams {
|
|
|
20
20
|
cwd: string;
|
|
21
21
|
stdio: string;
|
|
22
22
|
env?: NodeJS.ProcessEnv;
|
|
23
|
+
timeoutMs?: number;
|
|
23
24
|
}) => {
|
|
24
25
|
status: number | null;
|
|
26
|
+
signal?: NodeJS.Signals | null;
|
|
25
27
|
error?: Error;
|
|
26
28
|
};
|
|
27
29
|
/** Child-process environment (default: process.env). */
|
|
28
30
|
readonly env?: NodeJS.ProcessEnv;
|
|
31
|
+
/** Wall-clock spawn timeout in milliseconds (default: none). */
|
|
32
|
+
readonly timeoutMs?: number;
|
|
29
33
|
}
|
|
30
34
|
/**
|
|
31
35
|
* True when `path` is the directive framework source checkout root (not a
|
|
@@ -78,11 +78,16 @@ export function dispatchTaskCheck(frameworkRoot, projectRoot, seams = {}) {
|
|
|
78
78
|
cwd,
|
|
79
79
|
stdio: "inherit",
|
|
80
80
|
env: seams.env,
|
|
81
|
+
timeoutMs: seams.timeoutMs,
|
|
81
82
|
});
|
|
82
83
|
if (result.error !== undefined) {
|
|
83
84
|
process.stderr.write(`check: failed to invoke task: ${result.error.message}\n`);
|
|
84
85
|
return 2;
|
|
85
86
|
}
|
|
87
|
+
if (result.signal === "SIGTERM" && result.status === null && seams.timeoutMs !== undefined) {
|
|
88
|
+
process.stderr.write(`check: timed out after ${seams.timeoutMs / 60_000}m (Step 5 vitest coverage budget; #2652)\n`);
|
|
89
|
+
return 124;
|
|
90
|
+
}
|
|
86
91
|
return result.status ?? 1;
|
|
87
92
|
}
|
|
88
93
|
function defaultSpawn(cmd, args, opts) {
|
|
@@ -90,7 +95,10 @@ function defaultSpawn(cmd, args, opts) {
|
|
|
90
95
|
cwd: opts.cwd,
|
|
91
96
|
stdio: opts.stdio,
|
|
92
97
|
env: opts.env ?? process.env,
|
|
98
|
+
...(opts.timeoutMs !== undefined
|
|
99
|
+
? { timeout: opts.timeoutMs, killSignal: "SIGTERM" }
|
|
100
|
+
: {}),
|
|
93
101
|
});
|
|
94
|
-
return { status: result.status, error: result.error };
|
|
102
|
+
return { status: result.status, signal: result.signal, error: result.error };
|
|
95
103
|
}
|
|
96
104
|
//# sourceMappingURL=orchestrator.js.map
|
|
@@ -1,10 +1,6 @@
|
|
|
1
|
+
import { isPublishable, NonPublishableVersionError, toPep440 } from "../release/version.js";
|
|
1
2
|
import { DEV_FALLBACK, ENV_VAR } from "./constants.js";
|
|
2
|
-
export { DEV_FALLBACK, ENV_VAR };
|
|
3
|
-
export declare class NonPublishableVersionError extends Error {
|
|
4
|
-
constructor(message: string);
|
|
5
|
-
}
|
|
6
|
-
export declare function toPep440(version: string): string;
|
|
7
|
-
export declare function isPublishable(version: string): boolean;
|
|
3
|
+
export { DEV_FALLBACK, ENV_VAR, isPublishable, NonPublishableVersionError, toPep440 };
|
|
8
4
|
export declare function tagNameFromRef(ref: string): string;
|
|
9
5
|
export declare function latestPublishableTag(tags: Iterable<string>): string | null;
|
|
10
6
|
export declare function payloadIsOwnGitRoot(payloadDir: string): boolean;
|
|
@@ -2,113 +2,15 @@ import { execFileSync } from "node:child_process";
|
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { comparePublishableVersions, isPublishable, NonPublishableVersionError, toPep440, } from "../release/version.js";
|
|
5
6
|
import { DEV_FALLBACK, ENV_VAR } from "./constants.js";
|
|
6
|
-
export { DEV_FALLBACK, ENV_VAR };
|
|
7
|
-
export class NonPublishableVersionError extends Error {
|
|
8
|
-
constructor(message) {
|
|
9
|
-
super(message);
|
|
10
|
-
this.name = "NonPublishableVersionError";
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
const PRE_KIND_MAP = {
|
|
14
|
-
alpha: "a",
|
|
15
|
-
beta: "b",
|
|
16
|
-
rc: "rc",
|
|
17
|
-
};
|
|
18
|
-
const NON_PUBLISHABLE_KINDS = new Set(["test"]);
|
|
19
|
-
const PRERELEASE_RANK = {
|
|
20
|
-
alpha: 0,
|
|
21
|
-
beta: 1,
|
|
22
|
-
rc: 2,
|
|
23
|
-
"": 3,
|
|
24
|
-
};
|
|
7
|
+
export { DEV_FALLBACK, ENV_VAR, isPublishable, NonPublishableVersionError, toPep440 };
|
|
25
8
|
function frameworkRoot() {
|
|
26
9
|
const envRoot = process.env.DEFT_ROOT?.trim();
|
|
27
10
|
if (envRoot)
|
|
28
11
|
return resolve(envRoot);
|
|
29
12
|
return resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "..");
|
|
30
13
|
}
|
|
31
|
-
/** Parse `[v]X.Y.Z[-(rc|alpha|beta|test).N]` via linear scan. */
|
|
32
|
-
function parsePep440Tag(version) {
|
|
33
|
-
let i = 0;
|
|
34
|
-
if (version[i] === "v" || version[i] === "V")
|
|
35
|
-
i += 1;
|
|
36
|
-
const readInt = () => {
|
|
37
|
-
const ch = version[i] ?? "";
|
|
38
|
-
if (i >= version.length || ch < "0" || ch > "9")
|
|
39
|
-
return null;
|
|
40
|
-
let n = 0;
|
|
41
|
-
while (i < version.length) {
|
|
42
|
-
const digit = version[i] ?? "";
|
|
43
|
-
if (digit < "0" || digit > "9")
|
|
44
|
-
break;
|
|
45
|
-
n = n * 10 + Number(digit);
|
|
46
|
-
i += 1;
|
|
47
|
-
}
|
|
48
|
-
return n;
|
|
49
|
-
};
|
|
50
|
-
const major = readInt();
|
|
51
|
-
if (major === null || version[i] !== ".")
|
|
52
|
-
return null;
|
|
53
|
-
i += 1;
|
|
54
|
-
const minor = readInt();
|
|
55
|
-
if (minor === null || version[i] !== ".")
|
|
56
|
-
return null;
|
|
57
|
-
i += 1;
|
|
58
|
-
const patch = readInt();
|
|
59
|
-
if (patch === null)
|
|
60
|
-
return null;
|
|
61
|
-
if (i >= version.length)
|
|
62
|
-
return { major, minor, patch, kind: null, num: null };
|
|
63
|
-
if (version[i] !== "-")
|
|
64
|
-
return null;
|
|
65
|
-
i += 1;
|
|
66
|
-
const kindStart = i;
|
|
67
|
-
while (i < version.length && version[i] !== ".")
|
|
68
|
-
i += 1;
|
|
69
|
-
if (i >= version.length || version[i] !== ".")
|
|
70
|
-
return null;
|
|
71
|
-
const kind = version.slice(kindStart, i);
|
|
72
|
-
if (!["rc", "alpha", "beta", "test"].includes(kind))
|
|
73
|
-
return null;
|
|
74
|
-
i += 1;
|
|
75
|
-
const num = readInt();
|
|
76
|
-
if (num === null || i !== version.length)
|
|
77
|
-
return null;
|
|
78
|
-
return { major, minor, patch, kind, num };
|
|
79
|
-
}
|
|
80
|
-
export function toPep440(version) {
|
|
81
|
-
if (typeof version !== "string") {
|
|
82
|
-
throw new Error(`version must be a string, got ${typeof version}`);
|
|
83
|
-
}
|
|
84
|
-
const candidate = version.trim();
|
|
85
|
-
if (!candidate)
|
|
86
|
-
throw new Error("version must be a non-empty string");
|
|
87
|
-
const parsed = parsePep440Tag(candidate);
|
|
88
|
-
if (parsed === null) {
|
|
89
|
-
throw new Error(`Cannot normalize ${JSON.stringify(candidate)} to PEP 440: expected [v]X.Y.Z or [v]X.Y.Z-(rc|alpha|beta|test).N`);
|
|
90
|
-
}
|
|
91
|
-
const base = `${parsed.major}.${parsed.minor}.${parsed.patch}`;
|
|
92
|
-
if (parsed.kind === null)
|
|
93
|
-
return base;
|
|
94
|
-
if (NON_PUBLISHABLE_KINDS.has(parsed.kind)) {
|
|
95
|
-
throw new NonPublishableVersionError(`Version ${JSON.stringify(candidate)} carries non-publishable pre-release tag ${JSON.stringify(parsed.kind)}.${parsed.num} -- release pipeline MUST skip pyproject.toml [project].version sync for this tag.`);
|
|
96
|
-
}
|
|
97
|
-
const pepKind = PRE_KIND_MAP[parsed.kind];
|
|
98
|
-
if (pepKind === undefined) {
|
|
99
|
-
throw new Error(`Unmapped pre-release kind ${JSON.stringify(parsed.kind)} for version ${JSON.stringify(candidate)}; add it to PRE_KIND_MAP or NON_PUBLISHABLE_KINDS to keep parser in lockstep with the publishability classifier.`);
|
|
100
|
-
}
|
|
101
|
-
return `${base}${pepKind}${parsed.num}`;
|
|
102
|
-
}
|
|
103
|
-
export function isPublishable(version) {
|
|
104
|
-
try {
|
|
105
|
-
toPep440(version);
|
|
106
|
-
return true;
|
|
107
|
-
}
|
|
108
|
-
catch {
|
|
109
|
-
return false;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
14
|
export function tagNameFromRef(ref) {
|
|
113
15
|
let candidate = ref.trim();
|
|
114
16
|
if (!candidate)
|
|
@@ -123,49 +25,21 @@ export function tagNameFromRef(ref) {
|
|
|
123
25
|
candidate = candidate.slice(prefix.length);
|
|
124
26
|
return candidate.trim();
|
|
125
27
|
}
|
|
126
|
-
function publishableTagSortKey(version) {
|
|
127
|
-
const candidate = version.trim();
|
|
128
|
-
const parsed = parsePep440Tag(candidate);
|
|
129
|
-
if (parsed === null) {
|
|
130
|
-
throw new Error(`Cannot sort ${JSON.stringify(candidate)}: expected [v]X.Y.Z or [v]X.Y.Z-(rc|alpha|beta).N`);
|
|
131
|
-
}
|
|
132
|
-
if (parsed.kind !== null && NON_PUBLISHABLE_KINDS.has(parsed.kind)) {
|
|
133
|
-
throw new NonPublishableVersionError(`Version ${JSON.stringify(candidate)} carries non-publishable pre-release tag ${JSON.stringify(parsed.kind)}.`);
|
|
134
|
-
}
|
|
135
|
-
const kind = parsed.kind ?? "";
|
|
136
|
-
const prereleaseRank = PRERELEASE_RANK[kind];
|
|
137
|
-
if (prereleaseRank === undefined) {
|
|
138
|
-
throw new Error(`Unmapped pre-release kind ${JSON.stringify(parsed.kind)} for ${JSON.stringify(candidate)}`);
|
|
139
|
-
}
|
|
140
|
-
return [parsed.major, parsed.minor, parsed.patch, prereleaseRank, parsed.num ?? 0];
|
|
141
|
-
}
|
|
142
28
|
export function latestPublishableTag(tags) {
|
|
143
29
|
let bestTag = null;
|
|
144
|
-
let bestKey = null;
|
|
145
30
|
for (const raw of tags) {
|
|
146
31
|
const tag = tagNameFromRef(raw);
|
|
147
32
|
if (!tag || !isPublishable(tag))
|
|
148
33
|
continue;
|
|
149
34
|
try {
|
|
150
|
-
|
|
151
|
-
if (bestKey === null || compareTuple(key, bestKey) > 0) {
|
|
35
|
+
if (bestTag === null || comparePublishableVersions(tag, bestTag) > 0) {
|
|
152
36
|
bestTag = tag;
|
|
153
|
-
bestKey = key;
|
|
154
37
|
}
|
|
155
38
|
}
|
|
156
39
|
catch { }
|
|
157
40
|
}
|
|
158
41
|
return bestTag;
|
|
159
42
|
}
|
|
160
|
-
function compareTuple(a, b) {
|
|
161
|
-
for (let i = 0; i < a.length; i += 1) {
|
|
162
|
-
const av = a[i] ?? 0;
|
|
163
|
-
const bv = b[i] ?? 0;
|
|
164
|
-
if (av !== bv)
|
|
165
|
-
return av > bv ? 1 : -1;
|
|
166
|
-
}
|
|
167
|
-
return 0;
|
|
168
|
-
}
|
|
169
43
|
function readManifestTag(baseDir) {
|
|
170
44
|
const manifest = join(baseDir, "VERSION");
|
|
171
45
|
try {
|
|
@@ -66,7 +66,10 @@ export function fetchGreptileCommentBody(prNumber, repo, runGh) {
|
|
|
66
66
|
`repos/${resolvedRepo}/issues/${prNumber}/comments`,
|
|
67
67
|
"--paginate",
|
|
68
68
|
"--jq",
|
|
69
|
-
|
|
69
|
+
// Prefer most recently *updated* Greptile summary. Greptile edits the
|
|
70
|
+
// primary rolling summary in place; a later-created duplicate can stay
|
|
71
|
+
// SHA-stale and must not win over the refreshed primary (#1056 / #2658).
|
|
72
|
+
`[.[] | select(.user.login == "${GREPTILE_LOGIN}")] | sort_by(.updated_at) | last | .body // ""`,
|
|
70
73
|
];
|
|
71
74
|
const { returncode, stdout, stderr } = runGh(cmd);
|
|
72
75
|
if (returncode !== 0) {
|
|
@@ -123,7 +126,8 @@ function parsePaginatedComments(text) {
|
|
|
123
126
|
}
|
|
124
127
|
idx = end;
|
|
125
128
|
}
|
|
126
|
-
|
|
129
|
+
let bestBody = "";
|
|
130
|
+
let bestUpdatedAt = "";
|
|
127
131
|
for (const c of comments) {
|
|
128
132
|
if (c !== null &&
|
|
129
133
|
typeof c === "object" &&
|
|
@@ -136,13 +140,17 @@ function parsePaginatedComments(text) {
|
|
|
136
140
|
c.user.login === GREPTILE_LOGIN &&
|
|
137
141
|
"body" in c &&
|
|
138
142
|
typeof c.body === "string") {
|
|
139
|
-
|
|
143
|
+
const updatedAt = "updated_at" in c && typeof c.updated_at === "string" ? c.updated_at : "";
|
|
144
|
+
if (updatedAt >= bestUpdatedAt) {
|
|
145
|
+
bestUpdatedAt = updatedAt;
|
|
146
|
+
bestBody = c.body;
|
|
147
|
+
}
|
|
140
148
|
}
|
|
141
149
|
}
|
|
142
|
-
if (
|
|
150
|
+
if (bestBody.length === 0 && bestUpdatedAt.length === 0) {
|
|
143
151
|
return { body: "", error: "" };
|
|
144
152
|
}
|
|
145
|
-
return { body:
|
|
153
|
+
return { body: bestBody, error: "" };
|
|
146
154
|
}
|
|
147
155
|
/** Mirrors Python json.JSONDecoder.raw_decode for concatenated paginate arrays. */
|
|
148
156
|
class PaginatedJsonDecoder {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test-only gh doubles for pr-monitor / merge-readiness suites (#2652).
|
|
3
|
+
* Not part of the production CLI surface; excluded from coverage via `*.helpers.ts`.
|
|
4
|
+
*/
|
|
5
|
+
import type { RunGhFn } from "./types.js";
|
|
6
|
+
/** Empty GraphQL reviewThreads page for inline Greptile findings (#2620 / #2652). */
|
|
7
|
+
export declare const EMPTY_REVIEW_THREADS_GRAPHQL: string;
|
|
8
|
+
/** Prepend a graphql reviewThreads stub to any hermetic runGh test double. */
|
|
9
|
+
export declare function withGraphqlInlineStub(runGh: RunGhFn): RunGhFn;
|
|
10
|
+
export interface FakeRunGhOptions {
|
|
11
|
+
readonly headSha?: string;
|
|
12
|
+
readonly headOk?: boolean;
|
|
13
|
+
readonly commentsBody?: string;
|
|
14
|
+
readonly commentsOk?: boolean;
|
|
15
|
+
}
|
|
16
|
+
/** Hermetic runGh stub for monitor / merge-readiness unit tests (#2260 / #2652). */
|
|
17
|
+
export declare function fakeRunGhForMonitor(options?: FakeRunGhOptions): RunGhFn;
|
|
18
|
+
//# sourceMappingURL=test-gh-fixtures.helpers.d.ts.map
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/** Empty GraphQL reviewThreads page for inline Greptile findings (#2620 / #2652). */
|
|
2
|
+
export const EMPTY_REVIEW_THREADS_GRAPHQL = JSON.stringify({
|
|
3
|
+
data: {
|
|
4
|
+
repository: {
|
|
5
|
+
pullRequest: {
|
|
6
|
+
reviewThreads: {
|
|
7
|
+
pageInfo: { hasNextPage: false, endCursor: null },
|
|
8
|
+
nodes: [],
|
|
9
|
+
},
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
/** Prepend a graphql reviewThreads stub to any hermetic runGh test double. */
|
|
15
|
+
export function withGraphqlInlineStub(runGh) {
|
|
16
|
+
return (cmd) => {
|
|
17
|
+
if (cmd.join(" ").includes("graphql")) {
|
|
18
|
+
return { returncode: 0, stdout: EMPTY_REVIEW_THREADS_GRAPHQL, stderr: "" };
|
|
19
|
+
}
|
|
20
|
+
return runGh(cmd);
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/** Hermetic runGh stub for monitor / merge-readiness unit tests (#2260 / #2652). */
|
|
24
|
+
export function fakeRunGhForMonitor(options = {}) {
|
|
25
|
+
const headSha = options.headSha ?? "abc1234567890def1234567890abcdef12345678";
|
|
26
|
+
const headOk = options.headOk ?? true;
|
|
27
|
+
const commentsOk = options.commentsOk ?? true;
|
|
28
|
+
const commentsBody = options.commentsBody ?? "";
|
|
29
|
+
return (cmd) => {
|
|
30
|
+
if (!headOk) {
|
|
31
|
+
return { returncode: 1, stdout: "", stderr: "all-down" };
|
|
32
|
+
}
|
|
33
|
+
const joined = cmd.join(" ");
|
|
34
|
+
if (joined.includes("graphql")) {
|
|
35
|
+
return { returncode: 0, stdout: EMPTY_REVIEW_THREADS_GRAPHQL, stderr: "" };
|
|
36
|
+
}
|
|
37
|
+
if (joined.includes("headRefOid")) {
|
|
38
|
+
return { returncode: 0, stdout: `${headSha}\n`, stderr: "" };
|
|
39
|
+
}
|
|
40
|
+
if (joined.includes("/comments")) {
|
|
41
|
+
return commentsOk
|
|
42
|
+
? { returncode: 0, stdout: commentsBody, stderr: "" }
|
|
43
|
+
: { returncode: 1, stdout: "", stderr: "boom" };
|
|
44
|
+
}
|
|
45
|
+
if (joined.includes("/check-runs")) {
|
|
46
|
+
return {
|
|
47
|
+
returncode: 0,
|
|
48
|
+
stdout: JSON.stringify({
|
|
49
|
+
check_runs: [
|
|
50
|
+
{
|
|
51
|
+
name: "TypeScript (build + lint + test)",
|
|
52
|
+
status: "completed",
|
|
53
|
+
conclusion: "success",
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
}),
|
|
57
|
+
stderr: "",
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
if (joined.includes("/pulls/")) {
|
|
61
|
+
return {
|
|
62
|
+
returncode: 0,
|
|
63
|
+
stdout: JSON.stringify({
|
|
64
|
+
state: "open",
|
|
65
|
+
merged: false,
|
|
66
|
+
mergeable: true,
|
|
67
|
+
mergeable_state: "clean",
|
|
68
|
+
head: { sha: headSha },
|
|
69
|
+
}),
|
|
70
|
+
stderr: "",
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return { returncode: 1, stdout: "", stderr: `unexpected: ${joined}` };
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=test-gh-fixtures.helpers.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { cadenceIntervalAfterPoll } from "./cadence.js";
|
|
1
|
+
import { cadenceIntervalAfterPoll, cadenceIntervals } from "./cadence.js";
|
|
2
2
|
import { DEFAULT_CADENCE, EXIT_CAP_REACHED, EXIT_CLEAN, EXIT_CONFIG_ERROR, EXIT_PR_TERMINAL, } from "./constants.js";
|
|
3
3
|
import { callReadiness } from "./readiness.js";
|
|
4
4
|
const systemMonotonicClock = {
|
|
@@ -127,12 +127,21 @@ export function monitor(prNumber, repo, options = {}) {
|
|
|
127
127
|
const sleepFn = options.sleepFn ?? defaultSleep;
|
|
128
128
|
const callReadinessFn = options.callReadinessFn ?? ((n, r) => callReadiness(n, r, { runGh: options.runGh }));
|
|
129
129
|
const startedAt = clockFn.now();
|
|
130
|
+
const intervalSchedule = cadenceIntervals(cadence);
|
|
131
|
+
const minCadenceSec = intervalSchedule.length > 0
|
|
132
|
+
? Math.min(...intervalSchedule)
|
|
133
|
+
: cadenceIntervalAfterPoll(1, cadence);
|
|
134
|
+
// Fail closed when an injected clock never advances (infinite poll loop; #2652).
|
|
135
|
+
const maxPolls = Math.min(10_000, Math.ceil(capSeconds / Math.max(minCadenceSec, 1)) + intervalSchedule.length + 2);
|
|
130
136
|
let pollIndex = 0;
|
|
131
137
|
let lastPayload = {};
|
|
132
138
|
let lastExit = EXIT_CAP_REACHED;
|
|
133
139
|
let priorCadenceSeconds = cadenceIntervalAfterPoll(1, cadence);
|
|
134
140
|
while (true) {
|
|
135
141
|
pollIndex += 1;
|
|
142
|
+
if (pollIndex > maxPolls) {
|
|
143
|
+
return { exitCode: EXIT_CAP_REACHED, payload: lastPayload, pollCount: pollIndex - 1 };
|
|
144
|
+
}
|
|
136
145
|
const elapsed = clockFn.now() - startedAt;
|
|
137
146
|
if (elapsed > capSeconds) {
|
|
138
147
|
return { exitCode: EXIT_CAP_REACHED, payload: lastPayload, pollCount: pollIndex - 1 };
|
|
@@ -23,6 +23,11 @@ export declare const VERDICT_PENDING = "PENDING";
|
|
|
23
23
|
export declare const VERDICT_CONFIG = "CONFIG";
|
|
24
24
|
export declare const DEFAULT_MAX_WAIT_MINUTES = 30;
|
|
25
25
|
export declare const DEFAULT_POLL_SECONDS = 90;
|
|
26
|
+
/**
|
|
27
|
+
* Usage for `task pr:watch -- --help` / `-h` (#2652 / #1056).
|
|
28
|
+
* Canonical surface is the Task verb; engine stem is `pr-watch`.
|
|
29
|
+
*/
|
|
30
|
+
export declare const WATCH_HELP: string;
|
|
26
31
|
/**
|
|
27
32
|
* Consecutive polls with a review PRESENT but stuck on a stale (non-HEAD)
|
|
28
33
|
* commit before the loop surfaces STALL instead of waiting the full cap. Keeps
|
|
@@ -23,6 +23,33 @@ export const VERDICT_PENDING = "PENDING";
|
|
|
23
23
|
export const VERDICT_CONFIG = "CONFIG";
|
|
24
24
|
export const DEFAULT_MAX_WAIT_MINUTES = 30;
|
|
25
25
|
export const DEFAULT_POLL_SECONDS = 90;
|
|
26
|
+
/**
|
|
27
|
+
* Usage for `task pr:watch -- --help` / `-h` (#2652 / #1056).
|
|
28
|
+
* Canonical surface is the Task verb; engine stem is `pr-watch`.
|
|
29
|
+
*/
|
|
30
|
+
export const WATCH_HELP = "usage: task pr:watch -- <pr_number> [options]\n" +
|
|
31
|
+
"\n" +
|
|
32
|
+
"Blocking poll of a PR Greptile/SLizard review to a terminal three-state\n" +
|
|
33
|
+
"verdict (#1056). The invocation IS the wait — an orchestrator that promises\n" +
|
|
34
|
+
"to poll cannot silently forget. Canonical: `task pr:watch -- <N>`.\n" +
|
|
35
|
+
"Engine / CLI stem: `pr-watch` (also `directive pr watch` / `directive pr:watch`).\n" +
|
|
36
|
+
"\n" +
|
|
37
|
+
"positional arguments:\n" +
|
|
38
|
+
" pr_number GitHub pull request number (required unless --help)\n" +
|
|
39
|
+
"\n" +
|
|
40
|
+
"options:\n" +
|
|
41
|
+
" -h, --help Show this help and exit 0\n" +
|
|
42
|
+
" --one-shot Single probe (PENDING with no terminal verdict → exit 2)\n" +
|
|
43
|
+
" --json Emit the AC-4 JSON shape on stdout\n" +
|
|
44
|
+
" --max-wait-minutes N Cap for the blocking poll (default: 30)\n" +
|
|
45
|
+
" --poll-seconds N Seconds between probes (default: 90)\n" +
|
|
46
|
+
" --repo OWNER/REPO Override repository (default: GH_REPO / origin)\n" +
|
|
47
|
+
" --project-root PATH Chdir before probing (optional)\n" +
|
|
48
|
+
"\n" +
|
|
49
|
+
"exit codes:\n" +
|
|
50
|
+
" 0 CLEAN SHA-matched review, confidence > 3, no P0/P1, CI green\n" +
|
|
51
|
+
" 1 NEW_P0_P1 Blocking findings on the current (SHA-matched) review\n" +
|
|
52
|
+
" 2 ERRORED | STALL | TIMEOUT | config / usage error\n";
|
|
26
53
|
/**
|
|
27
54
|
* Consecutive polls with a review PRESENT but stuck on a stale (non-HEAD)
|
|
28
55
|
* commit before the loop surfaces STALL instead of waiting the full cap. Keeps
|
package/dist/pr-watch/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export * from "./constants.js";
|
|
2
|
-
export { cmdPrWatch, emitWatchJson, type ParsedWatchArgs, parseWatchArgs, printWatchHuman, type RunWatchOptions, runWatch, watchResultToJson, } from "./main.js";
|
|
2
|
+
export { cmdPrWatch, emitWatchJson, formatWatchHelp, type ParsedWatchArgs, parseWatchArgs, printWatchHuman, type RunWatchOptions, runWatch, watchResultToJson, } from "./main.js";
|
|
3
3
|
export { probeOnce } from "./probe.js";
|
|
4
4
|
export * from "./types.js";
|
|
5
5
|
export { formatWatchStatus, watch } from "./watch.js";
|
package/dist/pr-watch/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export * from "./constants.js";
|
|
2
|
-
export { cmdPrWatch, emitWatchJson, parseWatchArgs, printWatchHuman, runWatch, watchResultToJson, } from "./main.js";
|
|
2
|
+
export { cmdPrWatch, emitWatchJson, formatWatchHelp, parseWatchArgs, printWatchHuman, runWatch, watchResultToJson, } from "./main.js";
|
|
3
3
|
export { probeOnce } from "./probe.js";
|
|
4
4
|
export * from "./types.js";
|
|
5
5
|
export { formatWatchStatus, watch } from "./watch.js";
|
package/dist/pr-watch/main.d.ts
CHANGED
|
@@ -7,9 +7,12 @@ export interface ParsedWatchArgs {
|
|
|
7
7
|
readonly oneShot: boolean;
|
|
8
8
|
readonly emitJson: boolean;
|
|
9
9
|
readonly projectRoot: string | null;
|
|
10
|
+
readonly help: boolean;
|
|
10
11
|
readonly error?: string;
|
|
11
12
|
}
|
|
12
13
|
export declare function parseWatchArgs(argv: readonly string[]): ParsedWatchArgs;
|
|
14
|
+
/** Canonical help text for `task pr:watch -- --help` (#2652). */
|
|
15
|
+
export declare function formatWatchHelp(): string;
|
|
13
16
|
/** AC-4 stable --json shape (same field set as the #1039 Tier-1 instrumentation line). */
|
|
14
17
|
export declare function watchResultToJson(result: WatchResult): Record<string, unknown>;
|
|
15
18
|
export declare function emitWatchJson(result: WatchResult): string;
|
package/dist/pr-watch/main.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { defaultRunGh } from "../pr-merge-readiness/gh.js";
|
|
4
|
-
import { DEFAULT_MAX_WAIT_MINUTES, DEFAULT_POLL_SECONDS, EXIT_TERMINAL_ERROR, } from "./constants.js";
|
|
4
|
+
import { DEFAULT_MAX_WAIT_MINUTES, DEFAULT_POLL_SECONDS, EXIT_CLEAN, EXIT_TERMINAL_ERROR, WATCH_HELP, } from "./constants.js";
|
|
5
5
|
import { watch } from "./watch.js";
|
|
6
6
|
function fail(base, error) {
|
|
7
7
|
return { ...base, error };
|
|
@@ -15,6 +15,7 @@ export function parseWatchArgs(argv) {
|
|
|
15
15
|
oneShot: false,
|
|
16
16
|
emitJson: false,
|
|
17
17
|
projectRoot: null,
|
|
18
|
+
help: false,
|
|
18
19
|
};
|
|
19
20
|
let prNumber = null;
|
|
20
21
|
let repo = null;
|
|
@@ -23,6 +24,7 @@ export function parseWatchArgs(argv) {
|
|
|
23
24
|
let oneShot = false;
|
|
24
25
|
let emitJson = false;
|
|
25
26
|
let projectRoot = null;
|
|
27
|
+
let help = false;
|
|
26
28
|
const takePositive = (label, raw) => {
|
|
27
29
|
if (raw === undefined) {
|
|
28
30
|
return { error: `argument ${label}: expected one argument` };
|
|
@@ -35,7 +37,10 @@ export function parseWatchArgs(argv) {
|
|
|
35
37
|
};
|
|
36
38
|
for (let i = 0; i < argv.length; i += 1) {
|
|
37
39
|
const arg = argv[i];
|
|
38
|
-
if (arg === "--
|
|
40
|
+
if (arg === "--help" || arg === "-h") {
|
|
41
|
+
help = true;
|
|
42
|
+
}
|
|
43
|
+
else if (arg === "--json") {
|
|
39
44
|
emitJson = true;
|
|
40
45
|
}
|
|
41
46
|
else if (arg === "--one-shot") {
|
|
@@ -103,10 +108,26 @@ export function parseWatchArgs(argv) {
|
|
|
103
108
|
return fail(acc, `unrecognized arguments: ${arg}`);
|
|
104
109
|
}
|
|
105
110
|
}
|
|
111
|
+
if (help) {
|
|
112
|
+
return { prNumber, repo, maxWaitMinutes, pollSeconds, oneShot, emitJson, projectRoot, help };
|
|
113
|
+
}
|
|
106
114
|
if (prNumber === null) {
|
|
107
115
|
return fail(acc, "the following arguments are required: pr_number");
|
|
108
116
|
}
|
|
109
|
-
return {
|
|
117
|
+
return {
|
|
118
|
+
prNumber,
|
|
119
|
+
repo,
|
|
120
|
+
maxWaitMinutes,
|
|
121
|
+
pollSeconds,
|
|
122
|
+
oneShot,
|
|
123
|
+
emitJson,
|
|
124
|
+
projectRoot,
|
|
125
|
+
help,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/** Canonical help text for `task pr:watch -- --help` (#2652). */
|
|
129
|
+
export function formatWatchHelp() {
|
|
130
|
+
return WATCH_HELP;
|
|
110
131
|
}
|
|
111
132
|
/** Match Python json.dumps(..., indent=2) default ensure_ascii=True. */
|
|
112
133
|
function pythonJsonDumps(value) {
|
|
@@ -161,8 +182,13 @@ export function printWatchHuman(result) {
|
|
|
161
182
|
}
|
|
162
183
|
export function runWatch(argv, options = {}) {
|
|
163
184
|
const args = parseWatchArgs(argv);
|
|
185
|
+
if (args.help) {
|
|
186
|
+
process.stdout.write(formatWatchHelp());
|
|
187
|
+
return EXIT_CLEAN;
|
|
188
|
+
}
|
|
164
189
|
if (args.error !== undefined) {
|
|
165
190
|
process.stderr.write(`pr_watch: ${args.error}\n`);
|
|
191
|
+
process.stderr.write(`Try: task pr:watch -- --help\n`);
|
|
166
192
|
return EXIT_TERMINAL_ERROR;
|
|
167
193
|
}
|
|
168
194
|
let restoreCwd = null;
|
|
@@ -20,6 +20,11 @@ export declare const DESTRUCTIVE_GH_GATE_BYPASS_ENV = "DEFT_ALLOW_DESTRUCTIVE_GH
|
|
|
20
20
|
export declare const RELEASE_PREFLIGHT_ENV = "DEFT_RELEASE_PREFLIGHT";
|
|
21
21
|
/** Set only by release Step-5 preflight when --allow-coverage-debt=#N is supplied (#2573). */
|
|
22
22
|
export declare const COVERAGE_DEBT_ENV = "DEFT_ALLOW_COVERAGE_DEBT";
|
|
23
|
+
/** Hard wall-clock cap for release Step 5 `task check` / vitest coverage (#2652). */
|
|
24
|
+
export declare const RELEASE_CHECK_TIMEOUT_MS: number;
|
|
25
|
+
export declare const RELEASE_CHECK_TIMEOUT_MINUTES = 20;
|
|
26
|
+
/** Vitest coverage step cap in GHA CI (mirrors release Step 5 budget, #2652). */
|
|
27
|
+
export declare const CI_VITEST_COVERAGE_TIMEOUT_MINUTES = 20;
|
|
23
28
|
export declare const PYPROJECT_VERSION_LINE_RE: RegExp;
|
|
24
29
|
/** Byte-identical argparse --help from scripts/release.py (Python 3.12). */
|
|
25
30
|
export declare const RELEASE_HELP: string;
|
|
@@ -28,10 +28,16 @@ export const DESTRUCTIVE_GH_GATE_BYPASS_ENV = "DEFT_ALLOW_DESTRUCTIVE_GH_VERBS";
|
|
|
28
28
|
export const RELEASE_PREFLIGHT_ENV = "DEFT_RELEASE_PREFLIGHT";
|
|
29
29
|
/** Set only by release Step-5 preflight when --allow-coverage-debt=#N is supplied (#2573). */
|
|
30
30
|
export const COVERAGE_DEBT_ENV = "DEFT_ALLOW_COVERAGE_DEBT";
|
|
31
|
+
/** Hard wall-clock cap for release Step 5 `task check` / vitest coverage (#2652). */
|
|
32
|
+
export const RELEASE_CHECK_TIMEOUT_MS = 20 * 60 * 1000;
|
|
33
|
+
export const RELEASE_CHECK_TIMEOUT_MINUTES = 20;
|
|
34
|
+
/** Vitest coverage step cap in GHA CI (mirrors release Step 5 budget, #2652). */
|
|
35
|
+
export const CI_VITEST_COVERAGE_TIMEOUT_MINUTES = 20;
|
|
31
36
|
export const PYPROJECT_VERSION_LINE_RE = /version\s*=\s*"[^"]*"/;
|
|
32
37
|
/** Byte-identical argparse --help from scripts/release.py (Python 3.12). */
|
|
33
38
|
export const RELEASE_HELP = "usage: release [-h] [--dry-run] [--skip-tag] [--skip-release] [--allow-dirty]\n" +
|
|
34
39
|
" [--allow-vbrief-drift] [--allow-coverage-debt #N]\n" +
|
|
40
|
+
" [--allow-skip-ci #N]\n" +
|
|
35
41
|
" [--skip-ci] [--skip-build] [--no-draft]\n" +
|
|
36
42
|
" [--repo OWNER/REPO] [--base-branch BRANCH]\n" +
|
|
37
43
|
" [--project-root PATH] [--summary TEXT]\n" +
|
|
@@ -66,11 +72,17 @@ export const RELEASE_HELP = "usage: release [-h] [--dry-run] [--skip-tag] [--ski
|
|
|
66
72
|
" flag cites an operator-owned issue number (#2573).\n" +
|
|
67
73
|
" PowerShell: use --allow-coverage-debt=N (no bare #)\n" +
|
|
68
74
|
' or quote "#N"; unquoted # starts a comment (#2621).\n' +
|
|
69
|
-
" --skip-ci Skip Step
|
|
75
|
+
" --skip-ci Skip Step 5 (task ci:local / task check fallback).\n" +
|
|
70
76
|
" Used by `task release:e2e` to keep wall-clock\n" +
|
|
71
77
|
" manageable inside the auto-created temp repo (CI\n" +
|
|
72
78
|
" semantics are covered by the unit-test suite, not the\n" +
|
|
73
|
-
" e2e rehearsal)
|
|
79
|
+
" e2e rehearsal). Production cuts MUST pass\n" +
|
|
80
|
+
" --allow-skip-ci=#N citing the tracked incident (#2652);\n" +
|
|
81
|
+
" otherwise Step 5 runs with a hard timeout.\n" +
|
|
82
|
+
" --allow-skip-ci #N Acknowledge skipping Step 5 on a production cut (#2652).\n" +
|
|
83
|
+
" Emits a loud WARN — npm ships without vitest coverage.\n" +
|
|
84
|
+
" PowerShell: use --allow-skip-ci=N (no bare #) or quote\n" +
|
|
85
|
+
' "#N"; unquoted # starts a comment (#2621).\n' +
|
|
74
86
|
" --skip-build Skip Step 6 (task build). Used by `task release:e2e`\n" +
|
|
75
87
|
" to keep wall-clock manageable; build artefacts are not\n" +
|
|
76
88
|
" needed for the draft-release verification step.\n" +
|
package/dist/release/flags.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { parseCoverageDebtIssueNumber } from "../vitest-runner/coverage-debt.js";
|
|
2
2
|
import { DEFAULT_BASE_BRANCH, RELEASE_HELP } from "./constants.js";
|
|
3
|
+
import { parseSkipCiIncidentArgv } from "./skip-ci-incident.js";
|
|
3
4
|
export function parseReleaseFlags(args) {
|
|
4
5
|
let help = false;
|
|
5
6
|
let dryRun = false;
|
|
@@ -8,6 +9,8 @@ export function parseReleaseFlags(args) {
|
|
|
8
9
|
let allowDirty = false;
|
|
9
10
|
let allowVbriefDrift = false;
|
|
10
11
|
let allowCoverageDebtIssue = null;
|
|
12
|
+
const skipCiIncident = parseSkipCiIncidentArgv(args);
|
|
13
|
+
const allowSkipCiIssue = skipCiIncident.kind === "valid" ? skipCiIncident.issue : null;
|
|
11
14
|
let skipCi = false;
|
|
12
15
|
let skipBuild = false;
|
|
13
16
|
let draft = true;
|
|
@@ -71,6 +74,15 @@ export function parseReleaseFlags(args) {
|
|
|
71
74
|
else if (token === "--skip-ci") {
|
|
72
75
|
skipCi = true;
|
|
73
76
|
}
|
|
77
|
+
else if (token === "--allow-skip-ci") {
|
|
78
|
+
// Value consumed by parseSkipCiIncidentArgv above; advance past it here.
|
|
79
|
+
const value = takeValue(token, i);
|
|
80
|
+
if (value !== null)
|
|
81
|
+
i += 1;
|
|
82
|
+
}
|
|
83
|
+
else if (token.startsWith("--allow-skip-ci=")) {
|
|
84
|
+
// Value consumed by parseSkipCiIncidentArgv above.
|
|
85
|
+
}
|
|
74
86
|
else if (token === "--skip-build") {
|
|
75
87
|
skipBuild = true;
|
|
76
88
|
}
|
|
@@ -132,6 +144,9 @@ export function parseReleaseFlags(args) {
|
|
|
132
144
|
}
|
|
133
145
|
i += 1;
|
|
134
146
|
}
|
|
147
|
+
if (skipCiIncident.kind === "invalid") {
|
|
148
|
+
unknown.push(`--allow-skip-ci (${skipCiIncident.reason})`);
|
|
149
|
+
}
|
|
135
150
|
return {
|
|
136
151
|
help,
|
|
137
152
|
version,
|
|
@@ -144,6 +159,7 @@ export function parseReleaseFlags(args) {
|
|
|
144
159
|
allowDirty,
|
|
145
160
|
allowVbriefDrift,
|
|
146
161
|
allowCoverageDebtIssue,
|
|
162
|
+
allowSkipCiIssue,
|
|
147
163
|
skipCi,
|
|
148
164
|
skipBuild,
|
|
149
165
|
draft,
|