@rightkit/release 0.6.5 → 0.6.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 +79 -7
- package/android-play-upload.mjs +4 -4
- package/app-check.mjs +474 -17
- package/apple-profile-create.mjs +4 -3
- package/apple-profile-renew.mjs +5 -4
- package/bin/darwin/rightkit-swap +0 -0
- package/bin/rightkit-gate.mjs +431 -44
- package/binary-license-notices.mjs +2 -1
- package/build-release.mjs +7 -6
- package/cache-command.mjs +4 -3
- package/cargo-license-notices.mjs +2 -1
- package/cargo-manifest-text.mjs +25 -0
- package/cargo-profile-contract.mjs +26 -13
- package/cli/right-release.mjs +8 -1
- package/command-spec.mjs +4 -5
- package/deps.mjs +5 -4
- package/deterministic-scans.mjs +280 -32
- package/development.mjs +5 -5
- package/github-release.mjs +5 -3
- package/github-upload-gate.mjs +6 -6
- package/init-command.mjs +134 -8
- package/ios-upload.mjs +4 -4
- package/mirror-root-artifact.mjs +3 -4
- package/npm-license-notices.mjs +2 -1
- package/outdated-command.mjs +91 -0
- package/package.json +37 -1
- package/pipeline-admission.mjs +12 -0
- package/publish-cargo.mjs +215 -3
- package/publish-update.mjs +5 -3
- package/release-config.mjs +36 -5
- package/release-dependencies-command.mjs +4 -3
- package/release-qa-audit.mjs +12 -10
- package/release-qa-command.mjs +42 -7
- package/release-qa-hardening.mjs +10 -3
- package/release-qa-policy.mjs +214 -47
- package/release.mjs +4 -4
- package/rightkit-registry-resolver.mjs +5 -2
- package/rightkit-versions.json +11 -8
- package/stage-sidecar-command.mjs +5 -5
- package/test-command.mjs +3 -2
- package/upload-release.mjs +3 -2
- package/web-build.mjs +2 -1
package/app-check.mjs
CHANGED
|
@@ -16,13 +16,16 @@
|
|
|
16
16
|
// qa-platform G13 a Rust `"platform:<x>"` requirement that is not darwin or windows, the words rightkit-qa emits (it skips forever)
|
|
17
17
|
// test-harness G14 a Cargo.toml [[test]] whose file defines `fn main(` but lacks `harness = false` (libtest ignores it: 0 tests)
|
|
18
18
|
// scenario-skip G15 a `.scenario(` test that prints SKIP and returns: a skip that reports green
|
|
19
|
-
// release-config G16 right-release.config.mjs: a provider publish-update.mjs refuses, an updater keyFile, an
|
|
19
|
+
// release-config G16 right-release.config.json (or a legacy .mjs): a provider publish-update.mjs refuses, an updater keyFile, an
|
|
20
|
+
// undefined Cargo profile, JSON right-release cannot load
|
|
20
21
|
// skip-checks G17 a package.json script that runs right-release with --skip-checks
|
|
21
22
|
// qa-dev-server G18 a package.json qa* script that runs `tauri dev` (QA would test the dev server, not the app)
|
|
22
23
|
// ffmpeg-release-escape G19 `allow_in_release_builds` in non-test Rust outside crates/rightkit-ffmpeg
|
|
23
|
-
// switches-inventory G20 a right-release.config.mjs directory without a switches.json (or its releaseSwitches.inventory)
|
|
24
|
+
// switches-inventory G20 a right-release.config.json (or .mjs) directory without a switches.json (or its releaseSwitches.inventory)
|
|
24
25
|
// speech-link G21 `import Speech` anywhere under swift/RightKitSwift/Sources or Tests (Apple Speech is removed, rightkit#978;
|
|
25
26
|
// the LinkProbes control fixture is the one sanctioned import)
|
|
27
|
+
// e2e-under-cargo-test G22 `cargo test|run|nextest` (also as `rustright cargo`, or the retired `rightkit`) that runs an end-to-end
|
|
28
|
+
// crate (rightkit-qa driver modules) in the broker without --no-run: it holds the compile slot for the whole run
|
|
26
29
|
//
|
|
27
30
|
// Ratchet: findings that exist today may be listed so they print as KNOWN and do not fail; an unlisted finding fails; a listed
|
|
28
31
|
// identity (`${rule}|${file}|${subject}`, no line numbers) that is gone fails as STALE, so the list only shrinks. The baseline is
|
|
@@ -178,7 +181,7 @@ function fileUrls({ root, files }) {
|
|
|
178
181
|
return out;
|
|
179
182
|
}
|
|
180
183
|
|
|
181
|
-
// ---- G13-
|
|
184
|
+
// ---- G13-G22: app defects found from files alone ----------------------------------------------------------------------------
|
|
182
185
|
// Each finding is { problem, fix, file, subject }; `${rule}|${file}|${subject}` (no line numbers, so an edit above it does not
|
|
183
186
|
// change it) is its identity in ci/app-check-baseline.json.
|
|
184
187
|
const SKIPPED_TREE = /(^|\/)(node_modules|vendor|target)\//;
|
|
@@ -187,7 +190,17 @@ const lineOf = (text, offset) => text.slice(0, offset).split("\n").length;
|
|
|
187
190
|
/** `text` with every whole-line `//` comment blanked, offsets and line numbers kept (a URL inside a string is untouched). */
|
|
188
191
|
const blankComments = (text) => text.replace(/^[ \t]*\/\/.*$/gm, (m) => " ".repeat(m.length));
|
|
189
192
|
const rustFiles = (files) => files.filter((f) => f.endsWith(".rs") && scanned(f));
|
|
190
|
-
|
|
193
|
+
// The release config: the data config (right-release.config.json, M24) or a legacy module (right-release.config.mjs) not yet moved.
|
|
194
|
+
const configFiles = (files) => files.filter((f) => /(^|\/)right-release\.config\.(?:mjs|json)$/.test(f) && scanned(f));
|
|
195
|
+
const isJsonConfig = (file) => file.endsWith(".json");
|
|
196
|
+
/** Every `[key, value, parentKey]` of a parsed JSON value, depth first (the walk the JSON release config rules read). */
|
|
197
|
+
function* jsonEntries(node, parentKey = null) {
|
|
198
|
+
if (Array.isArray(node)) { for (const item of node) yield* jsonEntries(item, parentKey); return; }
|
|
199
|
+
if (!node || typeof node !== "object") return;
|
|
200
|
+
for (const [key, value] of Object.entries(node)) { yield [key, value, parentKey]; yield* jsonEntries(value, key); }
|
|
201
|
+
}
|
|
202
|
+
/** The offset of the first `"key"` in JSON text that is followed by `:` (a key, not a string value), else 0. */
|
|
203
|
+
const jsonKeyOffset = (text, key) => Math.max(0, text.search(new RegExp(`"${key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}"\\s*:`)));
|
|
191
204
|
const dirOf = (file) => path.posix.dirname(file);
|
|
192
205
|
|
|
193
206
|
// G13: rightkit-qa's util::platform() emits "darwin" or "windows"; a requirement spelled any other way never matches, so the
|
|
@@ -303,19 +316,33 @@ function releaseConfig({ root, files }) {
|
|
|
303
316
|
for (const file of configFiles(files)) {
|
|
304
317
|
const raw = readText(root, file);
|
|
305
318
|
if (raw === null) continue;
|
|
306
|
-
const text = blankComments(raw);
|
|
319
|
+
const text = isJsonConfig(file) ? raw : blankComments(raw);
|
|
307
320
|
const add = (kind, value, offset, why, fix) => out.push({ file, subject: `${kind}:${value}`, problem: `${file}:${lineOf(text, offset)} ${why}`, fix });
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
321
|
+
const badProvider = (provider, offset) => add("provider", provider, offset, `distribution provider "${provider}" is refused by packages/release/publish-update.mjs (it takes r2-rightapps or github-releases)`, 'set provider: "r2-rightapps" (private) or "github-releases" (public)');
|
|
322
|
+
const keyFile = (value, offset) => add("keyFile", value.slice(0, 60), offset, "updater keyFile: sign-updater.mjs refuses --key-file; updater keys come from the OS credential provider", "remove keyFile; the updater key is read from the credential store");
|
|
323
|
+
const profiles = [];
|
|
324
|
+
if (isJsonConfig(file)) {
|
|
325
|
+
let config;
|
|
326
|
+
try { config = JSON.parse(raw.replace(/^\uFEFF/, "")); } catch (error) {
|
|
327
|
+
add("json", "invalid", 0, `is not valid JSON (${error.message}): right-release refuses to load it`, "fix the JSON; the release config is strict JSON (no comments or trailing commas)");
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
for (const [key, value, parent] of jsonEntries(config)) {
|
|
331
|
+
if (key === "provider" && parent === "distribution" && typeof value === "string" && !RELEASE_PROVIDERS.has(value)) badProvider(value, text.indexOf(JSON.stringify(value)));
|
|
332
|
+
if (key === "keyFile" && parent === "updater") keyFile(JSON.stringify(value), jsonKeyOffset(text, "keyFile"));
|
|
333
|
+
if (key === "profile" && (parent === "winPackage" || parent === "macPackage") && typeof value === "string") profiles.push([value, text.indexOf(JSON.stringify(value))]);
|
|
334
|
+
}
|
|
335
|
+
} else {
|
|
336
|
+
for (const m of text.matchAll(/\bprovider\s*:\s*(["'`])([^"'`]+)\1/g)) if (!RELEASE_PROVIDERS.has(m[2])) badProvider(m[2], m.index);
|
|
337
|
+
for (const m of text.matchAll(/\bupdater\s*:\s*\{/g)) {
|
|
338
|
+
const block = braceBlock(text, m.index + m[0].length - 1);
|
|
339
|
+
const key = /\bkeyFile\s*:\s*([^,\n}]+)/.exec(block);
|
|
340
|
+
if (key) keyFile(key[1].trim(), m.index + key.index);
|
|
341
|
+
}
|
|
315
342
|
}
|
|
316
|
-
|
|
343
|
+
profiles.push(...[...text.matchAll(/(["'])--profile\1\s*,\s*(["'])([^"']+)\2/g)].map((m) => [m[3], m.index])
|
|
317
344
|
.concat([...text.matchAll(/(["'])--profile=([^"']+)\1/g)].map((m) => [m[2], m.index]))
|
|
318
|
-
.concat([...text.matchAll(/\bPROFILE\s*(?:\?\?|\|\|)\s*(["'])([^"']+)\1/g)].map((m) => [m[2], m.index]));
|
|
345
|
+
.concat([...text.matchAll(/\bPROFILE\s*(?:\?\?|\|\|)\s*(["'])([^"']+)\1/g)].map((m) => [m[2], m.index])));
|
|
319
346
|
if (profiles.length) {
|
|
320
347
|
const defined = profilesFor(root, files, dirOf(file));
|
|
321
348
|
for (const [name, offset] of profiles) {
|
|
@@ -374,10 +401,20 @@ function switchesInventory({ root, files }) {
|
|
|
374
401
|
for (const file of configFiles(files)) {
|
|
375
402
|
const raw = readText(root, file);
|
|
376
403
|
if (raw === null) continue;
|
|
377
|
-
const text = blankComments(raw);
|
|
404
|
+
const text = isJsonConfig(file) ? raw : blankComments(raw);
|
|
378
405
|
const dir = dirOf(file);
|
|
379
|
-
|
|
380
|
-
|
|
406
|
+
let block;
|
|
407
|
+
let named;
|
|
408
|
+
if (isJsonConfig(file)) {
|
|
409
|
+
let config;
|
|
410
|
+
try { config = JSON.parse(raw.replace(/^\uFEFF/, "")); } catch { config = null; }
|
|
411
|
+
const switches = config?.releaseSwitches;
|
|
412
|
+
if (switches && typeof switches === "object") block = { index: jsonKeyOffset(text, "releaseSwitches") };
|
|
413
|
+
if (typeof switches?.inventory === "string") named = switches.inventory;
|
|
414
|
+
} else {
|
|
415
|
+
block = /\breleaseSwitches\s*:\s*\{/.exec(text);
|
|
416
|
+
named = block ? /\binventory\s*:\s*(["'`])([^"'`]+)\1/.exec(braceBlock(text, block.index + block[0].length - 1))?.[2] : undefined;
|
|
417
|
+
}
|
|
381
418
|
const inventory = path.posix.normalize(path.posix.join(dir, named ?? "switches.json"));
|
|
382
419
|
if (fs.existsSync(path.join(root, inventory))) continue;
|
|
383
420
|
out.push({ file, subject: dir, problem: `${file}:${block ? lineOf(text, block.index) : 1} has no switches inventory (${inventory} does not exist)`, fix: named ? `commit ${inventory} or point releaseSwitches.inventory at the app's inventory` : `add ${path.posix.join(dir, "switches.json")} listing the app's QA and dev switches (see packages/release/fixtures/release-switches/app/switches.json)` });
|
|
@@ -403,6 +440,424 @@ function speechLink({ root, files }) {
|
|
|
403
440
|
return out;
|
|
404
441
|
}
|
|
405
442
|
|
|
443
|
+
// G22: the broker compiles; it does not run end-to-end tests. `cargo test`, `cargo run` and `cargo nextest` (plain, through the
|
|
444
|
+
// RustRight client as `rustright cargo`, or as `rightkit` while the retired alias lasts) run the test binary or the program inside the
|
|
445
|
+
// broker job, which holds the compile slot for the whole run (Pulse, 2026-10-23: 55 minutes with 15 builds queued) and has no GUI
|
|
446
|
+
// session or unscrubbed environment a real-app test needs. The fix is `--no-run` to compile and `rightkit-qa run-tests` to run
|
|
447
|
+
// outside the broker (crates/rightkit-qa/docs/run-tests.md). Found from files alone, in three steps:
|
|
448
|
+
// 1. the end-to-end crates: a Cargo package that depends on rightkit-qa not optionally (the optional dependency is the in-app control
|
|
449
|
+
// server behind a qa-native / dev-control feature) and has a .rs file, outside examples/, that names a driver module of it;
|
|
450
|
+
// 2. the commands: `cargo test|run|nextest` in a package.json script, a shell, PowerShell, batch, Node or Python script, a Makefile
|
|
451
|
+
// or justfile recipe, a GitHub workflow `run:` step, a git hook, or a code span of a runbook document;
|
|
452
|
+
// 3. the selection: a command that is not `--no-run` or a rightkit-qa run-tests / journeys run, and names an end-to-end crate by
|
|
453
|
+
// -p / --package, --manifest-path, --workspace / --all (a member of the workspace in the working directory), --test or --bin.
|
|
454
|
+
// Known limits, deliberate for an offline check: no cargo binary (a path dependency that cargo adds to a workspace implicitly is not
|
|
455
|
+
// a member here), a command assembled from variables is matched on its literal text only, a package.json script that calls another
|
|
456
|
+
// script is judged where the cargo text is, a cargo alias (`cargo t`) is not matched, and a bare `cargo test` selects nothing.
|
|
457
|
+
const E2E_DRIVER_MODULES = new Set(["harness", "control", "suite", "scenario", "engine", "in_app", "interactive", "page", "browser", "android"]);
|
|
458
|
+
const E2E_GUARD_TARGET = "qa_surface_guard"; // the one test target of an end-to-end crate that only runs the static guard
|
|
459
|
+
const E2E_NAME_ONLY = /-e2e$|^qa-/; // a name is not evidence: crates that match only this are listed for a reviewer by e2eCrates()
|
|
460
|
+
|
|
461
|
+
const unquote = (text) => String(text).replace(/^(["'])(.*)\1$/, "$2");
|
|
462
|
+
const escapeRegExp = (text) => String(text).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
463
|
+
/** A dotted TOML key split at the dots outside quotes: `target.'cfg(unix)'.dependencies` is three parts. */
|
|
464
|
+
const tomlPath = (text) => [...String(text).matchAll(/"([^"]*)"|'([^']*)'|([^\s.'"]+)/g)].map((m) => m[1] ?? m[2] ?? m[3]);
|
|
465
|
+
const tomlStrings = (text) => [...String(text).matchAll(/"([^"]*)"|'([^']*)'/g)].map((m) => m[1] ?? m[2]);
|
|
466
|
+
const uncomment = (line) => line.replace(/^\s*#.*$/, "").replace(/\s+#.*$/, "");
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* What the end-to-end check reads from a Cargo manifest, literal values only: the package name, whether it holds a [workspace] and its
|
|
470
|
+
* members and exclude, the dependencies of [dependencies], [dev-dependencies] and their [target.*] tables (inline, dotted-key and
|
|
471
|
+
* `[dependencies.name]` forms, each with optional, package and workspace), [workspace.dependencies] as key -> package, and the
|
|
472
|
+
* [[test]] and [[bin]] tables. Build dependencies are not dependencies of the tests.
|
|
473
|
+
*/
|
|
474
|
+
function parseCargoManifest(text) {
|
|
475
|
+
const manifest = { name: null, workspace: false, members: [], exclude: [], dependencies: [], workspaceDependencies: new Map(), tests: [], bins: [] };
|
|
476
|
+
const entries = new Map();
|
|
477
|
+
const dependency = (table, key) => {
|
|
478
|
+
const id = `${table}\0${key}`;
|
|
479
|
+
if (!entries.has(id)) { const entry = { key, optional: false, package: undefined, workspace: false }; entries.set(id, entry); manifest.dependencies.push(entry); }
|
|
480
|
+
return entries.get(id);
|
|
481
|
+
};
|
|
482
|
+
const setAttribute = (entry, attribute, value) => {
|
|
483
|
+
if (attribute === "optional") entry.optional = entry.optional || value === "true";
|
|
484
|
+
else if (attribute === "package") entry.package = tomlStrings(value)[0] ?? entry.package;
|
|
485
|
+
else if (attribute === "workspace") entry.workspace = entry.workspace || value === "true";
|
|
486
|
+
};
|
|
487
|
+
const setInline = (entry, value) => {
|
|
488
|
+
if (/\boptional\s*=\s*true\b/.test(value)) entry.optional = true;
|
|
489
|
+
const renamed = /\bpackage\s*=\s*(?:"([^"]+)"|'([^']+)')/.exec(value);
|
|
490
|
+
if (renamed) entry.package = renamed[1] ?? renamed[2];
|
|
491
|
+
if (/\bworkspace\s*=\s*true\b/.test(value)) entry.workspace = true;
|
|
492
|
+
};
|
|
493
|
+
const lines = String(text ?? "").split("\n");
|
|
494
|
+
let section = { kind: "other" };
|
|
495
|
+
for (let i = 0; i < lines.length; i++) {
|
|
496
|
+
const line = uncomment(lines[i]);
|
|
497
|
+
const header = /^\s*(\[\[?)\s*(.+?)\s*\]\]?\s*$/.exec(line);
|
|
498
|
+
if (header) {
|
|
499
|
+
const parts = tomlPath(header[2]);
|
|
500
|
+
if (header[1] === "[[") {
|
|
501
|
+
const item = parts[0] === "test" || parts[0] === "bin" ? { name: undefined, path: undefined } : null;
|
|
502
|
+
if (item) manifest[parts[0] === "test" ? "tests" : "bins"].push(item);
|
|
503
|
+
section = item ? { kind: "target", item } : { kind: "other" };
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
const at = parts[0] === "dependencies" || parts[0] === "dev-dependencies" ? 0 : parts[0] === "target" && (parts[2] === "dependencies" || parts[2] === "dev-dependencies") ? 2 : -1;
|
|
507
|
+
if (parts[0] === "package" && parts.length === 1) section = { kind: "package" };
|
|
508
|
+
else if (parts[0] === "workspace") {
|
|
509
|
+
manifest.workspace = true;
|
|
510
|
+
section = parts.length === 1 ? { kind: "workspace" } : parts[1] === "dependencies" ? (parts.length === 2 ? { kind: "workspace-dependencies" } : { kind: "workspace-dependency", key: parts[2] }) : { kind: "other" };
|
|
511
|
+
} else if (at >= 0) section = parts.length === at + 1 ? { kind: "dependencies", table: parts.join(".") } : { kind: "dependency", table: parts.slice(0, at + 1).join("."), key: parts[at + 1] };
|
|
512
|
+
else section = { kind: "other" };
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
const kv = /^\s*("[^"]+"|'[^']+'|[A-Za-z0-9_-]+)(?:\s*\.\s*("[^"]+"|'[^']+'|[A-Za-z0-9_-]+))?\s*=\s*(.*?)\s*$/.exec(line);
|
|
516
|
+
if (!kv) continue;
|
|
517
|
+
const key = unquote(kv[1]);
|
|
518
|
+
const attribute = kv[2] === undefined ? undefined : unquote(kv[2]);
|
|
519
|
+
const value = kv[3];
|
|
520
|
+
switch (section.kind) {
|
|
521
|
+
case "package":
|
|
522
|
+
if (key === "name" && attribute === undefined) manifest.name = tomlStrings(value)[0] ?? null;
|
|
523
|
+
break;
|
|
524
|
+
case "workspace":
|
|
525
|
+
if ((key === "members" || key === "exclude") && attribute === undefined) {
|
|
526
|
+
let list = value;
|
|
527
|
+
if (list.startsWith("[")) while (!list.includes("]") && i + 1 < lines.length) list += ` ${uncomment(lines[++i])}`;
|
|
528
|
+
manifest[key].push(...tomlStrings(list));
|
|
529
|
+
}
|
|
530
|
+
break;
|
|
531
|
+
case "target":
|
|
532
|
+
if ((key === "name" || key === "path") && attribute === undefined) section.item[key] = tomlStrings(value)[0];
|
|
533
|
+
break;
|
|
534
|
+
case "dependencies":
|
|
535
|
+
if (attribute === undefined) setInline(dependency(section.table, key), value); else setAttribute(dependency(section.table, key), attribute, value);
|
|
536
|
+
break;
|
|
537
|
+
case "dependency":
|
|
538
|
+
if (attribute === undefined) setAttribute(dependency(section.table, section.key), key, value);
|
|
539
|
+
break;
|
|
540
|
+
case "workspace-dependencies":
|
|
541
|
+
if (attribute === undefined) manifest.workspaceDependencies.set(key, /\bpackage\s*=\s*(?:"([^"]+)"|'([^']+)')/.exec(value)?.slice(1).find(Boolean) ?? key);
|
|
542
|
+
else if (attribute === "package") manifest.workspaceDependencies.set(key, tomlStrings(value)[0] ?? key);
|
|
543
|
+
break;
|
|
544
|
+
case "workspace-dependency":
|
|
545
|
+
if (key === "package" && attribute === undefined) manifest.workspaceDependencies.set(section.key, tomlStrings(value)[0] ?? section.key);
|
|
546
|
+
else if (!manifest.workspaceDependencies.has(section.key)) manifest.workspaceDependencies.set(section.key, section.key);
|
|
547
|
+
break;
|
|
548
|
+
default:
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
return manifest;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/** A `[workspace] members` pattern (`crates/*`, `apps/**`, `./x`) as a regular expression over a posix path; a pattern that is no
|
|
555
|
+
* valid glob matches nothing (the rule must not throw on a manifest cargo would refuse). */
|
|
556
|
+
function memberPattern(glob) {
|
|
557
|
+
const source = String(glob).replace(/^\.\//, "").replace(/\/+$/, "").split("**").map((part) => part.replace(/[.+^$(){}|\\]/g, "\\$&").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]")).join(".*");
|
|
558
|
+
try { return new RegExp(`^${source}$`); } catch { return /$^/; }
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/** True when the Rust source names a driver module of rightkit-qa through any of `names` (its crate identifier(s) in this package):
|
|
562
|
+
* `rightkit_qa::harness`, or a `use rightkit_qa::{...}` list with a driver module among its top-level items. Comments do not count. */
|
|
563
|
+
function namesDriverModule(source, names) {
|
|
564
|
+
const text = source.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, " ")).replace(/\/\/[^\n]*/g, "");
|
|
565
|
+
for (const name of names) {
|
|
566
|
+
for (const m of text.matchAll(new RegExp(`(?<!\\w)${escapeRegExp(name)}\\s*::\\s*(\\{|[A-Za-z_]\\w*)`, "g"))) {
|
|
567
|
+
if (m[1] !== "{") { if (E2E_DRIVER_MODULES.has(m[1])) return true; continue; }
|
|
568
|
+
const list = braceBlock(text, m.index + m[0].length - 1).slice(1, -1);
|
|
569
|
+
let depth = 0, item = "";
|
|
570
|
+
const items = [];
|
|
571
|
+
for (const ch of list) {
|
|
572
|
+
if (ch === "{") depth += 1; else if (ch === "}") depth -= 1;
|
|
573
|
+
if (ch === "," && depth === 0) { items.push(item); item = ""; } else item += ch;
|
|
574
|
+
}
|
|
575
|
+
items.push(item);
|
|
576
|
+
if (items.some((it) => E2E_DRIVER_MODULES.has(/^\s*([A-Za-z_]\w*)/.exec(it)?.[1]))) return true;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return false;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* The Cargo layout of a tree and the end-to-end crates in it. `crates` are the end-to-end ones as { name, dir, tests, bins } (a
|
|
584
|
+
* package that depends on rightkit-qa not optionally and has a non-example .rs file naming one of its driver modules); `nameOnly`
|
|
585
|
+
* lists packages named like one (`*-e2e`, `qa-*`) that are not, for a reviewer: a name is not evidence.
|
|
586
|
+
*/
|
|
587
|
+
export function e2eCrates(root, files) {
|
|
588
|
+
const manifests = new Map();
|
|
589
|
+
for (const file of files.filter((f) => /(^|\/)Cargo\.toml$/.test(f) && scanned(f))) {
|
|
590
|
+
const text = readText(root, file);
|
|
591
|
+
if (text !== null) manifests.set(dirOf(file), parseCargoManifest(text));
|
|
592
|
+
}
|
|
593
|
+
/** The package directory a file belongs to: the nearest directory above it with a Cargo.toml, when that holds a [package]. */
|
|
594
|
+
const ownerOf = (file) => {
|
|
595
|
+
for (let dir = dirOf(file); ; dir = dirOf(dir)) {
|
|
596
|
+
const manifest = manifests.get(dir);
|
|
597
|
+
if (manifest) return manifest.name === null ? null : dir;
|
|
598
|
+
if (dir === ".") return null;
|
|
599
|
+
}
|
|
600
|
+
};
|
|
601
|
+
const packageOf = (dep, dir) => {
|
|
602
|
+
if (dep.package) return dep.package;
|
|
603
|
+
if (dep.workspace) {
|
|
604
|
+
for (let d = dir; ; d = dirOf(d)) {
|
|
605
|
+
const inherited = manifests.get(d)?.workspaceDependencies.get(dep.key);
|
|
606
|
+
if (inherited) return inherited;
|
|
607
|
+
if (d === ".") break;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
return dep.key;
|
|
611
|
+
};
|
|
612
|
+
const identifiers = new Map(); // package dir -> how Rust code names rightkit-qa there (the key, with - as _)
|
|
613
|
+
for (const [dir, manifest] of manifests) {
|
|
614
|
+
if (manifest.name === null) continue;
|
|
615
|
+
const names = manifest.dependencies.filter((dep) => !dep.optional && packageOf(dep, dir) === "rightkit-qa").map((dep) => dep.key.replaceAll("-", "_"));
|
|
616
|
+
if (names.length) identifiers.set(dir, [...new Set(names)]);
|
|
617
|
+
}
|
|
618
|
+
const driving = new Set();
|
|
619
|
+
if (identifiers.size) {
|
|
620
|
+
for (const file of rustFiles(files)) {
|
|
621
|
+
const dir = ownerOf(file);
|
|
622
|
+
if (dir === null || !identifiers.has(dir) || driving.has(dir) || (dir === "." ? file : file.slice(dir.length + 1)).startsWith("examples/")) continue;
|
|
623
|
+
const text = readText(root, file);
|
|
624
|
+
if (text !== null && identifiers.get(dir).some((name) => text.includes(name)) && namesDriverModule(text, identifiers.get(dir))) driving.add(dir);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
const crates = [...driving].sort().map((dir) => {
|
|
628
|
+
const manifest = manifests.get(dir);
|
|
629
|
+
const within = (file) => (dir === "." ? file : file.startsWith(`${dir}/`) ? file.slice(dir.length + 1) : null);
|
|
630
|
+
const tests = new Set(manifest.tests.map((t) => t.name).filter(Boolean));
|
|
631
|
+
const bins = new Set(manifest.bins.map((b) => b.name).filter(Boolean));
|
|
632
|
+
for (const file of files) {
|
|
633
|
+
const rel = within(file);
|
|
634
|
+
if (rel === null || !scanned(file) || ownerOf(file) !== dir) continue;
|
|
635
|
+
const test = /^tests\/([^/]+)(?:\.rs|\/main\.rs)$/.exec(rel);
|
|
636
|
+
if (test) tests.add(test[1]);
|
|
637
|
+
const bin = /^src\/bin\/([^/]+)(?:\.rs|\/main\.rs)$/.exec(rel);
|
|
638
|
+
if (bin) bins.add(bin[1]);
|
|
639
|
+
if (rel === "src/main.rs") bins.add(manifest.name);
|
|
640
|
+
}
|
|
641
|
+
return { name: manifest.name, dir, tests, bins };
|
|
642
|
+
});
|
|
643
|
+
const nameOnly = [...manifests].filter(([dir, m]) => m.name !== null && E2E_NAME_ONLY.test(m.name) && !driving.has(dir)).map(([, m]) => m.name).sort();
|
|
644
|
+
/** The end-to-end crates that are members of the workspace rooted at `wsDir`: its root package, and every crate that matches a
|
|
645
|
+
* `members` glob, is not under `exclude` and is not a workspace root itself (a nested [workspace] makes a crate its own root). */
|
|
646
|
+
const membersOf = (wsDir) => {
|
|
647
|
+
const ws = manifests.get(wsDir);
|
|
648
|
+
if (!ws?.workspace) return [];
|
|
649
|
+
const included = ws.members.map(memberPattern);
|
|
650
|
+
const excluded = ws.exclude.map((x) => String(x).replace(/^\.\//, "").replace(/\/+$/, ""));
|
|
651
|
+
return crates.filter((c) => {
|
|
652
|
+
if (c.dir === wsDir) return true;
|
|
653
|
+
const rel = wsDir === "." ? c.dir : c.dir.startsWith(`${wsDir}/`) ? c.dir.slice(wsDir.length + 1) : null;
|
|
654
|
+
return rel !== null && !manifests.get(c.dir).workspace && included.some((re) => re.test(rel)) && !excluded.some((x) => rel === x || rel.startsWith(`${x}/`));
|
|
655
|
+
});
|
|
656
|
+
};
|
|
657
|
+
return { crates, nameOnly, manifests, membersOf };
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
const CARGO_COMMAND = /(?<![\w-])((?:(?:rustright|rightkit)\s+)?cargo)\s+(?:\+\S+\s+)?(test|run|nextest)\b([^&;|`\n]*)/g;
|
|
661
|
+
const CD_COMMAND = /(?<![\w./-])(?:cd|Set-Location)\s+(?:-(?:Path|LiteralPath)\s+)?("[^"]*"|'[^']*'|[^\s;&|)]+)/gi;
|
|
662
|
+
const COMMENT_LINE = /^\s*(?:#|\/\/|\/?\*|;|::|REM\b)/i;
|
|
663
|
+
const QA_RUNNER = /\brightkit-qa(?:\.exe)?\s+(?:run-tests|journeys\s+run)\b/;
|
|
664
|
+
const NEXTEST_NO_TESTS = new Set("list archive show-config self help".split(" ")); // nextest subcommands that run no test
|
|
665
|
+
const HISTORY_DOC = /^docs\/(?:archive|plans|provenance|handoff[^/]*)\/|(?:^|\/)(?:tasks|evidence)\/|(?:^|\/)(?:changelog|pending|status|state)\.md$/i;
|
|
666
|
+
const HOOK_FILE = /(?:^|\/)(?:\.githooks|\.husky|githooks)\/(?!_\/)[^/]+/;
|
|
667
|
+
const WORKFLOW_FILE = /^\.github\/(?:workflows\/[^/]+|actions\/.+\/action)\.ya?ml$/;
|
|
668
|
+
const BUILD_FILE = /(?:^|\/)(?:Makefile|makefile|GNUmakefile|[Jj]ustfile|\.justfile)$|\.mk$/;
|
|
669
|
+
const SCRIPT_FILE = /\.(?:sh|bash|zsh|ps1|psm1|bat|cmd|mjs|cjs|js|py)$/i;
|
|
670
|
+
|
|
671
|
+
/** A script's `cd` / `Set-Location` target applied to `cwd` (a posix path from the repository root). A target that is not a plain
|
|
672
|
+
* relative path (a variable, a substitution, an absolute path, one that leaves the repository) leaves the directory as it was: the
|
|
673
|
+
* `cd "$(dirname "$0")/.."` of a script under scripts/ is the repository root, where a script starts. */
|
|
674
|
+
function changeDirectory(cwd, target) {
|
|
675
|
+
const t = unquote(target);
|
|
676
|
+
if (!t || /[$`(%~]/.test(t) || /^([\\/]|[A-Za-z]:)/.test(t) || t === "-") return cwd;
|
|
677
|
+
const next = path.posix.normalize(path.posix.join(cwd, t.replaceAll("\\", "/")));
|
|
678
|
+
return next === ".." || next.startsWith("../") ? cwd : next;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/** The logical lines of physical `lines` ({ no, text }): comment lines dropped, and a line that ends with `continuation` joined to the
|
|
682
|
+
* next, numbered by the line the command starts on. */
|
|
683
|
+
function logicalLines(lines, continuation) {
|
|
684
|
+
const out = [];
|
|
685
|
+
let pending = null;
|
|
686
|
+
for (const line of lines) {
|
|
687
|
+
if (!pending && COMMENT_LINE.test(line.text)) continue;
|
|
688
|
+
const text = line.text.replace(/\s+$/, "");
|
|
689
|
+
const joined = pending ? `${pending.text} ${text.trim()}` : text;
|
|
690
|
+
const no = pending?.no ?? line.no;
|
|
691
|
+
if (continuation && text.endsWith(continuation)) { pending = { no, text: joined.slice(0, -continuation.length) }; continue; }
|
|
692
|
+
pending = null;
|
|
693
|
+
out.push({ no, text: joined });
|
|
694
|
+
}
|
|
695
|
+
if (pending) out.push(pending);
|
|
696
|
+
return out;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/** The `run:` steps of a GitHub workflow or composite action: { lines, cwd } where cwd is the step's working-directory (the
|
|
700
|
+
* repository root when it has none, or one that is an expression). */
|
|
701
|
+
function workflowSteps(text) {
|
|
702
|
+
const lines = text.split("\n");
|
|
703
|
+
const indent = (l) => l.length - l.trimStart().length;
|
|
704
|
+
const steps = [];
|
|
705
|
+
lines.forEach((raw, i) => {
|
|
706
|
+
const m = /^(\s*)(-\s+)?run:\s*(.*?)\s*$/.exec(raw);
|
|
707
|
+
if (!m) return;
|
|
708
|
+
const keyColumn = m[1].length + (m[2]?.length ?? 0);
|
|
709
|
+
const value = m[3];
|
|
710
|
+
let block;
|
|
711
|
+
if (/^[|>][+-]?\d*(?:\s+#.*)?$/.test(value)) {
|
|
712
|
+
block = [];
|
|
713
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
714
|
+
if (lines[j].trim() === "") continue;
|
|
715
|
+
if (indent(lines[j]) <= keyColumn) break;
|
|
716
|
+
block.push({ no: j + 1, text: lines[j].trim() });
|
|
717
|
+
}
|
|
718
|
+
// A folded scalar (`>`) is one line to the shell.
|
|
719
|
+
if (value.startsWith(">") && block.length) block = [{ no: block[0].no, text: block.map((l) => l.text).join(" ") }];
|
|
720
|
+
} else {
|
|
721
|
+
const plain = /^(["'])(.*)\1$/.test(value) ? unquote(value) : value.replace(/\s+#.*$/, "");
|
|
722
|
+
block = plain ? [{ no: i + 1, text: plain }] : [];
|
|
723
|
+
}
|
|
724
|
+
// The step's own working-directory sits beside `run:` (same column), on either side of it.
|
|
725
|
+
const sibling = (j) => { const k = /^(\s*)(-\s+)?working-directory:\s*(.*?)\s*$/.exec(lines[j]); return k && k[1].length + (k[2]?.length ?? 0) === keyColumn ? unquote(k[3].replace(/\s+#.*$/, "")) : null; };
|
|
726
|
+
let directory = null;
|
|
727
|
+
for (let j = i - 1; j >= 0 && directory === null; j--) {
|
|
728
|
+
if (lines[j].trim() === "") continue;
|
|
729
|
+
directory = sibling(j);
|
|
730
|
+
if (indent(lines[j]) < keyColumn) break;
|
|
731
|
+
}
|
|
732
|
+
for (let j = i + 1; j < lines.length && directory === null; j++) {
|
|
733
|
+
if (lines[j].trim() === "") continue;
|
|
734
|
+
if (indent(lines[j]) < keyColumn) break;
|
|
735
|
+
directory = sibling(j);
|
|
736
|
+
}
|
|
737
|
+
steps.push({ lines: block, cwd: directory === null ? "." : changeDirectory(".", directory) });
|
|
738
|
+
});
|
|
739
|
+
return steps;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/** The command-bearing units of a runbook document: each fenced code block (its lines) and each inline code span (one line). Prose
|
|
743
|
+
* outside code is never a command. */
|
|
744
|
+
function markdownUnits(text) {
|
|
745
|
+
const units = [];
|
|
746
|
+
let fence = null;
|
|
747
|
+
text.split("\n").forEach((raw, i) => {
|
|
748
|
+
if (fence) {
|
|
749
|
+
const close = /^\s*(`{3,}|~{3,})\s*$/.exec(raw);
|
|
750
|
+
if (close && close[1][0] === fence.char && close[1].length >= fence.length) { units.push(fence.lines); fence = null; } else fence.lines.push({ no: i + 1, text: raw });
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
const open = /^\s*(`{3,}|~{3,})/.exec(raw);
|
|
754
|
+
if (open) { fence = { char: open[1][0], length: open[1].length, lines: [] }; return; }
|
|
755
|
+
for (const span of raw.matchAll(/(`+)(.+?)\1(?!`)/g)) units.push([{ no: i + 1, text: span[2].trim() }]);
|
|
756
|
+
});
|
|
757
|
+
if (fence) units.push(fence.lines);
|
|
758
|
+
return units;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// G22: see the block comment above.
|
|
762
|
+
function e2eUnderCargoTest({ root, files }) {
|
|
763
|
+
const layout = e2eCrates(root, files);
|
|
764
|
+
if (!layout.crates.length) return [];
|
|
765
|
+
const out = [];
|
|
766
|
+
const seen = new Set();
|
|
767
|
+
const flagValues = (tokens, ...names) => tokens.flatMap((t, i) => names.flatMap((name) => (t === name ? (i + 1 < tokens.length ? [tokens[i + 1]] : []) : name.startsWith("--") && t.startsWith(`${name}=`) ? [t.slice(name.length + 1)] : [])));
|
|
768
|
+
/** The directory of the manifest a --manifest-path names (relative to the cwd, then to the repository root), when it holds one. */
|
|
769
|
+
const manifestDirectory = (value, cwd) => {
|
|
770
|
+
const base = value.replaceAll("\\", "/").replace(/(^|\/)Cargo\.toml$/, "") || ".";
|
|
771
|
+
if (path.posix.isAbsolute(base) || /^[A-Za-z]:/.test(base)) return null;
|
|
772
|
+
for (const from of [cwd, "."]) {
|
|
773
|
+
const dir = path.posix.normalize(path.posix.join(from, base));
|
|
774
|
+
if (dir !== ".." && !dir.startsWith("../") && layout.manifests.has(dir)) return dir;
|
|
775
|
+
}
|
|
776
|
+
return null;
|
|
777
|
+
};
|
|
778
|
+
/** The end-to-end crates a cargo command selects, each with the flag that did it: [{ crate, selector }], first rule first. */
|
|
779
|
+
const selected = (tokens, isRun, cwd) => {
|
|
780
|
+
const picks = new Map();
|
|
781
|
+
const pick = (crate, selector) => { if (!picks.has(crate)) picks.set(crate, selector); };
|
|
782
|
+
const packages = flagValues(tokens, "--package", "-p");
|
|
783
|
+
const manifestPaths = flagValues(tokens, "--manifest-path");
|
|
784
|
+
const tests = flagValues(tokens, "--test");
|
|
785
|
+
const wholeWorkspace = tokens.includes("--workspace") || tokens.includes("--all");
|
|
786
|
+
const excluded = new Set(flagValues(tokens, "--exclude"));
|
|
787
|
+
const guardOnly = tests.length > 0 && tests.every((t) => t === E2E_GUARD_TARGET); // `--test qa_surface_guard` runs the static guard only
|
|
788
|
+
const members = (dir) => layout.membersOf(dir).filter((c) => !excluded.has(c.name));
|
|
789
|
+
if (!guardOnly) {
|
|
790
|
+
for (const p of packages) for (const c of layout.crates.filter((crate) => crate.name === p)) pick(c, `-p ${p}`);
|
|
791
|
+
for (const mp of manifestPaths) {
|
|
792
|
+
const dir = manifestDirectory(mp, cwd);
|
|
793
|
+
if (dir === null) continue;
|
|
794
|
+
for (const c of layout.crates.filter((crate) => crate.dir === dir)) pick(c, `--manifest-path ${mp}`);
|
|
795
|
+
if (wholeWorkspace) for (const c of members(dir)) pick(c, `--manifest-path ${mp} ${tokens.includes("--workspace") ? "--workspace" : "--all"}`);
|
|
796
|
+
}
|
|
797
|
+
if (wholeWorkspace && !manifestPaths.length) for (const c of members(cwd)) pick(c, tokens.includes("--workspace") ? "--workspace" : "--all");
|
|
798
|
+
}
|
|
799
|
+
if (!packages.length && !manifestPaths.length) {
|
|
800
|
+
for (const t of tests) {
|
|
801
|
+
const owners = t === E2E_GUARD_TARGET ? [] : layout.crates.filter((c) => c.tests.has(t));
|
|
802
|
+
if (owners.length === 1) pick(owners[0], `--test ${t}`);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
if (isRun) for (const b of flagValues(tokens, "--bin")) for (const c of layout.crates.filter((crate) => crate.bins.has(b))) pick(c, `--bin ${b}`);
|
|
806
|
+
return [...picks].map(([crate, selector]) => ({ crate, selector }));
|
|
807
|
+
};
|
|
808
|
+
/** Scan logical lines ({ no, text }) that run in order from `cwd`; `what` names the place and `subject` overrides the crate name in the identity. */
|
|
809
|
+
const scan = (file, lines, cwd, what, subject) => {
|
|
810
|
+
for (const { no, text } of lines) {
|
|
811
|
+
if (!/cargo|\bcd\b|Set-Location/i.test(text)) continue; // a line with neither a command nor a directory change cannot matter
|
|
812
|
+
const events = [...text.matchAll(CD_COMMAND)].map((m) => ({ at: m.index, cd: m[1] })).concat([...text.matchAll(CARGO_COMMAND)].map((m) => ({ at: m.index, m })));
|
|
813
|
+
for (const event of events.sort((a, b) => a.at - b.at)) {
|
|
814
|
+
if (event.cd !== undefined) { cwd = changeDirectory(cwd, event.cd); continue; }
|
|
815
|
+
const [, client, verb] = event.m;
|
|
816
|
+
// A command inside a string literal ends at the quote that closes it: `run("rustright cargo test -p x")`.
|
|
817
|
+
let rest = event.m[3];
|
|
818
|
+
for (const quote of ['"', "'"]) {
|
|
819
|
+
if ((rest.split(quote).length - 1) % 2 === 1 && (text.slice(0, event.at).split(quote).length - 1) % 2 === 1) rest = rest.slice(0, rest.indexOf(quote));
|
|
820
|
+
}
|
|
821
|
+
const tokens = (rest.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) ?? []).map((t) => t.replace(/["']/g, "").replace(/\)+$/, ""));
|
|
822
|
+
const ownArguments = tokens.indexOf("--") < 0 ? tokens : tokens.slice(0, tokens.indexOf("--"));
|
|
823
|
+
const segment = text.slice(Math.max(text.lastIndexOf("&", event.at), text.lastIndexOf(";", event.at), text.lastIndexOf("|", event.at), text.lastIndexOf("`", event.at)) + 1, event.at + event.m[0].length);
|
|
824
|
+
if (ownArguments.includes("--no-run") || QA_RUNNER.test(segment)) continue;
|
|
825
|
+
if (ownArguments.some((t) => t === "--help" || t === "-h" || t === "--version" || t === "-V")) continue;
|
|
826
|
+
if (verb === "nextest" && NEXTEST_NO_TESTS.has(tokens[0])) continue;
|
|
827
|
+
for (const { crate, selector } of selected(ownArguments, verb === "run", cwd)) {
|
|
828
|
+
const key = `${file}\0${no}\0${crate.name}`;
|
|
829
|
+
if (seen.has(key)) continue;
|
|
830
|
+
seen.add(key);
|
|
831
|
+
const via = `${client.replace(/\s+/g, " ")} ${verb} ${selector}`;
|
|
832
|
+
out.push({ file, subject: subject ?? crate.name, problem: `${file}:${no} ${what} runs the end-to-end crate ${crate.name} under cargo test (${via}): the test binary runs in the broker's context and holds its compile slot for the whole run`, fix: `compile it with --no-run and run it outside the broker: rightkit-qa run-tests -p ${crate.name} [--test <target>] -- <libtest args>` });
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
};
|
|
837
|
+
for (const { file, text, manifest } of packageManifests(root, files)) {
|
|
838
|
+
for (const [name, command] of Object.entries(manifest.scripts)) {
|
|
839
|
+
if (typeof command === "string") scan(file, [{ no: scriptLine(text, name), text: command }], dirOf(file), `script "${name}"`, name);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
for (const file of files.filter(scanned)) {
|
|
843
|
+
const base = path.posix.basename(file);
|
|
844
|
+
const runbook = (file === "README.md" || file === "AGENTS.md" || file === "CLAUDE.md" || /^docs\/.+\.md$/.test(file)) && !HISTORY_DOC.test(file) && !/\d{4}-\d{2}-\d{2}/.test(base);
|
|
845
|
+
const workflow = WORKFLOW_FILE.test(file);
|
|
846
|
+
const hook = !workflow && HOOK_FILE.test(file);
|
|
847
|
+
const recipe = !workflow && !hook && BUILD_FILE.test(file);
|
|
848
|
+
const script = !workflow && !hook && !recipe && SCRIPT_FILE.test(file);
|
|
849
|
+
if (!(runbook || workflow || hook || recipe || script) || base === "package.json") continue;
|
|
850
|
+
const content = readText(root, file);
|
|
851
|
+
if (content === null || !/cargo/.test(content)) continue;
|
|
852
|
+
const continuation = /\.(?:ps1|psm1)$/i.test(file) ? "`" : /\.(?:bat|cmd)$/i.test(file) ? "^" : "\\";
|
|
853
|
+
const physical = content.split("\n").map((line, i) => ({ no: i + 1, text: line }));
|
|
854
|
+
if (workflow) for (const step of workflowSteps(content)) scan(file, logicalLines(step.lines, continuation), step.cwd, "workflow step");
|
|
855
|
+
else if (runbook) for (const unit of markdownUnits(content)) scan(file, logicalLines(unit, continuation), ".", "runbook command");
|
|
856
|
+
else scan(file, logicalLines(physical, continuation), ".", hook ? "git hook" : recipe ? "recipe" : "command");
|
|
857
|
+
}
|
|
858
|
+
return out;
|
|
859
|
+
}
|
|
860
|
+
|
|
406
861
|
export const RULES = Object.freeze([
|
|
407
862
|
{ id: "eol-policy", gap: "G04", run: eolPolicy },
|
|
408
863
|
{ id: "file-url", gap: "G12", run: fileUrls },
|
|
@@ -416,6 +871,7 @@ export const RULES = Object.freeze([
|
|
|
416
871
|
{ id: "ffmpeg-release-escape", gap: "G19", run: ffmpegReleaseEscape },
|
|
417
872
|
{ id: "switches-inventory", gap: "G20", run: switchesInventory },
|
|
418
873
|
{ id: "speech-link", gap: "G21", run: speechLink },
|
|
874
|
+
{ id: "e2e-under-cargo-test", gap: "G22", run: e2eUnderCargoTest },
|
|
419
875
|
]);
|
|
420
876
|
|
|
421
877
|
// ---- export: the committed files, by git alone ----------------------------------------------------------------------------------
|
|
@@ -740,6 +1196,7 @@ export const BUILD_REFUSES = Object.freeze(["switches-inventory"]);
|
|
|
740
1196
|
* classification test in app-check.test.mjs, so a new command cannot appear unclassified. */
|
|
741
1197
|
export const UNCHECKED_COMMANDS = new Map([
|
|
742
1198
|
["check", "is the check"],
|
|
1199
|
+
["update", "runs @rightkit/outdated over the app's RightKit pins (right-update); reads no release inputs and builds nothing"],
|
|
743
1200
|
["deps", "the shared dependency lane: reads and updates dependency pins, not the app's release inputs"],
|
|
744
1201
|
["hardening", "scans artifacts named on the command line; reads no source tree"],
|
|
745
1202
|
["harden", "alias of hardening"],
|
package/apple-profile-create.mjs
CHANGED
|
@@ -2,8 +2,9 @@ import { createHash, generateKeyPairSync, randomUUID, sign, X509Certificate } fr
|
|
|
2
2
|
import { chmodSync, copyFileSync, constants, existsSync, mkdirSync, mkdtempSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import { fileURLToPath
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { spawnSync } from "./background-process.mjs";
|
|
7
|
+
import { defaultReleaseConfigFile, importReleaseConfig } from "./release-config.mjs";
|
|
7
8
|
import { appleProfileAuth, decodeAppleProfile, findAppleSigningCertificate, signAppleApiJwt } from "./apple-profile-renew.mjs";
|
|
8
9
|
|
|
9
10
|
// right-release apple-profile-create: issue a Mac Development provisioning profile
|
|
@@ -452,10 +453,10 @@ export async function main(args = process.argv.slice(2), options = {}) {
|
|
|
452
453
|
return;
|
|
453
454
|
}
|
|
454
455
|
const parsed = parseCreateArgs(args);
|
|
455
|
-
const configPath = parsed.configPath ??
|
|
456
|
+
const configPath = parsed.configPath ?? defaultReleaseConfigFile();
|
|
456
457
|
const absolute = path.resolve(configPath);
|
|
457
458
|
let config = {};
|
|
458
|
-
if (existsSync(absolute))
|
|
459
|
+
if (existsSync(absolute)) config = await importReleaseConfig(absolute, { lenient: true });
|
|
459
460
|
else if (parsed.configPath) fail("APPLE_PROFILE_USAGE", `Config file not found: ${parsed.configPath}`);
|
|
460
461
|
return createAppleProfile({ ...options, root: path.dirname(absolute), config, ...parsed });
|
|
461
462
|
}
|
package/apple-profile-renew.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { createHash, createPrivateKey, randomUUID, sign, X509Certificate } from "node:crypto";
|
|
2
2
|
import { constants, copyFileSync, existsSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, realpathSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { fileURLToPath
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { spawnSync } from "./background-process.mjs";
|
|
6
|
+
import { defaultReleaseConfigFile, importReleaseConfig } from "./release-config.mjs";
|
|
6
7
|
import { isManagedHost, refuseManagedEnvSecret } from "./managed-host-secrets.mjs";
|
|
7
8
|
import os from "node:os";
|
|
8
9
|
|
|
@@ -226,7 +227,7 @@ export async function main(args = process.argv.slice(2), options = {}) {
|
|
|
226
227
|
return;
|
|
227
228
|
}
|
|
228
229
|
if (args[0] !== "profile" || args[1] !== "renew") fail("APPLE_PROFILE_USAGE", "Expected: right-release apple profile renew");
|
|
229
|
-
let configPath =
|
|
230
|
+
let configPath = defaultReleaseConfigFile(), profilePath, dryRun = false;
|
|
230
231
|
for (let i = 2; i < args.length; i++) {
|
|
231
232
|
if (args[i] === "--dry-run") dryRun = true;
|
|
232
233
|
else if (["--config", "--profile"].includes(args[i])) {
|
|
@@ -236,8 +237,8 @@ export async function main(args = process.argv.slice(2), options = {}) {
|
|
|
236
237
|
} else fail("APPLE_PROFILE_USAGE", `Unknown option: ${args[i]}`);
|
|
237
238
|
}
|
|
238
239
|
const absolute = path.resolve(configPath);
|
|
239
|
-
const
|
|
240
|
-
return renewAppleProfile({ ...options, root: path.dirname(absolute), config
|
|
240
|
+
const config = await importReleaseConfig(absolute, { lenient: true });
|
|
241
|
+
return renewAppleProfile({ ...options, root: path.dirname(absolute), config, profilePath, dryRun });
|
|
241
242
|
}
|
|
242
243
|
|
|
243
244
|
const realOrSelf = (file) => { try { return realpathSync(file); } catch { return path.resolve(file); } };
|
package/bin/darwin/rightkit-swap
CHANGED
|
Binary file
|