@quikit/quikpilot-runner 8.2.4 → 8.2.5
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 +142 -13
- 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,10 +73,11 @@ async function describeQuikTrackFailure(res) {
|
|
|
73
73
|
|
|
74
74
|
// src/callback.ts
|
|
75
75
|
async function reportBack(params) {
|
|
76
|
-
const { callbackUrl, runToken, status, branch, warnings } = params;
|
|
76
|
+
const { callbackUrl, runToken, status, branch, warnings, targeting } = params;
|
|
77
77
|
const body = { status };
|
|
78
78
|
if (branch) body.branch = branch;
|
|
79
79
|
if (warnings && warnings.length > 0) body.warnings = warnings;
|
|
80
|
+
if (targeting) body.targeting = targeting;
|
|
80
81
|
const res = await fetch(callbackUrl, {
|
|
81
82
|
method: "POST",
|
|
82
83
|
headers: { Authorization: `Bearer ${runToken}`, "Content-Type": "application/json" },
|
|
@@ -214,6 +215,13 @@ function matchesDeniedPath(path, patterns) {
|
|
|
214
215
|
}
|
|
215
216
|
return null;
|
|
216
217
|
}
|
|
218
|
+
function findDeniedContextPath(contextPaths, patterns) {
|
|
219
|
+
for (const path of contextPaths) {
|
|
220
|
+
const pattern = matchesDeniedPath(path, patterns);
|
|
221
|
+
if (pattern) return { path, pattern };
|
|
222
|
+
}
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
217
225
|
|
|
218
226
|
// src/envelope.ts
|
|
219
227
|
var HARD_FAILURE_CODES = /* @__PURE__ */ new Set(["SEMANTIC_MISMATCH", "CONTRACT_REJECTED"]);
|
|
@@ -384,13 +392,19 @@ function narrowJudgedByForTarget(pool, targets, codeTargetPath) {
|
|
|
384
392
|
function toWireJudgedBy(pool) {
|
|
385
393
|
return pool.map(({ automationId: _automationId, ...rest }) => rest);
|
|
386
394
|
}
|
|
387
|
-
async function runCodegen(gateway, targets, workspaceListing, requiredTests, deniedPaths, judgedByPool, warnings) {
|
|
395
|
+
async function runCodegen(gateway, targets, workspaceListing, requiredTests, deniedPaths, judgedByPool, warnings, contextLog) {
|
|
388
396
|
const results = [];
|
|
389
397
|
const listing = new Set(workspaceListing);
|
|
390
398
|
for (const target of targets) {
|
|
391
399
|
const isNewFile = target.kind === "NEW_FILE";
|
|
392
400
|
const currentContent = isNewFile ? "" : (0, import_node_fs.readFileSync)(target.path, "utf8");
|
|
393
401
|
const contextPaths = target.contextPaths ?? [];
|
|
402
|
+
const deniedContext = findDeniedContextPath(contextPaths, deniedPaths);
|
|
403
|
+
if (deniedContext) {
|
|
404
|
+
throw new Error(
|
|
405
|
+
`pilot.codegen contextPaths cited a denied path (${deniedContext.pattern}): ${deniedContext.path} (target ${target.path})`
|
|
406
|
+
);
|
|
407
|
+
}
|
|
394
408
|
for (const contextPath of contextPaths) {
|
|
395
409
|
if (!listing.has(contextPath)) {
|
|
396
410
|
throw new Error(
|
|
@@ -404,15 +418,14 @@ async function runCodegen(gateway, targets, workspaceListing, requiredTests, den
|
|
|
404
418
|
for (const s of added.skipped) {
|
|
405
419
|
warnings.push(`not shown to the test for ${target.path}: ${s.path} (${s.reason})`);
|
|
406
420
|
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
);
|
|
421
|
+
const contextRecord = {
|
|
422
|
+
targetPath: target.path,
|
|
423
|
+
contextPaths,
|
|
424
|
+
addedPaths: added.entries.map((e) => e.path),
|
|
425
|
+
skipped: added.skipped
|
|
426
|
+
};
|
|
427
|
+
console.log(JSON.stringify({ event: "codegen.context", ...contextRecord }));
|
|
428
|
+
contextLog?.push(contextRecord);
|
|
416
429
|
const contextData = {
|
|
417
430
|
targetPath: target.path,
|
|
418
431
|
currentContent,
|
|
@@ -470,6 +483,8 @@ async function runCodegen(gateway, targets, workspaceListing, requiredTests, den
|
|
|
470
483
|
if (envelope?.errorCode === CONTEXT_TOO_LARGE_CODE && added.entries.length > 0) {
|
|
471
484
|
contextData.contextContents = namedContents;
|
|
472
485
|
warnings.push(`context for ${target.path} was too large with the run's other files; retried without them`);
|
|
486
|
+
contextRecord.addedPaths = [];
|
|
487
|
+
contextRecord.retriedWithoutAdded = true;
|
|
473
488
|
res = await callCodegen();
|
|
474
489
|
if (!res.ok) {
|
|
475
490
|
throw new Error(`Codegen call failed for ${target.path}: ${res.status} ${res.statusText}`);
|
|
@@ -1200,6 +1215,12 @@ function validateTarget(target, workspaceRoot, workspaceListing, deniedPaths) {
|
|
|
1200
1215
|
`pilot.targeting proposed a target matching a denied path (${denied}): ${target.path}`
|
|
1201
1216
|
);
|
|
1202
1217
|
}
|
|
1218
|
+
const deniedContext = findDeniedContextPath(target.contextPaths ?? [], deniedPaths);
|
|
1219
|
+
if (deniedContext) {
|
|
1220
|
+
throw new Error(
|
|
1221
|
+
`pilot.targeting cited a denied path as context (${deniedContext.pattern}): ${deniedContext.path} (target ${target.path})`
|
|
1222
|
+
);
|
|
1223
|
+
}
|
|
1203
1224
|
if (target.kind !== "EXISTING" && target.kind !== "NEW_FILE") {
|
|
1204
1225
|
throw new Error(`pilot.targeting returned an unknown kind "${target.kind}" for ${target.path}`);
|
|
1205
1226
|
}
|
|
@@ -1269,6 +1290,108 @@ async function runTargeting(gateway, design, workspaceRoot, workspaceListing, re
|
|
|
1269
1290
|
};
|
|
1270
1291
|
}
|
|
1271
1292
|
|
|
1293
|
+
// src/targetingRecord.ts
|
|
1294
|
+
var MAX_TARGETS = 25;
|
|
1295
|
+
var MAX_IDS_PER_TARGET = 50;
|
|
1296
|
+
var MAX_CONTEXT_PATHS = 20;
|
|
1297
|
+
var MAX_ADDED_PATHS = 20;
|
|
1298
|
+
var MAX_SKIPPED_CONTEXT = 20;
|
|
1299
|
+
var MAX_SKIPPED_CASES = 200;
|
|
1300
|
+
var MAX_CONTEXT_RECORDS = 25;
|
|
1301
|
+
var MAX_PATH_CHARS = 300;
|
|
1302
|
+
var MAX_ID_CHARS = 100;
|
|
1303
|
+
var MAX_REASON_CHARS = 300;
|
|
1304
|
+
function clampList(values, max, maxChars) {
|
|
1305
|
+
let cut = false;
|
|
1306
|
+
const kept = [];
|
|
1307
|
+
for (const raw of values ?? []) {
|
|
1308
|
+
const v = typeof raw === "string" ? raw.trim() : "";
|
|
1309
|
+
if (v.length === 0) continue;
|
|
1310
|
+
if (v.length > maxChars || kept.length >= max) {
|
|
1311
|
+
cut = true;
|
|
1312
|
+
continue;
|
|
1313
|
+
}
|
|
1314
|
+
kept.push(v);
|
|
1315
|
+
}
|
|
1316
|
+
return { kept, cut };
|
|
1317
|
+
}
|
|
1318
|
+
function buildTargetingRecord(targeting, contextLog) {
|
|
1319
|
+
let truncated = false;
|
|
1320
|
+
const note = (cut) => {
|
|
1321
|
+
if (cut) truncated = true;
|
|
1322
|
+
};
|
|
1323
|
+
if (targeting.outcome === "NO_TARGET") {
|
|
1324
|
+
const record2 = { outcome: "NO_TARGET", targets: [], skippedCaseIds: [], codegenContext: [] };
|
|
1325
|
+
const reason = (targeting.reason ?? "").trim();
|
|
1326
|
+
if (reason.length > 0) {
|
|
1327
|
+
record2.noTargetReason = reason.slice(0, MAX_REASON_CHARS);
|
|
1328
|
+
if (reason.length > MAX_REASON_CHARS) record2.noTargetReasonTruncated = true;
|
|
1329
|
+
}
|
|
1330
|
+
return record2;
|
|
1331
|
+
}
|
|
1332
|
+
const targets = [];
|
|
1333
|
+
for (const t of targeting.targets) {
|
|
1334
|
+
if (targets.length >= MAX_TARGETS) {
|
|
1335
|
+
truncated = true;
|
|
1336
|
+
break;
|
|
1337
|
+
}
|
|
1338
|
+
const path = typeof t.path === "string" ? t.path.trim() : "";
|
|
1339
|
+
if (path.length === 0 || path.length > MAX_PATH_CHARS) {
|
|
1340
|
+
truncated = true;
|
|
1341
|
+
continue;
|
|
1342
|
+
}
|
|
1343
|
+
const contextPaths = clampList(t.contextPaths, MAX_CONTEXT_PATHS, MAX_PATH_CHARS);
|
|
1344
|
+
const automationIds = clampList(t.automationIds, MAX_IDS_PER_TARGET, MAX_ID_CHARS);
|
|
1345
|
+
note(contextPaths.cut);
|
|
1346
|
+
note(automationIds.cut);
|
|
1347
|
+
targets.push({ path, kind: t.kind, contextPaths: contextPaths.kept, automationIds: automationIds.kept });
|
|
1348
|
+
}
|
|
1349
|
+
const skipped = clampList(
|
|
1350
|
+
(targeting.skippedCases ?? []).map((c) => c.automationId),
|
|
1351
|
+
MAX_SKIPPED_CASES,
|
|
1352
|
+
MAX_ID_CHARS
|
|
1353
|
+
);
|
|
1354
|
+
note(skipped.cut);
|
|
1355
|
+
const codegenContext = [];
|
|
1356
|
+
for (const entry of contextLog) {
|
|
1357
|
+
if (codegenContext.length >= MAX_CONTEXT_RECORDS) {
|
|
1358
|
+
truncated = true;
|
|
1359
|
+
break;
|
|
1360
|
+
}
|
|
1361
|
+
const targetPath = typeof entry.targetPath === "string" ? entry.targetPath.trim() : "";
|
|
1362
|
+
if (targetPath.length === 0 || targetPath.length > MAX_PATH_CHARS) {
|
|
1363
|
+
truncated = true;
|
|
1364
|
+
continue;
|
|
1365
|
+
}
|
|
1366
|
+
const contextPaths = clampList(entry.contextPaths, MAX_CONTEXT_PATHS, MAX_PATH_CHARS);
|
|
1367
|
+
const addedPaths = clampList(entry.addedPaths, MAX_ADDED_PATHS, MAX_PATH_CHARS);
|
|
1368
|
+
const skippedPaths = clampList(
|
|
1369
|
+
(entry.skipped ?? []).map((s) => s.path),
|
|
1370
|
+
MAX_SKIPPED_CONTEXT,
|
|
1371
|
+
MAX_PATH_CHARS
|
|
1372
|
+
);
|
|
1373
|
+
note(contextPaths.cut);
|
|
1374
|
+
note(addedPaths.cut);
|
|
1375
|
+
note(skippedPaths.cut);
|
|
1376
|
+
const reasonByPath = new Map((entry.skipped ?? []).map((s) => [s.path.trim(), s.reason]));
|
|
1377
|
+
codegenContext.push({
|
|
1378
|
+
targetPath,
|
|
1379
|
+
contextPaths: contextPaths.kept,
|
|
1380
|
+
addedPaths: addedPaths.kept,
|
|
1381
|
+
skipped: skippedPaths.kept.map((path) => ({ path, reason: String(reasonByPath.get(path) ?? "skipped").slice(0, 40) })),
|
|
1382
|
+
...entry.retriedWithoutAdded ? { retriedWithoutAdded: true } : {}
|
|
1383
|
+
});
|
|
1384
|
+
}
|
|
1385
|
+
const record = {
|
|
1386
|
+
outcome: "TARGETED",
|
|
1387
|
+
targets,
|
|
1388
|
+
skippedCaseIds: skipped.kept,
|
|
1389
|
+
codegenContext
|
|
1390
|
+
};
|
|
1391
|
+
if (truncated) record.truncated = true;
|
|
1392
|
+
return record;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1272
1395
|
// src/testPlacementCheck.ts
|
|
1273
1396
|
function isDiscovered(targetPath, discoveredFiles) {
|
|
1274
1397
|
const normalized = targetPath.replace(/\\/g, "/");
|
|
@@ -1335,6 +1458,8 @@ async function run(env2) {
|
|
|
1335
1458
|
let warnings = [];
|
|
1336
1459
|
let targeted = false;
|
|
1337
1460
|
let generatedFilePaths = [];
|
|
1461
|
+
let targetingResult;
|
|
1462
|
+
const contextLog = [];
|
|
1338
1463
|
try {
|
|
1339
1464
|
runToken = await exchangeForRunToken(env2.tokenUrl, await fetchGithubOidcToken());
|
|
1340
1465
|
packet = await fetchPacket(env2.packetUrl, runToken, env2.githubRunId);
|
|
@@ -1349,6 +1474,7 @@ async function run(env2) {
|
|
|
1349
1474
|
packet.guardrails.deniedPaths,
|
|
1350
1475
|
packet.testDiscovery
|
|
1351
1476
|
);
|
|
1477
|
+
targetingResult = targeting;
|
|
1352
1478
|
if (targeting.outcome === "NO_TARGET") {
|
|
1353
1479
|
writePhase0Marker(env2.runId);
|
|
1354
1480
|
runCommand(packet.commands.setup, "Setup");
|
|
@@ -1392,7 +1518,8 @@ async function run(env2) {
|
|
|
1392
1518
|
packet.requiredTests,
|
|
1393
1519
|
packet.guardrails.deniedPaths,
|
|
1394
1520
|
judgedByPool,
|
|
1395
|
-
warnings
|
|
1521
|
+
warnings,
|
|
1522
|
+
contextLog
|
|
1396
1523
|
)
|
|
1397
1524
|
]);
|
|
1398
1525
|
const depCheck = checkImportDependencies(
|
|
@@ -1486,7 +1613,9 @@ async function run(env2) {
|
|
|
1486
1613
|
runToken,
|
|
1487
1614
|
status: hardFailure ? "failed" : "completed",
|
|
1488
1615
|
branch,
|
|
1489
|
-
warnings
|
|
1616
|
+
warnings,
|
|
1617
|
+
// Omitted when targeting itself never produced a result (nothing exists to record).
|
|
1618
|
+
targeting: targetingResult ? buildTargetingRecord(targetingResult, contextLog) : void 0
|
|
1490
1619
|
});
|
|
1491
1620
|
return { hardFailure };
|
|
1492
1621
|
}
|