@quikit/quikpilot-runner 8.2.5 → 8.2.7
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 +271 -20
- 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+)\]/;
|
|
@@ -1005,17 +1038,32 @@ var MAX_FAILURE_TEXT = 500;
|
|
|
1005
1038
|
function unescapeXmlEntities(s) {
|
|
1006
1039
|
return s.replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, "&");
|
|
1007
1040
|
}
|
|
1041
|
+
var FAILURE_FILE_LINE_RE = /^[ \t]*File:[ \t]*(\S[^\r\n]*?:\d+(?::\d+)?)[ \t]*$/m;
|
|
1042
|
+
var MAX_FAILURE_LOCATION = 300;
|
|
1043
|
+
function parseFailureLocation(body) {
|
|
1044
|
+
const found = FAILURE_FILE_LINE_RE.exec(unescapeXmlEntities(body));
|
|
1045
|
+
if (!found) return null;
|
|
1046
|
+
const path = normalizeSlashes(found[1]);
|
|
1047
|
+
const root = `${normalizeSlashes(process.cwd()).replace(/\/+$/, "")}/`;
|
|
1048
|
+
const shown = path.startsWith(root) ? path.slice(root.length) : path;
|
|
1049
|
+
return shown.length > MAX_FAILURE_LOCATION ? `${shown.slice(0, MAX_FAILURE_LOCATION)}\u2026` : shown;
|
|
1050
|
+
}
|
|
1008
1051
|
function extractFailureText(testcaseBody) {
|
|
1009
1052
|
const tag = FAILURE_TAG_RE.exec(testcaseBody);
|
|
1010
1053
|
if (!tag) return null;
|
|
1011
1054
|
const [, attrs = "", inner = ""] = tag;
|
|
1012
|
-
const
|
|
1055
|
+
const messageAttr = FAILURE_MESSAGE_ATTR_RE.exec(attrs)?.[1];
|
|
1056
|
+
const raw = (messageAttr ?? inner).trim();
|
|
1013
1057
|
if (!raw) return null;
|
|
1014
1058
|
const text = unescapeXmlEntities(raw);
|
|
1015
|
-
|
|
1059
|
+
const shortened = text.length > MAX_FAILURE_TEXT ? `${text.slice(0, MAX_FAILURE_TEXT)}\u2026` : text;
|
|
1060
|
+
const location = messageAttr === void 0 ? null : parseFailureLocation(inner);
|
|
1061
|
+
return location ? `${shortened} [at ${location}]` : shortened;
|
|
1016
1062
|
}
|
|
1063
|
+
var RESULT_RANK = { pass: 0, skipped: 1, fail: 2 };
|
|
1017
1064
|
function parseJUnitOutcomes(xml) {
|
|
1018
1065
|
const outcomes = /* @__PURE__ */ new Map();
|
|
1066
|
+
const automationIdResults = /* @__PURE__ */ new Map();
|
|
1019
1067
|
const failureText = /* @__PURE__ */ new Map();
|
|
1020
1068
|
const discoveredFiles = /* @__PURE__ */ new Set();
|
|
1021
1069
|
const discoveredAutomationIds = /* @__PURE__ */ new Set();
|
|
@@ -1043,9 +1091,14 @@ function parseJUnitOutcomes(xml) {
|
|
|
1043
1091
|
if (file) discoveredFiles.add(normalizeSlashes(file));
|
|
1044
1092
|
if (classname) discoveredFiles.add(normalizeSlashes(classname));
|
|
1045
1093
|
const bracket = QP_TOKEN_BRACKET_RE.exec(name)?.[1];
|
|
1046
|
-
if (bracket)
|
|
1094
|
+
if (bracket) {
|
|
1095
|
+
discoveredAutomationIds.add(bracket);
|
|
1096
|
+
const result = failed ? "fail" : SKIPPED_CHILD_RE.test(body) ? "skipped" : "pass";
|
|
1097
|
+
const seen = automationIdResults.get(bracket);
|
|
1098
|
+
if (!seen || RESULT_RANK[result] > RESULT_RANK[seen]) automationIdResults.set(bracket, result);
|
|
1099
|
+
}
|
|
1047
1100
|
}
|
|
1048
|
-
return { outcomes, failureText, discoveredFiles, discoveredAutomationIds };
|
|
1101
|
+
return { outcomes, automationIdResults, failureText, discoveredFiles, discoveredAutomationIds };
|
|
1049
1102
|
}
|
|
1050
1103
|
function readJUnitOutcomes(cwd) {
|
|
1051
1104
|
const report = findJUnitReport(cwd);
|
|
@@ -1056,6 +1109,9 @@ function readJUnitOutcomes(cwd) {
|
|
|
1056
1109
|
return null;
|
|
1057
1110
|
}
|
|
1058
1111
|
}
|
|
1112
|
+
function readAutomationIdResults(cwd = process.cwd()) {
|
|
1113
|
+
return readJUnitOutcomes(cwd)?.automationIdResults ?? null;
|
|
1114
|
+
}
|
|
1059
1115
|
function captureTestSnapshot(command, cwd = process.cwd()) {
|
|
1060
1116
|
const outcome = runTestCommand(command);
|
|
1061
1117
|
const parsed = readJUnitOutcomes(cwd);
|
|
@@ -1126,8 +1182,186 @@ function describeRegressions(check) {
|
|
|
1126
1182
|
return parts.join("; ");
|
|
1127
1183
|
}
|
|
1128
1184
|
|
|
1185
|
+
// src/baselineCheck.ts
|
|
1186
|
+
var BASELINE_CHECK_TIMEOUT_MS = 12e4;
|
|
1187
|
+
function shellQuote(value) {
|
|
1188
|
+
return `'${value.split("'").join("'\\''")}'`;
|
|
1189
|
+
}
|
|
1190
|
+
function buildSingleFileCommand(command, path) {
|
|
1191
|
+
const quoted = shellQuote(path);
|
|
1192
|
+
return command.includes("{file}") ? command.split("{file}").join(quoted) : `${command} ${quoted}`;
|
|
1193
|
+
}
|
|
1194
|
+
var isTestTarget2 = (t) => (t.automationIds?.length ?? 0) > 0;
|
|
1195
|
+
function checkableIds(ids, types) {
|
|
1196
|
+
if (!types) return [];
|
|
1197
|
+
return ids.filter((id) => {
|
|
1198
|
+
if (!Object.prototype.hasOwnProperty.call(types, id)) return false;
|
|
1199
|
+
const type = types[id];
|
|
1200
|
+
return typeof type === "string" && type.trim() !== "" && type.trim().toUpperCase() !== "REGRESSION";
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
function insideCheckout(cwd, path) {
|
|
1204
|
+
const abs = (0, import_node_path4.resolve)(cwd, path);
|
|
1205
|
+
const rel = (0, import_node_path4.relative)(cwd, abs);
|
|
1206
|
+
return rel === "" || rel.startsWith("..") || (0, import_node_path4.isAbsolute)(rel) ? null : abs;
|
|
1207
|
+
}
|
|
1208
|
+
function writeForCheck(abs, content) {
|
|
1209
|
+
const original = (0, import_node_fs6.existsSync)(abs) ? (0, import_node_fs6.readFileSync)(abs) : null;
|
|
1210
|
+
const createdDirs = [];
|
|
1211
|
+
if (original === null) {
|
|
1212
|
+
const missing = [];
|
|
1213
|
+
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);
|
|
1214
|
+
for (const d of missing.reverse()) {
|
|
1215
|
+
(0, import_node_fs6.mkdirSync)(d);
|
|
1216
|
+
createdDirs.push(d);
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
(0, import_node_fs6.writeFileSync)(abs, content);
|
|
1220
|
+
return { abs, original, createdDirs };
|
|
1221
|
+
}
|
|
1222
|
+
function restore(written) {
|
|
1223
|
+
for (const file of [...written].reverse()) {
|
|
1224
|
+
try {
|
|
1225
|
+
if (file.original !== null) (0, import_node_fs6.writeFileSync)(file.abs, file.original);
|
|
1226
|
+
else (0, import_node_fs6.rmSync)(file.abs, { force: true });
|
|
1227
|
+
for (const d of [...file.createdDirs].reverse()) {
|
|
1228
|
+
try {
|
|
1229
|
+
(0, import_node_fs6.rmdirSync)(d);
|
|
1230
|
+
} catch {
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
} catch (err) {
|
|
1234
|
+
console.error(`Baseline check could not restore ${file.abs}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
function untrackedFiles(cwd) {
|
|
1239
|
+
try {
|
|
1240
|
+
const out = (0, import_node_child_process3.execFileSync)("git", ["ls-files", "--others", "--exclude-standard", "-z"], {
|
|
1241
|
+
cwd,
|
|
1242
|
+
encoding: "utf8",
|
|
1243
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1244
|
+
maxBuffer: 64 * 1024 * 1024
|
|
1245
|
+
});
|
|
1246
|
+
return new Set(out.split("\0").filter(Boolean));
|
|
1247
|
+
} catch {
|
|
1248
|
+
return null;
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
var mtimeOf = (path) => {
|
|
1252
|
+
try {
|
|
1253
|
+
return (0, import_node_fs6.statSync)(path).mtimeMs;
|
|
1254
|
+
} catch {
|
|
1255
|
+
return null;
|
|
1256
|
+
}
|
|
1257
|
+
};
|
|
1258
|
+
function decideCause(targets, results, cwd) {
|
|
1259
|
+
const codeTargets = targets.filter((t) => !isTestTarget2(t));
|
|
1260
|
+
if (codeTargets.length === 0) return "ALREADY_TRUE";
|
|
1261
|
+
for (const t of codeTargets) {
|
|
1262
|
+
if (t.kind === "NEW_FILE") return "ALREADY_TRUE";
|
|
1263
|
+
const next = results.get(t.path);
|
|
1264
|
+
const abs = insideCheckout(cwd, t.path);
|
|
1265
|
+
if (next === void 0 || abs === null) return "ALREADY_TRUE";
|
|
1266
|
+
try {
|
|
1267
|
+
if ((0, import_node_fs6.readFileSync)(abs, "utf8") !== next) return "ALREADY_TRUE";
|
|
1268
|
+
} catch {
|
|
1269
|
+
return "ALREADY_TRUE";
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
return "NO_OP";
|
|
1273
|
+
}
|
|
1274
|
+
async function runBaselineCheck(params) {
|
|
1275
|
+
const { targets, results, testFileCmd, requiredTestTypes } = params;
|
|
1276
|
+
const cwd = params.cwd ?? process.cwd();
|
|
1277
|
+
const runFile = params.runFile ?? runCommandWithTimeout;
|
|
1278
|
+
const nothing = { findings: null, warnings: [] };
|
|
1279
|
+
if (!testFileCmd || testFileCmd.trim() === "") return nothing;
|
|
1280
|
+
const testTargets = targets.filter(isTestTarget2);
|
|
1281
|
+
const toCheck = testTargets.map((target) => ({ target, ids: checkableIds(target.automationIds ?? [], requiredTestTypes) })).filter((entry) => entry.ids.length > 0);
|
|
1282
|
+
if (toCheck.length === 0) {
|
|
1283
|
+
console.log("Baseline check: no case with a known, non-Regression label \u2014 nothing to check.");
|
|
1284
|
+
return nothing;
|
|
1285
|
+
}
|
|
1286
|
+
const warnings = [];
|
|
1287
|
+
const flagged = [];
|
|
1288
|
+
const written = [];
|
|
1289
|
+
const unsafe = /* @__PURE__ */ new Set();
|
|
1290
|
+
const skip = (path, why) => {
|
|
1291
|
+
const message = `Baseline check skipped for ${path}: ${why}`;
|
|
1292
|
+
console.warn(`::warning::${message}`);
|
|
1293
|
+
warnings.push(message);
|
|
1294
|
+
};
|
|
1295
|
+
try {
|
|
1296
|
+
const contentByPath = new Map(results.map((r) => [r.targetPath, r.newContent]));
|
|
1297
|
+
const cause = decideCause(targets, contentByPath, cwd);
|
|
1298
|
+
for (const target of testTargets) {
|
|
1299
|
+
const content = contentByPath.get(target.path);
|
|
1300
|
+
if (content === void 0) continue;
|
|
1301
|
+
const abs = insideCheckout(cwd, target.path);
|
|
1302
|
+
if (abs === null) {
|
|
1303
|
+
unsafe.add(target.path);
|
|
1304
|
+
continue;
|
|
1305
|
+
}
|
|
1306
|
+
written.push(writeForCheck(abs, content));
|
|
1307
|
+
}
|
|
1308
|
+
for (const { target, ids } of toCheck) {
|
|
1309
|
+
if (unsafe.has(target.path)) {
|
|
1310
|
+
skip(target.path, "the path is outside the checkout, so the file was not written or run.");
|
|
1311
|
+
continue;
|
|
1312
|
+
}
|
|
1313
|
+
if (!contentByPath.has(target.path)) continue;
|
|
1314
|
+
const reportBefore = findJUnitReport(cwd);
|
|
1315
|
+
const mtimeBefore = reportBefore ? mtimeOf(reportBefore) : null;
|
|
1316
|
+
const untrackedBefore = untrackedFiles(cwd);
|
|
1317
|
+
const outcome = await runFile(buildSingleFileCommand(testFileCmd, target.path), BASELINE_CHECK_TIMEOUT_MS);
|
|
1318
|
+
if (outcome === "timeout") {
|
|
1319
|
+
skip(
|
|
1320
|
+
target.path,
|
|
1321
|
+
`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.`
|
|
1322
|
+
);
|
|
1323
|
+
} else if (outcome === "error") {
|
|
1324
|
+
skip(target.path, "the single-file command could not be started. No test case from this file was flagged.");
|
|
1325
|
+
} else {
|
|
1326
|
+
const reportAfter = findJUnitReport(cwd);
|
|
1327
|
+
const fresh = reportAfter !== null && (reportAfter !== reportBefore || mtimeOf(reportAfter) !== mtimeBefore);
|
|
1328
|
+
if (!fresh) {
|
|
1329
|
+
skip(
|
|
1330
|
+
target.path,
|
|
1331
|
+
"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."
|
|
1332
|
+
);
|
|
1333
|
+
} else {
|
|
1334
|
+
const byId = readAutomationIdResults(cwd);
|
|
1335
|
+
for (const id of ids) if (byId?.get(id) === "pass") flagged.push(id);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
const untrackedAfter = untrackedFiles(cwd);
|
|
1339
|
+
if (untrackedBefore && untrackedAfter) {
|
|
1340
|
+
for (const file of untrackedAfter) {
|
|
1341
|
+
if (untrackedBefore.has(file)) continue;
|
|
1342
|
+
const abs = insideCheckout(cwd, file);
|
|
1343
|
+
if (abs) (0, import_node_fs6.rmSync)(abs, { force: true });
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
if (flagged.length === 0) return { findings: null, warnings };
|
|
1348
|
+
const automationIds = [...new Set(flagged)].slice(0, 200);
|
|
1349
|
+
const list = automationIds.join(", ");
|
|
1350
|
+
warnings.push(
|
|
1351
|
+
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.`
|
|
1352
|
+
);
|
|
1353
|
+
return { findings: { cause, automationIds }, warnings };
|
|
1354
|
+
} catch (err) {
|
|
1355
|
+
const message = `Baseline check could not run: ${err instanceof Error ? err.message : String(err)}`;
|
|
1356
|
+
console.warn(`::warning::${message}`);
|
|
1357
|
+
return { findings: null, warnings: [...warnings, message] };
|
|
1358
|
+
} finally {
|
|
1359
|
+
restore(written);
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1129
1363
|
// src/scopeCheck.ts
|
|
1130
|
-
var
|
|
1364
|
+
var import_node_fs7 = require("node:fs");
|
|
1131
1365
|
var ROUTE_METHOD_RE = /\b(GET|POST|PUT|PATCH|DELETE)\b/i;
|
|
1132
1366
|
var ROUTE_PATH_RE = /\/[A-Za-z0-9_\-{}[\]]+(?:\/[A-Za-z0-9_\-:{}[\]]+)+/;
|
|
1133
1367
|
var ROUTE_EXPORT_RE = /\bexport\s+(?:async\s+)?function\s+(GET|POST|PUT|PATCH|DELETE)\s*\(/;
|
|
@@ -1184,7 +1418,7 @@ function checkApiScope(results, targets, apiChanges) {
|
|
|
1184
1418
|
} else {
|
|
1185
1419
|
let currentContent;
|
|
1186
1420
|
try {
|
|
1187
|
-
currentContent = (0,
|
|
1421
|
+
currentContent = (0, import_node_fs7.readFileSync)(targetPath, "utf8");
|
|
1188
1422
|
} catch {
|
|
1189
1423
|
currentContent = "";
|
|
1190
1424
|
}
|
|
@@ -1203,7 +1437,7 @@ function describeScopeViolations(check) {
|
|
|
1203
1437
|
}
|
|
1204
1438
|
|
|
1205
1439
|
// src/targeting.ts
|
|
1206
|
-
var
|
|
1440
|
+
var import_node_path5 = require("node:path");
|
|
1207
1441
|
var CONTRACT_VERSION2 = "test-targets-1";
|
|
1208
1442
|
function listWorkspaceFiles(workspaceRoot) {
|
|
1209
1443
|
return listFiles(workspaceRoot ?? ".");
|
|
@@ -1232,9 +1466,9 @@ function validateTarget(target, workspaceRoot, workspaceListing, deniedPaths) {
|
|
|
1232
1466
|
}
|
|
1233
1467
|
return;
|
|
1234
1468
|
}
|
|
1235
|
-
const root = (0,
|
|
1236
|
-
const rel = (0,
|
|
1237
|
-
if (rel.startsWith("..") || (0,
|
|
1469
|
+
const root = (0, import_node_path5.resolve)(workspaceRoot ?? ".");
|
|
1470
|
+
const rel = (0, import_node_path5.relative)(root, (0, import_node_path5.resolve)(target.path));
|
|
1471
|
+
if (rel.startsWith("..") || (0, import_node_path5.isAbsolute)(rel)) {
|
|
1238
1472
|
throw new Error(
|
|
1239
1473
|
`pilot.targeting named a NEW_FILE path that escapes the workspace root: ${target.path}`
|
|
1240
1474
|
);
|
|
@@ -1455,10 +1689,11 @@ async function run(env2) {
|
|
|
1455
1689
|
let packet;
|
|
1456
1690
|
let branch;
|
|
1457
1691
|
let runToken;
|
|
1458
|
-
|
|
1692
|
+
const warnings = [];
|
|
1459
1693
|
let targeted = false;
|
|
1460
1694
|
let generatedFilePaths = [];
|
|
1461
1695
|
let targetingResult;
|
|
1696
|
+
let baselineFindings;
|
|
1462
1697
|
const contextLog = [];
|
|
1463
1698
|
try {
|
|
1464
1699
|
runToken = await exchangeForRunToken(env2.tokenUrl, await fetchGithubOidcToken());
|
|
@@ -1493,8 +1728,9 @@ async function run(env2) {
|
|
|
1493
1728
|
const blockingOverlaps = overlaps.filter((o) => o.severity === "identical");
|
|
1494
1729
|
const softOverlaps = overlaps.filter((o) => o.severity === "similar");
|
|
1495
1730
|
if (softOverlaps.length > 0) {
|
|
1496
|
-
|
|
1497
|
-
|
|
1731
|
+
const overlapWarnings = describeOverlapWarnings(softOverlaps);
|
|
1732
|
+
warnings.push(...overlapWarnings);
|
|
1733
|
+
for (const w of overlapWarnings) {
|
|
1498
1734
|
console.warn(`QuikPilot targeting warning (not blocking): ${w}`);
|
|
1499
1735
|
}
|
|
1500
1736
|
}
|
|
@@ -1544,6 +1780,20 @@ async function run(env2) {
|
|
|
1544
1780
|
`QuikPilot's generated tests are not where the test runner will find them \u2014 ${describeTestPlacementViolations(placementCheck)}`
|
|
1545
1781
|
);
|
|
1546
1782
|
}
|
|
1783
|
+
try {
|
|
1784
|
+
const baselineCheck = await runBaselineCheck({
|
|
1785
|
+
targets: targeting.targets,
|
|
1786
|
+
results,
|
|
1787
|
+
testFileCmd: packet.commands.testFile,
|
|
1788
|
+
requiredTestTypes: packet.requiredTestTypes
|
|
1789
|
+
});
|
|
1790
|
+
warnings.push(...baselineCheck.warnings);
|
|
1791
|
+
baselineFindings = baselineCheck.findings ?? void 0;
|
|
1792
|
+
} catch (err) {
|
|
1793
|
+
const message = `Baseline check could not run: ${err instanceof Error ? err.message : String(err)}`;
|
|
1794
|
+
console.error(message);
|
|
1795
|
+
warnings.push(message);
|
|
1796
|
+
}
|
|
1547
1797
|
generatedFilePaths = results.map((r) => r.targetPath);
|
|
1548
1798
|
flushCodegenResults(results);
|
|
1549
1799
|
try {
|
|
@@ -1615,7 +1865,8 @@ async function run(env2) {
|
|
|
1615
1865
|
branch,
|
|
1616
1866
|
warnings,
|
|
1617
1867
|
// Omitted when targeting itself never produced a result (nothing exists to record).
|
|
1618
|
-
targeting: targetingResult ? buildTargetingRecord(targetingResult, contextLog) : void 0
|
|
1868
|
+
targeting: targetingResult ? buildTargetingRecord(targetingResult, contextLog) : void 0,
|
|
1869
|
+
baselineFindings
|
|
1619
1870
|
});
|
|
1620
1871
|
return { hardFailure };
|
|
1621
1872
|
}
|