@quikit/quikpilot-runner 8.2.5 → 8.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -27
- package/dist/index.js +256 -18
- package/dist/index.js.map +4 -4
- package/package.json +26 -26
package/README.md
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
# @quikit/quikpilot-runner
|
|
2
|
-
|
|
3
|
-
QuikPilot's CI runner. Invoked automatically by the `.github/workflows/quikpilot.yml` file QuikPilot
|
|
4
|
-
installs in an onboarded repository — it fetches the run's work packet, runs your setup/lint/test
|
|
5
|
-
commands, reports results to QuikTest, enforces the denied-path guardrail, commits, pushes, and
|
|
6
|
-
reports back to QuikTrack.
|
|
7
|
-
|
|
8
|
-
This package is not meant to be installed or run by hand. It exists so a fix to QuikPilot's runner
|
|
9
|
-
logic ships as a new package version rather than a new pull request into every onboarded repository
|
|
10
|
-
— the workflow file itself lives in your repository (QuikIT cannot update it directly), so it stays a
|
|
11
|
-
thin, ~25-line launcher that invokes an exact, pinned version of this package via `npx`.
|
|
12
|
-
|
|
13
|
-
It carries no secrets and needs none of your credentials: it authenticates to QuikTrack and to GitHub
|
|
14
|
-
using short-lived tokens exchanged at runtime, never a value baked into the workflow file or this
|
|
15
|
-
package.
|
|
16
|
-
|
|
17
|
-
## Versioning and support
|
|
18
|
-
|
|
19
|
-
Every workflow file pins an **exact** version of this package (`npx --yes
|
|
20
|
-
@quikit/quikpilot-runner@<version>`, never a range or `latest`) — a repository never picks up a new
|
|
21
|
-
release on its own. Older published versions are not removed from npm and keep working as long as a
|
|
22
|
-
repository's workflow file keeps pinning them; upgrading to a newer version only happens through a new
|
|
23
|
-
pull request into that repository (QuikTrack's "re-bootstrap" flow), never silently.
|
|
24
|
-
|
|
25
|
-
Releases follow [SemVer](https://semver.org/): a major version bump means the workflow file's own
|
|
26
|
-
shape changed (inputs, permissions) and requires a new pull request regardless of this package's
|
|
27
|
-
version; minor/patch releases are runner-logic fixes safe to pick up on the next re-bootstrap.
|
|
1
|
+
# @quikit/quikpilot-runner
|
|
2
|
+
|
|
3
|
+
QuikPilot's CI runner. Invoked automatically by the `.github/workflows/quikpilot.yml` file QuikPilot
|
|
4
|
+
installs in an onboarded repository — it fetches the run's work packet, runs your setup/lint/test
|
|
5
|
+
commands, reports results to QuikTest, enforces the denied-path guardrail, commits, pushes, and
|
|
6
|
+
reports back to QuikTrack.
|
|
7
|
+
|
|
8
|
+
This package is not meant to be installed or run by hand. It exists so a fix to QuikPilot's runner
|
|
9
|
+
logic ships as a new package version rather than a new pull request into every onboarded repository
|
|
10
|
+
— the workflow file itself lives in your repository (QuikIT cannot update it directly), so it stays a
|
|
11
|
+
thin, ~25-line launcher that invokes an exact, pinned version of this package via `npx`.
|
|
12
|
+
|
|
13
|
+
It carries no secrets and needs none of your credentials: it authenticates to QuikTrack and to GitHub
|
|
14
|
+
using short-lived tokens exchanged at runtime, never a value baked into the workflow file or this
|
|
15
|
+
package.
|
|
16
|
+
|
|
17
|
+
## Versioning and support
|
|
18
|
+
|
|
19
|
+
Every workflow file pins an **exact** version of this package (`npx --yes
|
|
20
|
+
@quikit/quikpilot-runner@<version>`, never a range or `latest`) — a repository never picks up a new
|
|
21
|
+
release on its own. Older published versions are not removed from npm and keep working as long as a
|
|
22
|
+
repository's workflow file keeps pinning them; upgrading to a newer version only happens through a new
|
|
23
|
+
pull request into that repository (QuikTrack's "re-bootstrap" flow), never silently.
|
|
24
|
+
|
|
25
|
+
Releases follow [SemVer](https://semver.org/): a major version bump means the workflow file's own
|
|
26
|
+
shape changed (inputs, permissions) and requires a new pull request regardless of this package's
|
|
27
|
+
version; minor/patch releases are runner-logic fixes safe to pick up on the next re-bootstrap.
|
package/dist/index.js
CHANGED
|
@@ -73,11 +73,12 @@ async function describeQuikTrackFailure(res) {
|
|
|
73
73
|
|
|
74
74
|
// src/callback.ts
|
|
75
75
|
async function reportBack(params) {
|
|
76
|
-
const { callbackUrl, runToken, status, branch, warnings, targeting } = params;
|
|
76
|
+
const { callbackUrl, runToken, status, branch, warnings, targeting, baselineFindings } = params;
|
|
77
77
|
const body = { status };
|
|
78
78
|
if (branch) body.branch = branch;
|
|
79
79
|
if (warnings && warnings.length > 0) body.warnings = warnings;
|
|
80
80
|
if (targeting) body.targeting = targeting;
|
|
81
|
+
if (baselineFindings) body.baselineFindings = baselineFindings;
|
|
81
82
|
const res = await fetch(callbackUrl, {
|
|
82
83
|
method: "POST",
|
|
83
84
|
headers: { Authorization: `Bearer ${runToken}`, "Content-Type": "application/json" },
|
|
@@ -109,10 +110,36 @@ function runTestCommand(command) {
|
|
|
109
110
|
}
|
|
110
111
|
function runTestCommandAsync(command) {
|
|
111
112
|
if (!command) return Promise.resolve("success");
|
|
112
|
-
return new Promise((
|
|
113
|
+
return new Promise((resolve4) => {
|
|
113
114
|
const child = (0, import_node_child_process.spawn)(command, { shell: "/bin/bash", stdio: "inherit" });
|
|
114
|
-
child.on("exit", (code) =>
|
|
115
|
-
child.on("error", () =>
|
|
115
|
+
child.on("exit", (code) => resolve4(code === 0 ? "success" : "failure"));
|
|
116
|
+
child.on("error", () => resolve4("failure"));
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
function runCommandWithTimeout(command, timeoutMs) {
|
|
120
|
+
return new Promise((resolve4) => {
|
|
121
|
+
let settled = false;
|
|
122
|
+
const finish = (outcome) => {
|
|
123
|
+
if (settled) return;
|
|
124
|
+
settled = true;
|
|
125
|
+
clearTimeout(timer);
|
|
126
|
+
resolve4(outcome);
|
|
127
|
+
};
|
|
128
|
+
const child = (0, import_node_child_process.spawn)(command, { shell: "/bin/bash", stdio: "inherit", detached: true });
|
|
129
|
+
const timer = setTimeout(() => {
|
|
130
|
+
try {
|
|
131
|
+
if (child.pid !== void 0) process.kill(-child.pid, "SIGKILL");
|
|
132
|
+
else child.kill("SIGKILL");
|
|
133
|
+
} catch {
|
|
134
|
+
try {
|
|
135
|
+
child.kill("SIGKILL");
|
|
136
|
+
} catch {
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
finish("timeout");
|
|
140
|
+
}, timeoutMs);
|
|
141
|
+
child.on("exit", () => finish("exited"));
|
|
142
|
+
child.on("error", () => finish("error"));
|
|
116
143
|
});
|
|
117
144
|
}
|
|
118
145
|
|
|
@@ -959,8 +986,8 @@ async function reportToQuikTest(params) {
|
|
|
959
986
|
try {
|
|
960
987
|
console.log(`reporting ${report}`);
|
|
961
988
|
const ciUrl = buildCiUrl(githubServerUrl, githubRepository, githubRunId);
|
|
962
|
-
const { readFileSync:
|
|
963
|
-
const body =
|
|
989
|
+
const { readFileSync: readFileSync6 } = await import("node:fs");
|
|
990
|
+
const body = readFileSync6(report);
|
|
964
991
|
let url = `${resultsUrl}?ciUrl=${encodeURIComponent(ciUrl)}&automationIdFormat=bracket`;
|
|
965
992
|
if (generatedFiles) {
|
|
966
993
|
url += `&generatedFiles=${encodeURIComponent(generatedFiles.join(","))}`;
|
|
@@ -987,12 +1014,18 @@ async function reportToQuikTest(params) {
|
|
|
987
1014
|
}
|
|
988
1015
|
}
|
|
989
1016
|
|
|
1017
|
+
// src/baselineCheck.ts
|
|
1018
|
+
var import_node_child_process3 = require("node:child_process");
|
|
1019
|
+
var import_node_fs6 = require("node:fs");
|
|
1020
|
+
var import_node_path4 = require("node:path");
|
|
1021
|
+
|
|
990
1022
|
// src/regressionCheck.ts
|
|
991
1023
|
var import_node_fs5 = require("node:fs");
|
|
992
1024
|
var TESTCASE_RE = /<testcase\b([^>]*?)(?:\/>|>([\s\S]*?)<\/testcase>)/g;
|
|
993
1025
|
var NAME_ATTR_RE = /\bname="([^"]*)"/;
|
|
994
1026
|
var CLASSNAME_ATTR_RE = /\bclassname="([^"]*)"/;
|
|
995
1027
|
var FAILURE_CHILD_RE = /<failure\b|<error\b/;
|
|
1028
|
+
var SKIPPED_CHILD_RE = /<skipped\b/;
|
|
996
1029
|
var TESTSUITE_OPEN_RE = /<testsuite\b([^>]*?)>/g;
|
|
997
1030
|
var FILE_ATTR_RE = /\bfile="([^"]*)"/;
|
|
998
1031
|
var QP_TOKEN_BRACKET_RE = /\[(QP-[A-Z][A-Z0-9]*-\d+-\d+)\]/;
|
|
@@ -1014,8 +1047,10 @@ function extractFailureText(testcaseBody) {
|
|
|
1014
1047
|
const text = unescapeXmlEntities(raw);
|
|
1015
1048
|
return text.length > MAX_FAILURE_TEXT ? `${text.slice(0, MAX_FAILURE_TEXT)}\u2026` : text;
|
|
1016
1049
|
}
|
|
1050
|
+
var RESULT_RANK = { pass: 0, skipped: 1, fail: 2 };
|
|
1017
1051
|
function parseJUnitOutcomes(xml) {
|
|
1018
1052
|
const outcomes = /* @__PURE__ */ new Map();
|
|
1053
|
+
const automationIdResults = /* @__PURE__ */ new Map();
|
|
1019
1054
|
const failureText = /* @__PURE__ */ new Map();
|
|
1020
1055
|
const discoveredFiles = /* @__PURE__ */ new Set();
|
|
1021
1056
|
const discoveredAutomationIds = /* @__PURE__ */ new Set();
|
|
@@ -1043,9 +1078,14 @@ function parseJUnitOutcomes(xml) {
|
|
|
1043
1078
|
if (file) discoveredFiles.add(normalizeSlashes(file));
|
|
1044
1079
|
if (classname) discoveredFiles.add(normalizeSlashes(classname));
|
|
1045
1080
|
const bracket = QP_TOKEN_BRACKET_RE.exec(name)?.[1];
|
|
1046
|
-
if (bracket)
|
|
1081
|
+
if (bracket) {
|
|
1082
|
+
discoveredAutomationIds.add(bracket);
|
|
1083
|
+
const result = failed ? "fail" : SKIPPED_CHILD_RE.test(body) ? "skipped" : "pass";
|
|
1084
|
+
const seen = automationIdResults.get(bracket);
|
|
1085
|
+
if (!seen || RESULT_RANK[result] > RESULT_RANK[seen]) automationIdResults.set(bracket, result);
|
|
1086
|
+
}
|
|
1047
1087
|
}
|
|
1048
|
-
return { outcomes, failureText, discoveredFiles, discoveredAutomationIds };
|
|
1088
|
+
return { outcomes, automationIdResults, failureText, discoveredFiles, discoveredAutomationIds };
|
|
1049
1089
|
}
|
|
1050
1090
|
function readJUnitOutcomes(cwd) {
|
|
1051
1091
|
const report = findJUnitReport(cwd);
|
|
@@ -1056,6 +1096,9 @@ function readJUnitOutcomes(cwd) {
|
|
|
1056
1096
|
return null;
|
|
1057
1097
|
}
|
|
1058
1098
|
}
|
|
1099
|
+
function readAutomationIdResults(cwd = process.cwd()) {
|
|
1100
|
+
return readJUnitOutcomes(cwd)?.automationIdResults ?? null;
|
|
1101
|
+
}
|
|
1059
1102
|
function captureTestSnapshot(command, cwd = process.cwd()) {
|
|
1060
1103
|
const outcome = runTestCommand(command);
|
|
1061
1104
|
const parsed = readJUnitOutcomes(cwd);
|
|
@@ -1126,8 +1169,186 @@ function describeRegressions(check) {
|
|
|
1126
1169
|
return parts.join("; ");
|
|
1127
1170
|
}
|
|
1128
1171
|
|
|
1172
|
+
// src/baselineCheck.ts
|
|
1173
|
+
var BASELINE_CHECK_TIMEOUT_MS = 12e4;
|
|
1174
|
+
function shellQuote(value) {
|
|
1175
|
+
return `'${value.split("'").join("'\\''")}'`;
|
|
1176
|
+
}
|
|
1177
|
+
function buildSingleFileCommand(command, path) {
|
|
1178
|
+
const quoted = shellQuote(path);
|
|
1179
|
+
return command.includes("{file}") ? command.split("{file}").join(quoted) : `${command} ${quoted}`;
|
|
1180
|
+
}
|
|
1181
|
+
var isTestTarget2 = (t) => (t.automationIds?.length ?? 0) > 0;
|
|
1182
|
+
function checkableIds(ids, types) {
|
|
1183
|
+
if (!types) return [];
|
|
1184
|
+
return ids.filter((id) => {
|
|
1185
|
+
if (!Object.prototype.hasOwnProperty.call(types, id)) return false;
|
|
1186
|
+
const type = types[id];
|
|
1187
|
+
return typeof type === "string" && type.trim() !== "" && type.trim().toUpperCase() !== "REGRESSION";
|
|
1188
|
+
});
|
|
1189
|
+
}
|
|
1190
|
+
function insideCheckout(cwd, path) {
|
|
1191
|
+
const abs = (0, import_node_path4.resolve)(cwd, path);
|
|
1192
|
+
const rel = (0, import_node_path4.relative)(cwd, abs);
|
|
1193
|
+
return rel === "" || rel.startsWith("..") || (0, import_node_path4.isAbsolute)(rel) ? null : abs;
|
|
1194
|
+
}
|
|
1195
|
+
function writeForCheck(abs, content) {
|
|
1196
|
+
const original = (0, import_node_fs6.existsSync)(abs) ? (0, import_node_fs6.readFileSync)(abs) : null;
|
|
1197
|
+
const createdDirs = [];
|
|
1198
|
+
if (original === null) {
|
|
1199
|
+
const missing = [];
|
|
1200
|
+
for (let d = (0, import_node_path4.dirname)(abs); !(0, import_node_fs6.existsSync)(d); d = (0, import_node_path4.dirname)(d)) missing.push(d);
|
|
1201
|
+
for (const d of missing.reverse()) {
|
|
1202
|
+
(0, import_node_fs6.mkdirSync)(d);
|
|
1203
|
+
createdDirs.push(d);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
(0, import_node_fs6.writeFileSync)(abs, content);
|
|
1207
|
+
return { abs, original, createdDirs };
|
|
1208
|
+
}
|
|
1209
|
+
function restore(written) {
|
|
1210
|
+
for (const file of [...written].reverse()) {
|
|
1211
|
+
try {
|
|
1212
|
+
if (file.original !== null) (0, import_node_fs6.writeFileSync)(file.abs, file.original);
|
|
1213
|
+
else (0, import_node_fs6.rmSync)(file.abs, { force: true });
|
|
1214
|
+
for (const d of [...file.createdDirs].reverse()) {
|
|
1215
|
+
try {
|
|
1216
|
+
(0, import_node_fs6.rmdirSync)(d);
|
|
1217
|
+
} catch {
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
} catch (err) {
|
|
1221
|
+
console.error(`Baseline check could not restore ${file.abs}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
function untrackedFiles(cwd) {
|
|
1226
|
+
try {
|
|
1227
|
+
const out = (0, import_node_child_process3.execFileSync)("git", ["ls-files", "--others", "--exclude-standard", "-z"], {
|
|
1228
|
+
cwd,
|
|
1229
|
+
encoding: "utf8",
|
|
1230
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1231
|
+
maxBuffer: 64 * 1024 * 1024
|
|
1232
|
+
});
|
|
1233
|
+
return new Set(out.split("\0").filter(Boolean));
|
|
1234
|
+
} catch {
|
|
1235
|
+
return null;
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
var mtimeOf = (path) => {
|
|
1239
|
+
try {
|
|
1240
|
+
return (0, import_node_fs6.statSync)(path).mtimeMs;
|
|
1241
|
+
} catch {
|
|
1242
|
+
return null;
|
|
1243
|
+
}
|
|
1244
|
+
};
|
|
1245
|
+
function decideCause(targets, results, cwd) {
|
|
1246
|
+
const codeTargets = targets.filter((t) => !isTestTarget2(t));
|
|
1247
|
+
if (codeTargets.length === 0) return "ALREADY_TRUE";
|
|
1248
|
+
for (const t of codeTargets) {
|
|
1249
|
+
if (t.kind === "NEW_FILE") return "ALREADY_TRUE";
|
|
1250
|
+
const next = results.get(t.path);
|
|
1251
|
+
const abs = insideCheckout(cwd, t.path);
|
|
1252
|
+
if (next === void 0 || abs === null) return "ALREADY_TRUE";
|
|
1253
|
+
try {
|
|
1254
|
+
if ((0, import_node_fs6.readFileSync)(abs, "utf8") !== next) return "ALREADY_TRUE";
|
|
1255
|
+
} catch {
|
|
1256
|
+
return "ALREADY_TRUE";
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
return "NO_OP";
|
|
1260
|
+
}
|
|
1261
|
+
async function runBaselineCheck(params) {
|
|
1262
|
+
const { targets, results, testFileCmd, requiredTestTypes } = params;
|
|
1263
|
+
const cwd = params.cwd ?? process.cwd();
|
|
1264
|
+
const runFile = params.runFile ?? runCommandWithTimeout;
|
|
1265
|
+
const nothing = { findings: null, warnings: [] };
|
|
1266
|
+
if (!testFileCmd || testFileCmd.trim() === "") return nothing;
|
|
1267
|
+
const testTargets = targets.filter(isTestTarget2);
|
|
1268
|
+
const toCheck = testTargets.map((target) => ({ target, ids: checkableIds(target.automationIds ?? [], requiredTestTypes) })).filter((entry) => entry.ids.length > 0);
|
|
1269
|
+
if (toCheck.length === 0) {
|
|
1270
|
+
console.log("Baseline check: no case with a known, non-Regression label \u2014 nothing to check.");
|
|
1271
|
+
return nothing;
|
|
1272
|
+
}
|
|
1273
|
+
const warnings = [];
|
|
1274
|
+
const flagged = [];
|
|
1275
|
+
const written = [];
|
|
1276
|
+
const unsafe = /* @__PURE__ */ new Set();
|
|
1277
|
+
const skip = (path, why) => {
|
|
1278
|
+
const message = `Baseline check skipped for ${path}: ${why}`;
|
|
1279
|
+
console.warn(`::warning::${message}`);
|
|
1280
|
+
warnings.push(message);
|
|
1281
|
+
};
|
|
1282
|
+
try {
|
|
1283
|
+
const contentByPath = new Map(results.map((r) => [r.targetPath, r.newContent]));
|
|
1284
|
+
const cause = decideCause(targets, contentByPath, cwd);
|
|
1285
|
+
for (const target of testTargets) {
|
|
1286
|
+
const content = contentByPath.get(target.path);
|
|
1287
|
+
if (content === void 0) continue;
|
|
1288
|
+
const abs = insideCheckout(cwd, target.path);
|
|
1289
|
+
if (abs === null) {
|
|
1290
|
+
unsafe.add(target.path);
|
|
1291
|
+
continue;
|
|
1292
|
+
}
|
|
1293
|
+
written.push(writeForCheck(abs, content));
|
|
1294
|
+
}
|
|
1295
|
+
for (const { target, ids } of toCheck) {
|
|
1296
|
+
if (unsafe.has(target.path)) {
|
|
1297
|
+
skip(target.path, "the path is outside the checkout, so the file was not written or run.");
|
|
1298
|
+
continue;
|
|
1299
|
+
}
|
|
1300
|
+
if (!contentByPath.has(target.path)) continue;
|
|
1301
|
+
const reportBefore = findJUnitReport(cwd);
|
|
1302
|
+
const mtimeBefore = reportBefore ? mtimeOf(reportBefore) : null;
|
|
1303
|
+
const untrackedBefore = untrackedFiles(cwd);
|
|
1304
|
+
const outcome = await runFile(buildSingleFileCommand(testFileCmd, target.path), BASELINE_CHECK_TIMEOUT_MS);
|
|
1305
|
+
if (outcome === "timeout") {
|
|
1306
|
+
skip(
|
|
1307
|
+
target.path,
|
|
1308
|
+
`the single-file command did not finish within ${BASELINE_CHECK_TIMEOUT_MS / 1e3} seconds and was stopped. No test case from this file was flagged.`
|
|
1309
|
+
);
|
|
1310
|
+
} else if (outcome === "error") {
|
|
1311
|
+
skip(target.path, "the single-file command could not be started. No test case from this file was flagged.");
|
|
1312
|
+
} else {
|
|
1313
|
+
const reportAfter = findJUnitReport(cwd);
|
|
1314
|
+
const fresh = reportAfter !== null && (reportAfter !== reportBefore || mtimeOf(reportAfter) !== mtimeBefore);
|
|
1315
|
+
if (!fresh) {
|
|
1316
|
+
skip(
|
|
1317
|
+
target.path,
|
|
1318
|
+
"the single-file command wrote no new JUnit report (it must write the same JUnit report as the test command), so nothing could be checked."
|
|
1319
|
+
);
|
|
1320
|
+
} else {
|
|
1321
|
+
const byId = readAutomationIdResults(cwd);
|
|
1322
|
+
for (const id of ids) if (byId?.get(id) === "pass") flagged.push(id);
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
const untrackedAfter = untrackedFiles(cwd);
|
|
1326
|
+
if (untrackedBefore && untrackedAfter) {
|
|
1327
|
+
for (const file of untrackedAfter) {
|
|
1328
|
+
if (untrackedBefore.has(file)) continue;
|
|
1329
|
+
const abs = insideCheckout(cwd, file);
|
|
1330
|
+
if (abs) (0, import_node_fs6.rmSync)(abs, { force: true });
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
if (flagged.length === 0) return { findings: null, warnings };
|
|
1335
|
+
const automationIds = [...new Set(flagged)].slice(0, 200);
|
|
1336
|
+
const list = automationIds.join(", ");
|
|
1337
|
+
warnings.push(
|
|
1338
|
+
cause === "NO_OP" ? `Baseline check: ${automationIds.length} test case(s) passed against the unmodified code, and every file the generated change edited came out unchanged, so the change itself may have been a no-op: ${list}.` : `Baseline check: ${automationIds.length} test case(s) passed against the unmodified code, so they may assert something already true: ${list}. This check cannot tell that from a change that did nothing.`
|
|
1339
|
+
);
|
|
1340
|
+
return { findings: { cause, automationIds }, warnings };
|
|
1341
|
+
} catch (err) {
|
|
1342
|
+
const message = `Baseline check could not run: ${err instanceof Error ? err.message : String(err)}`;
|
|
1343
|
+
console.warn(`::warning::${message}`);
|
|
1344
|
+
return { findings: null, warnings: [...warnings, message] };
|
|
1345
|
+
} finally {
|
|
1346
|
+
restore(written);
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1129
1350
|
// src/scopeCheck.ts
|
|
1130
|
-
var
|
|
1351
|
+
var import_node_fs7 = require("node:fs");
|
|
1131
1352
|
var ROUTE_METHOD_RE = /\b(GET|POST|PUT|PATCH|DELETE)\b/i;
|
|
1132
1353
|
var ROUTE_PATH_RE = /\/[A-Za-z0-9_\-{}[\]]+(?:\/[A-Za-z0-9_\-:{}[\]]+)+/;
|
|
1133
1354
|
var ROUTE_EXPORT_RE = /\bexport\s+(?:async\s+)?function\s+(GET|POST|PUT|PATCH|DELETE)\s*\(/;
|
|
@@ -1184,7 +1405,7 @@ function checkApiScope(results, targets, apiChanges) {
|
|
|
1184
1405
|
} else {
|
|
1185
1406
|
let currentContent;
|
|
1186
1407
|
try {
|
|
1187
|
-
currentContent = (0,
|
|
1408
|
+
currentContent = (0, import_node_fs7.readFileSync)(targetPath, "utf8");
|
|
1188
1409
|
} catch {
|
|
1189
1410
|
currentContent = "";
|
|
1190
1411
|
}
|
|
@@ -1203,7 +1424,7 @@ function describeScopeViolations(check) {
|
|
|
1203
1424
|
}
|
|
1204
1425
|
|
|
1205
1426
|
// src/targeting.ts
|
|
1206
|
-
var
|
|
1427
|
+
var import_node_path5 = require("node:path");
|
|
1207
1428
|
var CONTRACT_VERSION2 = "test-targets-1";
|
|
1208
1429
|
function listWorkspaceFiles(workspaceRoot) {
|
|
1209
1430
|
return listFiles(workspaceRoot ?? ".");
|
|
@@ -1232,9 +1453,9 @@ function validateTarget(target, workspaceRoot, workspaceListing, deniedPaths) {
|
|
|
1232
1453
|
}
|
|
1233
1454
|
return;
|
|
1234
1455
|
}
|
|
1235
|
-
const root = (0,
|
|
1236
|
-
const rel = (0,
|
|
1237
|
-
if (rel.startsWith("..") || (0,
|
|
1456
|
+
const root = (0, import_node_path5.resolve)(workspaceRoot ?? ".");
|
|
1457
|
+
const rel = (0, import_node_path5.relative)(root, (0, import_node_path5.resolve)(target.path));
|
|
1458
|
+
if (rel.startsWith("..") || (0, import_node_path5.isAbsolute)(rel)) {
|
|
1238
1459
|
throw new Error(
|
|
1239
1460
|
`pilot.targeting named a NEW_FILE path that escapes the workspace root: ${target.path}`
|
|
1240
1461
|
);
|
|
@@ -1455,10 +1676,11 @@ async function run(env2) {
|
|
|
1455
1676
|
let packet;
|
|
1456
1677
|
let branch;
|
|
1457
1678
|
let runToken;
|
|
1458
|
-
|
|
1679
|
+
const warnings = [];
|
|
1459
1680
|
let targeted = false;
|
|
1460
1681
|
let generatedFilePaths = [];
|
|
1461
1682
|
let targetingResult;
|
|
1683
|
+
let baselineFindings;
|
|
1462
1684
|
const contextLog = [];
|
|
1463
1685
|
try {
|
|
1464
1686
|
runToken = await exchangeForRunToken(env2.tokenUrl, await fetchGithubOidcToken());
|
|
@@ -1493,8 +1715,9 @@ async function run(env2) {
|
|
|
1493
1715
|
const blockingOverlaps = overlaps.filter((o) => o.severity === "identical");
|
|
1494
1716
|
const softOverlaps = overlaps.filter((o) => o.severity === "similar");
|
|
1495
1717
|
if (softOverlaps.length > 0) {
|
|
1496
|
-
|
|
1497
|
-
|
|
1718
|
+
const overlapWarnings = describeOverlapWarnings(softOverlaps);
|
|
1719
|
+
warnings.push(...overlapWarnings);
|
|
1720
|
+
for (const w of overlapWarnings) {
|
|
1498
1721
|
console.warn(`QuikPilot targeting warning (not blocking): ${w}`);
|
|
1499
1722
|
}
|
|
1500
1723
|
}
|
|
@@ -1544,6 +1767,20 @@ async function run(env2) {
|
|
|
1544
1767
|
`QuikPilot's generated tests are not where the test runner will find them \u2014 ${describeTestPlacementViolations(placementCheck)}`
|
|
1545
1768
|
);
|
|
1546
1769
|
}
|
|
1770
|
+
try {
|
|
1771
|
+
const baselineCheck = await runBaselineCheck({
|
|
1772
|
+
targets: targeting.targets,
|
|
1773
|
+
results,
|
|
1774
|
+
testFileCmd: packet.commands.testFile,
|
|
1775
|
+
requiredTestTypes: packet.requiredTestTypes
|
|
1776
|
+
});
|
|
1777
|
+
warnings.push(...baselineCheck.warnings);
|
|
1778
|
+
baselineFindings = baselineCheck.findings ?? void 0;
|
|
1779
|
+
} catch (err) {
|
|
1780
|
+
const message = `Baseline check could not run: ${err instanceof Error ? err.message : String(err)}`;
|
|
1781
|
+
console.error(message);
|
|
1782
|
+
warnings.push(message);
|
|
1783
|
+
}
|
|
1547
1784
|
generatedFilePaths = results.map((r) => r.targetPath);
|
|
1548
1785
|
flushCodegenResults(results);
|
|
1549
1786
|
try {
|
|
@@ -1615,7 +1852,8 @@ async function run(env2) {
|
|
|
1615
1852
|
branch,
|
|
1616
1853
|
warnings,
|
|
1617
1854
|
// Omitted when targeting itself never produced a result (nothing exists to record).
|
|
1618
|
-
targeting: targetingResult ? buildTargetingRecord(targetingResult, contextLog) : void 0
|
|
1855
|
+
targeting: targetingResult ? buildTargetingRecord(targetingResult, contextLog) : void 0,
|
|
1856
|
+
baselineFindings
|
|
1619
1857
|
});
|
|
1620
1858
|
return { hardFailure };
|
|
1621
1859
|
}
|