@quikit/quikpilot-runner 7.0.6 → 8.1.0
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.js +437 -73
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -23,6 +23,54 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
23
23
|
mod
|
|
24
24
|
));
|
|
25
25
|
|
|
26
|
+
// src/env.ts
|
|
27
|
+
function required(name, value) {
|
|
28
|
+
if (!value) {
|
|
29
|
+
throw new Error(`Missing required environment variable: ${name}`);
|
|
30
|
+
}
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
function readRunnerEnv(env2 = process.env) {
|
|
34
|
+
const githubRunId = required("GITHUB_RUN_ID", env2.GITHUB_RUN_ID);
|
|
35
|
+
const githubServerUrl = env2.GITHUB_SERVER_URL ?? "https://github.com";
|
|
36
|
+
const githubRepository = required("GITHUB_REPOSITORY", env2.GITHUB_REPOSITORY);
|
|
37
|
+
if (env2.MODE === "readiness_check") {
|
|
38
|
+
return {
|
|
39
|
+
mode: "readiness_check",
|
|
40
|
+
readinessToken: required("READINESS_TOKEN", env2.READINESS_TOKEN),
|
|
41
|
+
readinessCallbackUrl: required("READINESS_CALLBACK_URL", env2.READINESS_CALLBACK_URL),
|
|
42
|
+
setupCmd: env2.SETUP_CMD ?? "",
|
|
43
|
+
testCmd: required("TEST_CMD", env2.TEST_CMD),
|
|
44
|
+
githubRunId,
|
|
45
|
+
githubServerUrl,
|
|
46
|
+
githubRepository
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
mode: "run",
|
|
51
|
+
runId: required("RUN_ID", env2.RUN_ID),
|
|
52
|
+
callbackUrl: required("CALLBACK_URL", env2.CALLBACK_URL),
|
|
53
|
+
packetUrl: required("PACKET_URL", env2.PACKET_URL),
|
|
54
|
+
tokenUrl: required("TOKEN_URL", env2.TOKEN_URL),
|
|
55
|
+
installationTokenUrl: required("INSTALLATION_TOKEN_URL", env2.INSTALLATION_TOKEN_URL),
|
|
56
|
+
githubRunId,
|
|
57
|
+
githubServerUrl,
|
|
58
|
+
githubRepository
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// src/quiktrackError.ts
|
|
63
|
+
async function describeQuikTrackFailure(res) {
|
|
64
|
+
try {
|
|
65
|
+
const body = await res.json();
|
|
66
|
+
if (body && typeof body === "object" && typeof body.reason_code === "string") {
|
|
67
|
+
return `${res.status} ${res.statusText} (${body.reason_code})`;
|
|
68
|
+
}
|
|
69
|
+
} catch {
|
|
70
|
+
}
|
|
71
|
+
return `${res.status} ${res.statusText}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
26
74
|
// src/callback.ts
|
|
27
75
|
async function reportBack(params) {
|
|
28
76
|
const { callbackUrl, runToken, status, branch, warnings } = params;
|
|
@@ -35,7 +83,7 @@ async function reportBack(params) {
|
|
|
35
83
|
body: JSON.stringify(body)
|
|
36
84
|
});
|
|
37
85
|
if (!res.ok) {
|
|
38
|
-
throw new Error(`Report back to QuikTrack failed: ${
|
|
86
|
+
throw new Error(`Report back to QuikTrack failed: ${await describeQuikTrackFailure(res)}`);
|
|
39
87
|
}
|
|
40
88
|
}
|
|
41
89
|
|
|
@@ -155,6 +203,14 @@ var import_node_fs = require("node:fs");
|
|
|
155
203
|
var import_node_path = require("node:path");
|
|
156
204
|
|
|
157
205
|
// src/envelope.ts
|
|
206
|
+
var HARD_FAILURE_CODES = /* @__PURE__ */ new Set(["SEMANTIC_MISMATCH", "CONTRACT_REJECTED"]);
|
|
207
|
+
function checkHardFailure(envelope, useCase) {
|
|
208
|
+
const code = envelope?.errorCode;
|
|
209
|
+
if (typeof code === "string" && HARD_FAILURE_CODES.has(code)) {
|
|
210
|
+
const detail = envelope.error ?? envelope.normalizedText ?? "no detail";
|
|
211
|
+
throw new Error(`${useCase} failed (${code}, not retried): ${detail}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
158
214
|
function unwrapExecute(envelope, useCase) {
|
|
159
215
|
if (envelope?.success === false) {
|
|
160
216
|
throw new Error(
|
|
@@ -167,11 +223,97 @@ function unwrapExecute(envelope, useCase) {
|
|
|
167
223
|
return envelope.structuredJson;
|
|
168
224
|
}
|
|
169
225
|
|
|
226
|
+
// src/retryFetch.ts
|
|
227
|
+
var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([502, 503, 504]);
|
|
228
|
+
var RETRYABLE_NETWORK_ERROR_CODES = /* @__PURE__ */ new Set(["ECONNRESET", "ETIMEDOUT"]);
|
|
229
|
+
function isRetryableNetworkError(err) {
|
|
230
|
+
const code = err?.cause?.code ?? err?.code;
|
|
231
|
+
return typeof code === "string" && RETRYABLE_NETWORK_ERROR_CODES.has(code);
|
|
232
|
+
}
|
|
233
|
+
async function fetchWithRetry(url, init, { retries = 2, backoffMs = 250 } = {}) {
|
|
234
|
+
for (let attempt = 0; ; attempt++) {
|
|
235
|
+
try {
|
|
236
|
+
const res = await fetch(url, init);
|
|
237
|
+
if (RETRYABLE_STATUS_CODES.has(res.status) && attempt < retries) {
|
|
238
|
+
await new Promise((r) => setTimeout(r, backoffMs));
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
return res;
|
|
242
|
+
} catch (err) {
|
|
243
|
+
if (isRetryableNetworkError(err) && attempt < retries) {
|
|
244
|
+
await new Promise((r) => setTimeout(r, backoffMs));
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
throw err;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
170
252
|
// src/codegen.ts
|
|
171
253
|
var FILE_TOO_LARGE_CODE = "CODEGEN_FILE_TOO_LARGE";
|
|
172
254
|
var CONTEXT_TOO_LARGE_CODE = "CODEGEN_CONTEXT_TOO_LARGE";
|
|
173
|
-
var
|
|
174
|
-
|
|
255
|
+
var OUTPUT_TOO_LARGE_CODE = "CODEGEN_OUTPUT_TOO_LARGE";
|
|
256
|
+
var NOT_RETRIED_CODES = /* @__PURE__ */ new Set([FILE_TOO_LARGE_CODE, CONTEXT_TOO_LARGE_CODE, OUTPUT_TOO_LARGE_CODE]);
|
|
257
|
+
var SUSPECT_PLACEHOLDER_RE = /rest(?: of the)? (?:file|code) unchanged|remainder unchanged|\/\/\s*\.\.\.\s*(?:rest|remaining)/i;
|
|
258
|
+
var SUSPECT_LENGTH_DROP_RATIO = 0.6;
|
|
259
|
+
var SUSPECT_REMOVED_LINE_RATIO = 0.3;
|
|
260
|
+
var MIN_LINES_FOR_REMOVED_LINE_CHECK = 10;
|
|
261
|
+
var JS_TS_EXTENSION_RE = /\.(?:ts|tsx|js|jsx|mjs|cjs)$/;
|
|
262
|
+
var EXPORTED_SYMBOL_RE = /^export\s+(?:default\s+)?(?:async\s+)?(?:function\*?|class|const|let|var|interface|type|enum)\s+([A-Za-z_$][\w$]*)/gm;
|
|
263
|
+
function countRemovedLines(currentContent, newContent) {
|
|
264
|
+
const remaining = /* @__PURE__ */ new Map();
|
|
265
|
+
for (const line of newContent.split("\n")) {
|
|
266
|
+
remaining.set(line, (remaining.get(line) ?? 0) + 1);
|
|
267
|
+
}
|
|
268
|
+
let removed = 0;
|
|
269
|
+
for (const line of currentContent.split("\n")) {
|
|
270
|
+
const count = remaining.get(line) ?? 0;
|
|
271
|
+
if (count > 0) {
|
|
272
|
+
remaining.set(line, count - 1);
|
|
273
|
+
} else {
|
|
274
|
+
removed++;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return removed;
|
|
278
|
+
}
|
|
279
|
+
function extractExportedSymbols(content) {
|
|
280
|
+
const symbols = /* @__PURE__ */ new Set();
|
|
281
|
+
for (const match of content.matchAll(EXPORTED_SYMBOL_RE)) {
|
|
282
|
+
symbols.add(match[1]);
|
|
283
|
+
}
|
|
284
|
+
return symbols;
|
|
285
|
+
}
|
|
286
|
+
function removedUnexplainedSymbols(targetPath, planText, currentContent, newContent) {
|
|
287
|
+
if (!JS_TS_EXTENSION_RE.test(targetPath)) return [];
|
|
288
|
+
const before = extractExportedSymbols(currentContent);
|
|
289
|
+
const after = extractExportedSymbols(newContent);
|
|
290
|
+
const lowerPlan = planText.toLowerCase();
|
|
291
|
+
return [...before].filter((symbol) => !after.has(symbol) && !lowerPlan.includes(symbol.toLowerCase()));
|
|
292
|
+
}
|
|
293
|
+
function looksLikeTruncatedRewrite(target, currentContent, newContent) {
|
|
294
|
+
if (SUSPECT_PLACEHOLDER_RE.test(newContent)) {
|
|
295
|
+
return "contains placeholder text suggesting an abbreviated rewrite";
|
|
296
|
+
}
|
|
297
|
+
if (currentContent.length > 0 && newContent.length < currentContent.length * SUSPECT_LENGTH_DROP_RATIO) {
|
|
298
|
+
return `sharp length drop (${currentContent.length} -> ${newContent.length} chars)`;
|
|
299
|
+
}
|
|
300
|
+
const currentLineCount = currentContent.length > 0 ? currentContent.split("\n").length : 0;
|
|
301
|
+
if (currentLineCount >= MIN_LINES_FOR_REMOVED_LINE_CHECK) {
|
|
302
|
+
const removedLines = countRemovedLines(currentContent, newContent);
|
|
303
|
+
if (removedLines > currentLineCount * SUSPECT_REMOVED_LINE_RATIO) {
|
|
304
|
+
const pct = (removedLines / currentLineCount * 100).toFixed(0);
|
|
305
|
+
return `removed ${removedLines} of ${currentLineCount} lines (${pct}%) without a matching drop in character count`;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
const planText = target.planStepsImplemented.join(" ");
|
|
309
|
+
const unexplainedSymbols = removedUnexplainedSymbols(target.path, planText, currentContent, newContent);
|
|
310
|
+
if (unexplainedSymbols.length > 0) {
|
|
311
|
+
return `removed exported symbol(s) the plan steps don't mention: ${unexplainedSymbols.join(", ")}`;
|
|
312
|
+
}
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
var CONTRACT_VERSION = "test-targets-1";
|
|
316
|
+
async function runCodegen(gateway, targets, workspaceListing, requiredTests, deniedPaths) {
|
|
175
317
|
const results = [];
|
|
176
318
|
const listing = new Set(workspaceListing);
|
|
177
319
|
for (const target of targets) {
|
|
@@ -191,12 +333,21 @@ async function runCodegen(gateway, targets, workspaceListing) {
|
|
|
191
333
|
currentContent,
|
|
192
334
|
planSteps: target.planStepsImplemented,
|
|
193
335
|
kind: target.kind,
|
|
194
|
-
contextContents
|
|
336
|
+
contextContents,
|
|
337
|
+
// QUIKTR-1083 (§15.6) — required on every call on the versioned path.
|
|
338
|
+
contractVersion: CONTRACT_VERSION
|
|
195
339
|
};
|
|
196
340
|
if (isNewFile) {
|
|
197
341
|
contextData.workspaceListing = workspaceListing;
|
|
198
342
|
}
|
|
199
|
-
|
|
343
|
+
if (deniedPaths.length > 0) {
|
|
344
|
+
contextData.deniedPaths = deniedPaths;
|
|
345
|
+
}
|
|
346
|
+
if (target.automationIds && target.automationIds.length > 0) {
|
|
347
|
+
const wanted = new Set(target.automationIds);
|
|
348
|
+
contextData.requiredTests = requiredTests.filter((tc) => wanted.has(tc.automationId));
|
|
349
|
+
}
|
|
350
|
+
const res = await fetchWithRetry(`${gateway.baseUrl}/ai/execute`, {
|
|
200
351
|
method: "POST",
|
|
201
352
|
headers: { "content-type": "application/json", "x-run-token": gateway.token },
|
|
202
353
|
body: JSON.stringify({
|
|
@@ -211,6 +362,7 @@ async function runCodegen(gateway, targets, workspaceListing) {
|
|
|
211
362
|
throw new Error(`Codegen call failed for ${target.path}: ${res.status} ${res.statusText}`);
|
|
212
363
|
}
|
|
213
364
|
const envelope = await res.json();
|
|
365
|
+
checkHardFailure(envelope, "pilot.codegen");
|
|
214
366
|
if (typeof envelope?.errorCode === "string" && NOT_RETRIED_CODES.has(envelope.errorCode)) {
|
|
215
367
|
const detail = envelope.error ? `: ${envelope.error}` : "";
|
|
216
368
|
throw new Error(`Codegen refused ${target.path}: ${envelope.errorCode} (not retried)${detail}`);
|
|
@@ -221,6 +373,24 @@ async function runCodegen(gateway, targets, workspaceListing) {
|
|
|
221
373
|
`Codegen refused ${target.path}${body.refusalReason ? `: ${body.refusalReason}` : ""}`
|
|
222
374
|
);
|
|
223
375
|
}
|
|
376
|
+
if (!isNewFile) {
|
|
377
|
+
const suspectReason = looksLikeTruncatedRewrite(target, currentContent, body.newContent);
|
|
378
|
+
if (suspectReason) {
|
|
379
|
+
throw new Error(
|
|
380
|
+
`Codegen output for ${target.path} looks like a truncated/abbreviated rewrite (${suspectReason}) \u2014 refusing to apply it. Interim guard per \xA716.7; the AI Runtime's own output-cap check is not live yet.`
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
console.log(
|
|
385
|
+
JSON.stringify({
|
|
386
|
+
event: "codegen.metrics",
|
|
387
|
+
targetPath: target.path,
|
|
388
|
+
kind: target.kind,
|
|
389
|
+
inputLength: currentContent.length,
|
|
390
|
+
outputLength: body.newContent.length,
|
|
391
|
+
removedLines: isNewFile ? null : countRemovedLines(currentContent, body.newContent)
|
|
392
|
+
})
|
|
393
|
+
);
|
|
224
394
|
results.push({ targetPath: target.path, newContent: body.newContent });
|
|
225
395
|
}
|
|
226
396
|
return results;
|
|
@@ -414,26 +584,6 @@ function describeDependencyViolations(check) {
|
|
|
414
584
|
return check.violations.map((v) => `${v.file} imports ${v.packages.join(", ")}, not a dependency of this repository`).join("; ");
|
|
415
585
|
}
|
|
416
586
|
|
|
417
|
-
// src/env.ts
|
|
418
|
-
function required(name, value) {
|
|
419
|
-
if (!value) {
|
|
420
|
-
throw new Error(`Missing required environment variable: ${name}`);
|
|
421
|
-
}
|
|
422
|
-
return value;
|
|
423
|
-
}
|
|
424
|
-
function readRunnerEnv(env = process.env) {
|
|
425
|
-
return {
|
|
426
|
-
runId: required("RUN_ID", env.RUN_ID),
|
|
427
|
-
callbackUrl: required("CALLBACK_URL", env.CALLBACK_URL),
|
|
428
|
-
packetUrl: required("PACKET_URL", env.PACKET_URL),
|
|
429
|
-
tokenUrl: required("TOKEN_URL", env.TOKEN_URL),
|
|
430
|
-
installationTokenUrl: required("INSTALLATION_TOKEN_URL", env.INSTALLATION_TOKEN_URL),
|
|
431
|
-
githubRunId: required("GITHUB_RUN_ID", env.GITHUB_RUN_ID),
|
|
432
|
-
githubServerUrl: env.GITHUB_SERVER_URL ?? "https://github.com",
|
|
433
|
-
githubRepository: required("GITHUB_REPOSITORY", env.GITHUB_REPOSITORY)
|
|
434
|
-
};
|
|
435
|
-
}
|
|
436
|
-
|
|
437
587
|
// src/guardrail.ts
|
|
438
588
|
function checkGuardrail(deniedPaths) {
|
|
439
589
|
addAll();
|
|
@@ -451,7 +601,7 @@ async function exchangeForInstallationToken(installationTokenUrl, runToken) {
|
|
|
451
601
|
headers: { Authorization: `Bearer ${runToken}` }
|
|
452
602
|
});
|
|
453
603
|
if (!res.ok) {
|
|
454
|
-
throw new Error(`Exchange installation token failed: ${
|
|
604
|
+
throw new Error(`Exchange installation token failed: ${await describeQuikTrackFailure(res)}`);
|
|
455
605
|
}
|
|
456
606
|
const body = await res.json();
|
|
457
607
|
if (body.success !== true) {
|
|
@@ -462,14 +612,14 @@ async function exchangeForInstallationToken(installationTokenUrl, runToken) {
|
|
|
462
612
|
|
|
463
613
|
// src/oidc.ts
|
|
464
614
|
var QUIKPILOT_OIDC_AUDIENCE = "quiktrack-quikpilot";
|
|
465
|
-
async function fetchGithubOidcToken(
|
|
615
|
+
async function fetchGithubOidcToken(env2 = process.env) {
|
|
466
616
|
const requestUrl = required(
|
|
467
617
|
"ACTIONS_ID_TOKEN_REQUEST_URL",
|
|
468
|
-
|
|
618
|
+
env2.ACTIONS_ID_TOKEN_REQUEST_URL
|
|
469
619
|
);
|
|
470
620
|
const requestToken = required(
|
|
471
621
|
"ACTIONS_ID_TOKEN_REQUEST_TOKEN",
|
|
472
|
-
|
|
622
|
+
env2.ACTIONS_ID_TOKEN_REQUEST_TOKEN
|
|
473
623
|
);
|
|
474
624
|
const separator = requestUrl.includes("?") ? "&" : "?";
|
|
475
625
|
const url = `${requestUrl}${separator}audience=${encodeURIComponent(QUIKPILOT_OIDC_AUDIENCE)}`;
|
|
@@ -487,6 +637,10 @@ async function fetchGithubOidcToken(env = process.env) {
|
|
|
487
637
|
}
|
|
488
638
|
|
|
489
639
|
// src/overlapCheck.ts
|
|
640
|
+
var TEST_PATH_RE = /(\.(test|spec)\.[jt]sx?$)|(^|\/)(__tests__|tests?)\//i;
|
|
641
|
+
function isTestTarget(path) {
|
|
642
|
+
return TEST_PATH_RE.test(path);
|
|
643
|
+
}
|
|
490
644
|
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
491
645
|
"a",
|
|
492
646
|
"an",
|
|
@@ -548,6 +702,9 @@ function jaccard(a, b) {
|
|
|
548
702
|
const union = a.size + b.size - intersection;
|
|
549
703
|
return intersection / union;
|
|
550
704
|
}
|
|
705
|
+
function normalizedStep(step) {
|
|
706
|
+
return step.trim().toLowerCase().replace(/\s+/g, " ");
|
|
707
|
+
}
|
|
551
708
|
var OVERLAP_THRESHOLD = 0.5;
|
|
552
709
|
function findOverlappingSteps(targets) {
|
|
553
710
|
const items = targets.flatMap(
|
|
@@ -561,12 +718,14 @@ function findOverlappingSteps(targets) {
|
|
|
561
718
|
for (let i = 0; i < items.length; i++) {
|
|
562
719
|
for (let j = i + 1; j < items.length; j++) {
|
|
563
720
|
if (items[i].target === items[j].target) continue;
|
|
721
|
+
if (isTestTarget(items[i].target) || isTestTarget(items[j].target)) continue;
|
|
564
722
|
if (jaccard(items[i].words, items[j].words) >= OVERLAP_THRESHOLD) {
|
|
565
723
|
warnings.push({
|
|
566
724
|
targetA: items[i].target,
|
|
567
725
|
stepA: items[i].step,
|
|
568
726
|
targetB: items[j].target,
|
|
569
|
-
stepB: items[j].step
|
|
727
|
+
stepB: items[j].step,
|
|
728
|
+
severity: normalizedStep(items[i].step) === normalizedStep(items[j].step) ? "identical" : "similar"
|
|
570
729
|
});
|
|
571
730
|
}
|
|
572
731
|
}
|
|
@@ -586,7 +745,7 @@ async function fetchPacket(packetUrl, runToken, githubRunId) {
|
|
|
586
745
|
headers: { Authorization: `Bearer ${runToken}` }
|
|
587
746
|
});
|
|
588
747
|
if (!res.ok) {
|
|
589
|
-
throw new Error(`Fetch work packet failed: ${
|
|
748
|
+
throw new Error(`Fetch work packet failed: ${await describeQuikTrackFailure(res)}`);
|
|
590
749
|
}
|
|
591
750
|
const body = await res.json();
|
|
592
751
|
if (body.success !== true) {
|
|
@@ -662,9 +821,9 @@ async function reportToQuikTest(params) {
|
|
|
662
821
|
try {
|
|
663
822
|
console.log(`reporting ${report}`);
|
|
664
823
|
const ciUrl = buildCiUrl(githubServerUrl, githubRepository, githubRunId);
|
|
665
|
-
const { readFileSync:
|
|
666
|
-
const body =
|
|
667
|
-
const url = `${resultsUrl}?ciUrl=${encodeURIComponent(ciUrl)}`;
|
|
824
|
+
const { readFileSync: readFileSync5 } = await import("node:fs");
|
|
825
|
+
const body = readFileSync5(report);
|
|
826
|
+
const url = `${resultsUrl}?ciUrl=${encodeURIComponent(ciUrl)}&automationIdFormat=bracket`;
|
|
668
827
|
const res = await fetch(url, {
|
|
669
828
|
method: "POST",
|
|
670
829
|
headers: { Authorization: `Bearer ${runToken}`, "Content-Type": "text/xml" },
|
|
@@ -685,8 +844,24 @@ var TESTCASE_RE = /<testcase\b([^>]*?)(?:\/>|>([\s\S]*?)<\/testcase>)/g;
|
|
|
685
844
|
var NAME_ATTR_RE = /\bname="([^"]*)"/;
|
|
686
845
|
var CLASSNAME_ATTR_RE = /\bclassname="([^"]*)"/;
|
|
687
846
|
var FAILURE_CHILD_RE = /<failure\b|<error\b/;
|
|
847
|
+
var FAILURE_TAG_RE = /<(?:failure|error)\b([^>]*?)(?:\/>|>([\s\S]*?)<\/(?:failure|error)>)/;
|
|
848
|
+
var FAILURE_MESSAGE_ATTR_RE = /\bmessage="([^"]*)"/;
|
|
849
|
+
var MAX_FAILURE_TEXT = 500;
|
|
850
|
+
function unescapeXmlEntities(s) {
|
|
851
|
+
return s.replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, "&");
|
|
852
|
+
}
|
|
853
|
+
function extractFailureText(testcaseBody) {
|
|
854
|
+
const tag = FAILURE_TAG_RE.exec(testcaseBody);
|
|
855
|
+
if (!tag) return null;
|
|
856
|
+
const [, attrs = "", inner = ""] = tag;
|
|
857
|
+
const raw = (FAILURE_MESSAGE_ATTR_RE.exec(attrs)?.[1] ?? inner).trim();
|
|
858
|
+
if (!raw) return null;
|
|
859
|
+
const text = unescapeXmlEntities(raw);
|
|
860
|
+
return text.length > MAX_FAILURE_TEXT ? `${text.slice(0, MAX_FAILURE_TEXT)}\u2026` : text;
|
|
861
|
+
}
|
|
688
862
|
function parseJUnitOutcomes(xml) {
|
|
689
|
-
const
|
|
863
|
+
const outcomes = /* @__PURE__ */ new Map();
|
|
864
|
+
const failureText = /* @__PURE__ */ new Map();
|
|
690
865
|
let match;
|
|
691
866
|
TESTCASE_RE.lastIndex = 0;
|
|
692
867
|
while (match = TESTCASE_RE.exec(xml)) {
|
|
@@ -695,9 +870,14 @@ function parseJUnitOutcomes(xml) {
|
|
|
695
870
|
if (!name) continue;
|
|
696
871
|
const classname = CLASSNAME_ATTR_RE.exec(attrs)?.[1];
|
|
697
872
|
const id = classname ? `${classname}::${name}` : name;
|
|
698
|
-
|
|
873
|
+
const failed = FAILURE_CHILD_RE.test(body);
|
|
874
|
+
outcomes.set(id, !failed);
|
|
875
|
+
if (failed) {
|
|
876
|
+
const text = extractFailureText(body);
|
|
877
|
+
if (text) failureText.set(id, text);
|
|
878
|
+
}
|
|
699
879
|
}
|
|
700
|
-
return
|
|
880
|
+
return { outcomes, failureText };
|
|
701
881
|
}
|
|
702
882
|
function readJUnitOutcomes(cwd) {
|
|
703
883
|
const report = findJUnitReport(cwd);
|
|
@@ -710,35 +890,55 @@ function readJUnitOutcomes(cwd) {
|
|
|
710
890
|
}
|
|
711
891
|
function captureTestSnapshot(command, cwd = process.cwd()) {
|
|
712
892
|
const outcome = runTestCommand(command);
|
|
713
|
-
|
|
893
|
+
const parsed = readJUnitOutcomes(cwd);
|
|
894
|
+
return { outcome, results: parsed?.outcomes ?? null, failureText: parsed?.failureText };
|
|
714
895
|
}
|
|
715
896
|
async function captureBaselineSnapshot(command, cwd = process.cwd()) {
|
|
716
897
|
const outcome = await runTestCommandAsync(command);
|
|
717
|
-
|
|
898
|
+
const parsed = readJUnitOutcomes(cwd);
|
|
899
|
+
return { outcome, results: parsed?.outcomes ?? null, failureText: parsed?.failureText };
|
|
718
900
|
}
|
|
719
901
|
function checkForRegressions(baseline, after) {
|
|
720
902
|
if (!baseline.results || !after.results) {
|
|
721
|
-
return { blocked: false, regressions: [], newFailures: [], preExisting: [] };
|
|
903
|
+
return { blocked: false, regressions: [], newFailures: [], preExisting: [], failureText: /* @__PURE__ */ new Map() };
|
|
722
904
|
}
|
|
723
905
|
const regressions = [];
|
|
724
906
|
const newFailures = [];
|
|
725
907
|
const preExisting = [];
|
|
908
|
+
const failureText = /* @__PURE__ */ new Map();
|
|
909
|
+
const afterFailureText = after.failureText;
|
|
726
910
|
for (const [testId, passed] of after.results) {
|
|
727
911
|
if (passed) continue;
|
|
728
912
|
const before = baseline.results.get(testId);
|
|
729
|
-
if (before === true)
|
|
730
|
-
|
|
731
|
-
|
|
913
|
+
if (before === true) {
|
|
914
|
+
regressions.push(testId);
|
|
915
|
+
const text = afterFailureText?.get(testId);
|
|
916
|
+
if (text) failureText.set(testId, text);
|
|
917
|
+
} else if (before === false) {
|
|
918
|
+
preExisting.push(testId);
|
|
919
|
+
} else {
|
|
920
|
+
newFailures.push(testId);
|
|
921
|
+
const text = afterFailureText?.get(testId);
|
|
922
|
+
if (text) failureText.set(testId, text);
|
|
923
|
+
}
|
|
732
924
|
}
|
|
733
|
-
return { blocked: regressions.length > 0 || newFailures.length > 0, regressions, newFailures, preExisting };
|
|
925
|
+
return { blocked: regressions.length > 0 || newFailures.length > 0, regressions, newFailures, preExisting, failureText };
|
|
734
926
|
}
|
|
735
927
|
function describeRegressions(check) {
|
|
928
|
+
const withText = (id) => {
|
|
929
|
+
const text = check.failureText.get(id);
|
|
930
|
+
return text ? `${id} (${text})` : id;
|
|
931
|
+
};
|
|
736
932
|
const parts = [];
|
|
737
933
|
if (check.regressions.length > 0) {
|
|
738
|
-
parts.push(
|
|
934
|
+
parts.push(
|
|
935
|
+
`${check.regressions.length} regression(s) (passed before, now fail): ${check.regressions.map(withText).join(", ")}`
|
|
936
|
+
);
|
|
739
937
|
}
|
|
740
938
|
if (check.newFailures.length > 0) {
|
|
741
|
-
parts.push(
|
|
939
|
+
parts.push(
|
|
940
|
+
`${check.newFailures.length} new failure(s) (did not exist before): ${check.newFailures.map(withText).join(", ")}`
|
|
941
|
+
);
|
|
742
942
|
}
|
|
743
943
|
if (check.preExisting.length > 0) {
|
|
744
944
|
parts.push(`${check.preExisting.length} pre-existing failure(s), unrelated to this change, not blocking`);
|
|
@@ -746,12 +946,99 @@ function describeRegressions(check) {
|
|
|
746
946
|
return parts.join("; ");
|
|
747
947
|
}
|
|
748
948
|
|
|
949
|
+
// src/scopeCheck.ts
|
|
950
|
+
var import_node_fs6 = require("node:fs");
|
|
951
|
+
var ROUTE_METHOD_RE = /\b(GET|POST|PUT|PATCH|DELETE)\b/i;
|
|
952
|
+
var ROUTE_PATH_RE = /\/[A-Za-z0-9_\-{}[\]]+(?:\/[A-Za-z0-9_\-:{}[\]]+)+/;
|
|
953
|
+
var ROUTE_EXPORT_RE = /\bexport\s+(?:async\s+)?function\s+(GET|POST|PUT|PATCH|DELETE)\s*\(/;
|
|
954
|
+
var NO_PATH = "(this file's route)";
|
|
955
|
+
function detectMentions(text) {
|
|
956
|
+
const found = [];
|
|
957
|
+
for (const line of text.split("\n")) {
|
|
958
|
+
const methodMatch = ROUTE_METHOD_RE.exec(line);
|
|
959
|
+
const pathMatch = ROUTE_PATH_RE.exec(line);
|
|
960
|
+
if (methodMatch && pathMatch) {
|
|
961
|
+
found.push({ method: methodMatch[1].toUpperCase(), path: pathMatch[0] });
|
|
962
|
+
}
|
|
963
|
+
const exportMatch = ROUTE_EXPORT_RE.exec(line);
|
|
964
|
+
if (exportMatch) {
|
|
965
|
+
found.push({ method: exportMatch[1].toUpperCase(), path: NO_PATH });
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
return found;
|
|
969
|
+
}
|
|
970
|
+
function normalizePath(path) {
|
|
971
|
+
return path.replace(/:[A-Za-z0-9_]+|\{[^}]+\}|\[[^\]]+\]/g, "*");
|
|
972
|
+
}
|
|
973
|
+
function surfaceCovers(surface, mention) {
|
|
974
|
+
if (!new RegExp(`\\b${mention.method}\\b`, "i").test(surface)) return false;
|
|
975
|
+
if (mention.path === NO_PATH) return true;
|
|
976
|
+
const declaredPath = normalizePath(surface);
|
|
977
|
+
const mentionPath = normalizePath(mention.path);
|
|
978
|
+
return declaredPath.includes(mentionPath) || mentionPath.includes(declaredPath);
|
|
979
|
+
}
|
|
980
|
+
function addedLines(before, after) {
|
|
981
|
+
const beforeLines = new Set(before.split("\n"));
|
|
982
|
+
return after.split("\n").filter((line) => !beforeLines.has(line)).join("\n");
|
|
983
|
+
}
|
|
984
|
+
function checkApiScope(results, targets, apiChanges) {
|
|
985
|
+
const targetsByPath = new Map(targets.map((t) => [t.path, t]));
|
|
986
|
+
const violations = [];
|
|
987
|
+
for (const { targetPath, newContent } of results) {
|
|
988
|
+
const target = targetsByPath.get(targetPath);
|
|
989
|
+
const isNewFile = target?.kind === "NEW_FILE";
|
|
990
|
+
let scanned;
|
|
991
|
+
if (isNewFile) {
|
|
992
|
+
scanned = newContent;
|
|
993
|
+
} else {
|
|
994
|
+
let currentContent;
|
|
995
|
+
try {
|
|
996
|
+
currentContent = (0, import_node_fs6.readFileSync)(targetPath, "utf8");
|
|
997
|
+
} catch {
|
|
998
|
+
currentContent = "";
|
|
999
|
+
}
|
|
1000
|
+
scanned = addedLines(currentContent, newContent);
|
|
1001
|
+
}
|
|
1002
|
+
for (const mention of detectMentions(scanned)) {
|
|
1003
|
+
if (!apiChanges.some((c) => surfaceCovers(c.surface, mention))) {
|
|
1004
|
+
violations.push({ file: targetPath, method: mention.method, path: mention.path });
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
return { blocked: violations.length > 0, violations };
|
|
1009
|
+
}
|
|
1010
|
+
function describeScopeViolations(check) {
|
|
1011
|
+
return check.violations.map((v) => `${v.file} adds ${v.method} ${v.path}, not declared in the design's API changes`).join("; ");
|
|
1012
|
+
}
|
|
1013
|
+
|
|
749
1014
|
// src/targeting.ts
|
|
750
1015
|
var import_node_path4 = require("node:path");
|
|
1016
|
+
|
|
1017
|
+
// src/deniedPaths.ts
|
|
1018
|
+
function globToRegExp(pattern) {
|
|
1019
|
+
const escaped = pattern.split("").map((c) => /[.+^${}()|[\]\\]/.test(c) ? `\\${c}` : c).join("");
|
|
1020
|
+
const withGlobs = escaped.replace(/\*\*\//g, " SLASH ").replace(/\*\*/g, " STAR ").replace(/\*/g, "[^/]*").replace(/ SLASH /g, "(?:.*/)?").replace(/ STAR /g, ".*");
|
|
1021
|
+
return new RegExp(`^${withGlobs}$`);
|
|
1022
|
+
}
|
|
1023
|
+
function matchesDeniedPath(path, patterns) {
|
|
1024
|
+
for (const pattern of patterns) {
|
|
1025
|
+
if (globToRegExp(pattern).test(path)) return pattern;
|
|
1026
|
+
}
|
|
1027
|
+
return null;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// src/targeting.ts
|
|
1031
|
+
var CONTRACT_VERSION2 = "test-targets-1";
|
|
751
1032
|
function listWorkspaceFiles(workspaceRoot) {
|
|
752
1033
|
return listFiles(workspaceRoot ?? ".");
|
|
753
1034
|
}
|
|
754
|
-
function validateTarget(target, workspaceRoot, workspaceListing) {
|
|
1035
|
+
function validateTarget(target, workspaceRoot, workspaceListing, deniedPaths) {
|
|
1036
|
+
const denied = matchesDeniedPath(target.path, deniedPaths);
|
|
1037
|
+
if (denied) {
|
|
1038
|
+
throw new Error(
|
|
1039
|
+
`pilot.targeting proposed a target matching a denied path (${denied}): ${target.path}`
|
|
1040
|
+
);
|
|
1041
|
+
}
|
|
755
1042
|
if (target.kind !== "EXISTING" && target.kind !== "NEW_FILE") {
|
|
756
1043
|
throw new Error(`pilot.targeting returned an unknown kind "${target.kind}" for ${target.path}`);
|
|
757
1044
|
}
|
|
@@ -771,8 +1058,22 @@ function validateTarget(target, workspaceRoot, workspaceListing) {
|
|
|
771
1058
|
);
|
|
772
1059
|
}
|
|
773
1060
|
}
|
|
774
|
-
async function runTargeting(gateway, design, workspaceRoot, workspaceListing, requiredTests) {
|
|
775
|
-
const
|
|
1061
|
+
async function runTargeting(gateway, design, workspaceRoot, workspaceListing, requiredTests, deniedPaths) {
|
|
1062
|
+
const contextData = {
|
|
1063
|
+
// NESTED, NOT FLATTENED — a second vocabulary for the design shape is exactly what
|
|
1064
|
+
// QUIKTR-1034 cost.
|
|
1065
|
+
workspaceListing,
|
|
1066
|
+
design,
|
|
1067
|
+
// QUIKTR-1065/1083 — requiredTests carried through so targeting can propose a test-file
|
|
1068
|
+
// target naming which cases it covers (§15.1's Case shape, unchanged).
|
|
1069
|
+
requiredTests,
|
|
1070
|
+
// QUIKTR-1083 (§15.6) — required on the versioned path.
|
|
1071
|
+
contractVersion: CONTRACT_VERSION2
|
|
1072
|
+
};
|
|
1073
|
+
if (deniedPaths.length > 0) {
|
|
1074
|
+
contextData.deniedPaths = deniedPaths;
|
|
1075
|
+
}
|
|
1076
|
+
const res = await fetchWithRetry(`${gateway.baseUrl}/ai/execute`, {
|
|
776
1077
|
method: "POST",
|
|
777
1078
|
headers: { "content-type": "application/json", "x-run-token": gateway.token },
|
|
778
1079
|
body: JSON.stringify({
|
|
@@ -780,18 +1081,14 @@ async function runTargeting(gateway, design, workspaceRoot, workspaceListing, re
|
|
|
780
1081
|
useCase: "pilot.targeting",
|
|
781
1082
|
expectedOutputType: "json",
|
|
782
1083
|
userPrompt: "",
|
|
783
|
-
|
|
784
|
-
// QUIKTR-1034 cost.
|
|
785
|
-
// QUIKTR-1065 — requiredTests carried through so targeting CAN propose a test-file
|
|
786
|
-
// target naming which cases it covers. The AI Runtime prompt doesn't read this yet
|
|
787
|
-
// (that half is theirs, not this package's) — sending it now is what makes it possible.
|
|
788
|
-
contextData: { workspaceListing, design, requiredTests }
|
|
1084
|
+
contextData
|
|
789
1085
|
})
|
|
790
1086
|
});
|
|
791
1087
|
if (!res.ok) {
|
|
792
1088
|
throw new Error(`Targeting call failed: ${res.status} ${res.statusText}`);
|
|
793
1089
|
}
|
|
794
1090
|
const envelope = await res.json();
|
|
1091
|
+
checkHardFailure(envelope, "pilot.targeting");
|
|
795
1092
|
const body = unwrapExecute(envelope, "pilot.targeting");
|
|
796
1093
|
if (body.outcome === "NO_TARGET") {
|
|
797
1094
|
return { outcome: "NO_TARGET", reason: body.noTargetReason ?? null };
|
|
@@ -801,9 +1098,13 @@ async function runTargeting(gateway, design, workspaceRoot, workspaceListing, re
|
|
|
801
1098
|
}
|
|
802
1099
|
const listing = new Set(workspaceListing);
|
|
803
1100
|
for (const target of body.targets) {
|
|
804
|
-
validateTarget(target, workspaceRoot, listing);
|
|
1101
|
+
validateTarget(target, workspaceRoot, listing, deniedPaths);
|
|
805
1102
|
}
|
|
806
|
-
return {
|
|
1103
|
+
return {
|
|
1104
|
+
outcome: "TARGETED",
|
|
1105
|
+
targets: body.targets,
|
|
1106
|
+
skippedCases: body.skippedCases ?? []
|
|
1107
|
+
};
|
|
807
1108
|
}
|
|
808
1109
|
|
|
809
1110
|
// src/tokenExchange.ts
|
|
@@ -813,7 +1114,7 @@ async function exchangeForRunToken(tokenUrl, oidcToken) {
|
|
|
813
1114
|
headers: { Authorization: `Bearer ${oidcToken}` }
|
|
814
1115
|
});
|
|
815
1116
|
if (!res.ok) {
|
|
816
|
-
throw new Error(`Exchange run token failed: ${
|
|
1117
|
+
throw new Error(`Exchange run token failed: ${await describeQuikTrackFailure(res)}`);
|
|
817
1118
|
}
|
|
818
1119
|
const body = await res.json();
|
|
819
1120
|
if (body.success !== true) {
|
|
@@ -823,7 +1124,7 @@ async function exchangeForRunToken(tokenUrl, oidcToken) {
|
|
|
823
1124
|
}
|
|
824
1125
|
|
|
825
1126
|
// src/run.ts
|
|
826
|
-
async function run(
|
|
1127
|
+
async function run(env2) {
|
|
827
1128
|
let hardFailure = false;
|
|
828
1129
|
let packet;
|
|
829
1130
|
let branch;
|
|
@@ -831,8 +1132,8 @@ async function run(env = readRunnerEnv()) {
|
|
|
831
1132
|
let warnings = [];
|
|
832
1133
|
let targeted = false;
|
|
833
1134
|
try {
|
|
834
|
-
runToken = await exchangeForRunToken(
|
|
835
|
-
packet = await fetchPacket(
|
|
1135
|
+
runToken = await exchangeForRunToken(env2.tokenUrl, await fetchGithubOidcToken());
|
|
1136
|
+
packet = await fetchPacket(env2.packetUrl, runToken, env2.githubRunId);
|
|
836
1137
|
checkoutTargetBranch(packet.targetBranch);
|
|
837
1138
|
const workspaceListing = listWorkspaceFiles(packet.workspaceRoot);
|
|
838
1139
|
const targeting = await runTargeting(
|
|
@@ -840,26 +1141,47 @@ async function run(env = readRunnerEnv()) {
|
|
|
840
1141
|
packet.design,
|
|
841
1142
|
packet.workspaceRoot,
|
|
842
1143
|
workspaceListing,
|
|
843
|
-
packet.requiredTests
|
|
1144
|
+
packet.requiredTests,
|
|
1145
|
+
packet.guardrails.deniedPaths
|
|
844
1146
|
);
|
|
845
1147
|
if (targeting.outcome === "NO_TARGET") {
|
|
846
|
-
writePhase0Marker(
|
|
1148
|
+
writePhase0Marker(env2.runId);
|
|
847
1149
|
runCommand(packet.commands.setup, "Setup");
|
|
848
1150
|
runCommand(packet.commands.lint, "Lint");
|
|
849
1151
|
runTestCommand(packet.commands.test);
|
|
850
1152
|
} else {
|
|
851
1153
|
targeted = true;
|
|
1154
|
+
if (targeting.skippedCases?.length) {
|
|
1155
|
+
for (const skipped of targeting.skippedCases) {
|
|
1156
|
+
const w = `Test case ${skipped.automationId} has no given/when/then \u2014 skipped, no test was written for it.`;
|
|
1157
|
+
warnings.push(w);
|
|
1158
|
+
console.warn(`QuikPilot targeting warning (not blocking): ${w}`);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
852
1161
|
const overlaps = findOverlappingSteps(targeting.targets);
|
|
853
|
-
|
|
854
|
-
|
|
1162
|
+
const blockingOverlaps = overlaps.filter((o) => o.severity === "identical");
|
|
1163
|
+
const softOverlaps = overlaps.filter((o) => o.severity === "similar");
|
|
1164
|
+
if (softOverlaps.length > 0) {
|
|
1165
|
+
warnings = describeOverlapWarnings(softOverlaps);
|
|
855
1166
|
for (const w of warnings) {
|
|
856
1167
|
console.warn(`QuikPilot targeting warning (not blocking): ${w}`);
|
|
857
1168
|
}
|
|
858
1169
|
}
|
|
1170
|
+
if (blockingOverlaps.length > 0) {
|
|
1171
|
+
throw new Error(
|
|
1172
|
+
`QuikPilot's generated plan gives two files identical instructions \u2014 ${describeOverlapWarnings(blockingOverlaps).join("; ")}`
|
|
1173
|
+
);
|
|
1174
|
+
}
|
|
859
1175
|
runCommand(packet.commands.setup, "Setup");
|
|
860
1176
|
const [baseline, results] = await Promise.all([
|
|
861
1177
|
captureBaselineSnapshot(packet.commands.test),
|
|
862
|
-
runCodegen(
|
|
1178
|
+
runCodegen(
|
|
1179
|
+
packet.gateway,
|
|
1180
|
+
targeting.targets,
|
|
1181
|
+
workspaceListing,
|
|
1182
|
+
packet.requiredTests,
|
|
1183
|
+
packet.guardrails.deniedPaths
|
|
1184
|
+
)
|
|
863
1185
|
]);
|
|
864
1186
|
const depCheck = checkImportDependencies(
|
|
865
1187
|
results,
|
|
@@ -871,6 +1193,12 @@ async function run(env = readRunnerEnv()) {
|
|
|
871
1193
|
`QuikPilot's generated code imports packages this repository doesn't have \u2014 ${describeDependencyViolations(depCheck)}`
|
|
872
1194
|
);
|
|
873
1195
|
}
|
|
1196
|
+
const scopeCheck = checkApiScope(results, targeting.targets, packet.design?.apiChanges ?? []);
|
|
1197
|
+
if (scopeCheck.blocked) {
|
|
1198
|
+
throw new Error(
|
|
1199
|
+
`QuikPilot's generated code exceeds the approved design \u2014 ${describeScopeViolations(scopeCheck)}`
|
|
1200
|
+
);
|
|
1201
|
+
}
|
|
874
1202
|
flushCodegenResults(results);
|
|
875
1203
|
try {
|
|
876
1204
|
runCommand(packet.commands.format, "Format");
|
|
@@ -897,20 +1225,20 @@ async function run(env = readRunnerEnv()) {
|
|
|
897
1225
|
await reportToQuikTest({
|
|
898
1226
|
resultsUrl: packet?.quikTest?.resultsUrl,
|
|
899
1227
|
runToken,
|
|
900
|
-
githubServerUrl:
|
|
901
|
-
githubRepository:
|
|
902
|
-
githubRunId:
|
|
1228
|
+
githubServerUrl: env2.githubServerUrl,
|
|
1229
|
+
githubRepository: env2.githubRepository,
|
|
1230
|
+
githubRunId: env2.githubRunId
|
|
903
1231
|
});
|
|
904
1232
|
if (!hardFailure && packet) {
|
|
905
1233
|
try {
|
|
906
1234
|
checkGuardrail(packet.guardrails.deniedPaths);
|
|
907
1235
|
const installationToken = await exchangeForInstallationToken(
|
|
908
|
-
|
|
1236
|
+
env2.installationTokenUrl,
|
|
909
1237
|
runToken
|
|
910
1238
|
);
|
|
911
1239
|
const result = commitAndPush(
|
|
912
1240
|
packet.branch,
|
|
913
|
-
|
|
1241
|
+
env2.runId,
|
|
914
1242
|
installationToken,
|
|
915
1243
|
targeted,
|
|
916
1244
|
packet.issueKey,
|
|
@@ -923,7 +1251,7 @@ async function run(env = readRunnerEnv()) {
|
|
|
923
1251
|
}
|
|
924
1252
|
}
|
|
925
1253
|
await reportBack({
|
|
926
|
-
callbackUrl:
|
|
1254
|
+
callbackUrl: env2.callbackUrl,
|
|
927
1255
|
runToken,
|
|
928
1256
|
status: hardFailure ? "failed" : "completed",
|
|
929
1257
|
branch,
|
|
@@ -932,8 +1260,44 @@ async function run(env = readRunnerEnv()) {
|
|
|
932
1260
|
return { hardFailure };
|
|
933
1261
|
}
|
|
934
1262
|
|
|
1263
|
+
// src/readinessCheck.ts
|
|
1264
|
+
async function runReadinessCheck(env2) {
|
|
1265
|
+
let setupOk = true;
|
|
1266
|
+
try {
|
|
1267
|
+
runCommand(env2.setupCmd || null, "Setup");
|
|
1268
|
+
} catch (err) {
|
|
1269
|
+
setupOk = false;
|
|
1270
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
1271
|
+
}
|
|
1272
|
+
const testOutcome = runTestCommand(env2.testCmd);
|
|
1273
|
+
const testOk = testOutcome === "success";
|
|
1274
|
+
const junitPath = findJUnitReport();
|
|
1275
|
+
const result = {
|
|
1276
|
+
setupOk,
|
|
1277
|
+
testOk,
|
|
1278
|
+
junitFound: junitPath !== null,
|
|
1279
|
+
junitPath
|
|
1280
|
+
};
|
|
1281
|
+
try {
|
|
1282
|
+
const res = await fetch(env2.readinessCallbackUrl, {
|
|
1283
|
+
method: "POST",
|
|
1284
|
+
headers: { Authorization: `Bearer ${env2.readinessToken}`, "Content-Type": "application/json" },
|
|
1285
|
+
body: JSON.stringify(result)
|
|
1286
|
+
});
|
|
1287
|
+
if (!res.ok) {
|
|
1288
|
+
throw new Error(`Readiness callback POST failed: ${await describeQuikTrackFailure(res)}`);
|
|
1289
|
+
}
|
|
1290
|
+
} catch (err) {
|
|
1291
|
+
console.warn(
|
|
1292
|
+
`::warning::QuikPilot readiness check failed to report its result: ${err instanceof Error ? err.message : String(err)}`
|
|
1293
|
+
);
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
|
|
935
1297
|
// src/index.ts
|
|
936
|
-
|
|
1298
|
+
var env = readRunnerEnv();
|
|
1299
|
+
var task = env.mode === "readiness_check" ? runReadinessCheck(env).then(() => ({ hardFailure: false })) : run(env);
|
|
1300
|
+
task.then(({ hardFailure }) => {
|
|
937
1301
|
process.exitCode = hardFailure ? 1 : 0;
|
|
938
1302
|
}).catch((err) => {
|
|
939
1303
|
console.error(err instanceof Error ? err.message : String(err));
|