mandrel-platform 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -12
- package/config/stryker.base.json +7 -2
- package/package.json +2 -2
- package/scripts/audit-check.mjs +331 -6
- package/scripts/audit-check.test.mjs +382 -1
- package/scripts/check-affected-mode.test.mjs +5 -51
- package/scripts/check-codeql-gating.test.mjs +649 -0
- package/scripts/check-destructive-migration.mjs +277 -11
- package/scripts/check-destructive-migration.test.mjs +334 -0
- package/scripts/check-environments-isolation-audit.test.mjs +212 -0
- package/scripts/check-fail-fast-attribution.test.mjs +90 -4
- package/scripts/check-first-party-pin-freshness.mjs +200 -22
- package/scripts/check-first-party-pin-freshness.test.mjs +296 -0
- package/scripts/check-gitleaks-allowlist.test.mjs +312 -0
- package/scripts/check-osv-scan-mode.test.mjs +5 -50
- package/scripts/check-release-type.mjs +591 -0
- package/scripts/check-release-type.test.mjs +678 -0
- package/scripts/check-setup-toolchain-store.test.mjs +139 -0
- package/scripts/check-toolchain-cache-default.test.mjs +308 -0
- package/scripts/lib/yaml-step.mjs +109 -0
- package/scripts/lib/yaml-step.test.mjs +156 -0
- package/scripts/osv-report-gate.test.mjs +289 -0
- package/scripts/osv-track-issue.test.mjs +155 -0
- package/scripts/stryker-base-config.test.mjs +256 -0
- package/scripts/track-issue.test.mjs +375 -0
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
|
|
31
31
|
import assert from "node:assert/strict";
|
|
32
32
|
import { test } from "node:test";
|
|
33
|
-
import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
|
|
33
|
+
import { mkdtempSync, writeFileSync, readFileSync, rmSync } from "node:fs";
|
|
34
34
|
import { tmpdir } from "node:os";
|
|
35
35
|
import { join } from "node:path";
|
|
36
36
|
|
|
@@ -43,6 +43,9 @@ import {
|
|
|
43
43
|
parseArgs,
|
|
44
44
|
loadAllowlist,
|
|
45
45
|
runCli,
|
|
46
|
+
isBoundedOverride,
|
|
47
|
+
findUnboundedOverrides,
|
|
48
|
+
lintOverrides,
|
|
46
49
|
} from "./audit-check.mjs";
|
|
47
50
|
|
|
48
51
|
const TODAY = "2026-07-02";
|
|
@@ -456,3 +459,381 @@ test("loadAllowlist: non-array throws", () => {
|
|
|
456
459
|
rmSync(dir, { recursive: true, force: true });
|
|
457
460
|
}
|
|
458
461
|
});
|
|
462
|
+
|
|
463
|
+
// ---------------------------------------------------------------------------
|
|
464
|
+
// Unbounded dependency overrides (Story #365)
|
|
465
|
+
//
|
|
466
|
+
// The failure these close: nothing lints for an unbounded override. Written as
|
|
467
|
+
// a bare lower bound it rewrites a dependent's range open-endedly, leaving the
|
|
468
|
+
// committed lockfile as the only pin — so any fresh resolution re-picks the
|
|
469
|
+
// newest release and can cross a major. Two consumers derived this rule
|
|
470
|
+
// independently after a major jump silently emptied a test suite, and both
|
|
471
|
+
// then found further already-escaped overrides.
|
|
472
|
+
// ---------------------------------------------------------------------------
|
|
473
|
+
|
|
474
|
+
test("isBoundedOverride: a bare lower bound is unbounded; a capped range is not", () => {
|
|
475
|
+
for (const unbounded of [">=1.2.3", "> 1.2.3", ">=0", "*", "x", "latest", "", " "]) {
|
|
476
|
+
assert.equal(isBoundedOverride(unbounded), false, `${JSON.stringify(unbounded)} is unbounded`);
|
|
477
|
+
}
|
|
478
|
+
for (const bounded of ["1.2.3", "^1.2.3", "~1.2.3", "1.2.x", ">=1.2.3 <2.0.0", "<2.0.0"]) {
|
|
479
|
+
assert.equal(isBoundedOverride(bounded), true, `${JSON.stringify(bounded)} is bounded`);
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
// The wildcard truth table (Story #375).
|
|
484
|
+
//
|
|
485
|
+
// The shipped regex enumerated only some wildcard spellings, so `x.x.x` and
|
|
486
|
+
// `x.x` — the exact shape the lint exists to catch, and what npm reads as
|
|
487
|
+
// plain `*` — fell through the catch-all and were reported bounded. The
|
|
488
|
+
// deciding question is the MAJOR position: a wildcard there is open to every
|
|
489
|
+
// future release, while a wildcard below it (`1.2.x`, `1.*`) stays inside its
|
|
490
|
+
// major and is genuinely bounded. Every spelling is enumerated so the next
|
|
491
|
+
// regex tweak has to keep answering all of them.
|
|
492
|
+
const WILDCARD_TRUTH_TABLE = [
|
|
493
|
+
["*", false],
|
|
494
|
+
["x", false],
|
|
495
|
+
["X", false],
|
|
496
|
+
["*.*", false],
|
|
497
|
+
["x.x", false],
|
|
498
|
+
["X.X", false],
|
|
499
|
+
["*.*.*", false],
|
|
500
|
+
["x.x.x", false],
|
|
501
|
+
["X.X.X", false],
|
|
502
|
+
["x.*", false],
|
|
503
|
+
["*.x.x", false],
|
|
504
|
+
["x.2.3", false],
|
|
505
|
+
["*.x", false],
|
|
506
|
+
["latest", false],
|
|
507
|
+
["next", false],
|
|
508
|
+
["LATEST", false],
|
|
509
|
+
["NEXT", false],
|
|
510
|
+
["1.x", true],
|
|
511
|
+
["1.X", true],
|
|
512
|
+
["1.*", true],
|
|
513
|
+
["1.2.x", true],
|
|
514
|
+
["1.2.*", true],
|
|
515
|
+
];
|
|
516
|
+
|
|
517
|
+
test("AC-1/AC-4: every wildcard spelling is judged on its major position", () => {
|
|
518
|
+
for (const [spec, expected] of WILDCARD_TRUTH_TABLE) {
|
|
519
|
+
assert.equal(
|
|
520
|
+
isBoundedOverride(spec),
|
|
521
|
+
expected,
|
|
522
|
+
`${JSON.stringify(spec)} should be bounded=${expected}`,
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
test("AC-2: a non-registry specifier expresses no upper bound", () => {
|
|
528
|
+
// Each of these re-resolves to whatever the source holds at install time —
|
|
529
|
+
// a branch head, a workspace sibling, a path — so none of them is a range
|
|
530
|
+
// the lint can call bounded.
|
|
531
|
+
for (const spec of [
|
|
532
|
+
"github:owner/repo",
|
|
533
|
+
"github:owner/repo#v1.2.3",
|
|
534
|
+
"git+https://github.com/owner/repo.git",
|
|
535
|
+
"git+ssh://git@github.com/owner/repo.git#main",
|
|
536
|
+
"git://github.com/owner/repo.git",
|
|
537
|
+
"workspace:*",
|
|
538
|
+
"workspace:^",
|
|
539
|
+
"file:../local-pkg",
|
|
540
|
+
"link:../local-pkg",
|
|
541
|
+
"https://example.com/pkg-1.2.3.tgz",
|
|
542
|
+
]) {
|
|
543
|
+
assert.equal(
|
|
544
|
+
isBoundedOverride(spec),
|
|
545
|
+
false,
|
|
546
|
+
`${JSON.stringify(spec)} pins nothing`,
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
test("AC-2: a git specifier carrying #semver: is judged on that range", () => {
|
|
552
|
+
assert.equal(
|
|
553
|
+
isBoundedOverride("git+https://github.com/owner/repo.git#semver:^1.2.3"),
|
|
554
|
+
true,
|
|
555
|
+
);
|
|
556
|
+
assert.equal(isBoundedOverride("github:owner/repo#semver:>=1.2.3"), false);
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
test("AC-3: a range carrying an explicit upper bound stays bounded", () => {
|
|
560
|
+
for (const bounded of [
|
|
561
|
+
"1.2.3",
|
|
562
|
+
"=1.2.3",
|
|
563
|
+
"^1.2.3",
|
|
564
|
+
"~1.2.3",
|
|
565
|
+
">=1.2.3 <2.0.0",
|
|
566
|
+
"<2.0.0",
|
|
567
|
+
"1.2.3 - 2.0.0",
|
|
568
|
+
]) {
|
|
569
|
+
assert.equal(
|
|
570
|
+
isBoundedOverride(bounded),
|
|
571
|
+
true,
|
|
572
|
+
`${JSON.stringify(bounded)} is bounded`,
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
test("AC-3: a wildcard LOWER end still counts as bounded when the range caps it", () => {
|
|
578
|
+
// The wildcard test asks about the major position, so it may only be applied
|
|
579
|
+
// to a single bare token. Applied to a compound range it read the first
|
|
580
|
+
// dot-segment of the whole string and called `x.x <2.0.0` unbounded — a
|
|
581
|
+
// range that plainly carries an upper bound — while the equivalent
|
|
582
|
+
// `* <2.0.0` passed, because its first segment is `* <2`. That
|
|
583
|
+
// self-inconsistency is the tell: it was an artifact of check ordering.
|
|
584
|
+
for (const bounded of [
|
|
585
|
+
"x.x <2.0.0",
|
|
586
|
+
"x.x.x <2.0.0",
|
|
587
|
+
"* <2.0.0",
|
|
588
|
+
"x.x.x - 2.0.0",
|
|
589
|
+
"x - 2.0.0",
|
|
590
|
+
"* - 2.0.0",
|
|
591
|
+
]) {
|
|
592
|
+
assert.equal(
|
|
593
|
+
isBoundedOverride(bounded),
|
|
594
|
+
true,
|
|
595
|
+
`${JSON.stringify(bounded)} is bounded`,
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
// Residual fail-opens left open by the dotted-wildcard fix. Both classes reach
|
|
601
|
+
// the catch-all `return true` and so read as bounded while pinning nothing.
|
|
602
|
+
//
|
|
603
|
+
// Class 1 — an operator in front of the wildcard. The wildcard test is gated on
|
|
604
|
+
// a single BARE token, so any leading range operator defeats it: `^x.x.x` is
|
|
605
|
+
// `^` applied to "any version", which is still any version.
|
|
606
|
+
const OPERATOR_PREFIXED_WILDCARDS = [
|
|
607
|
+
["^x.x.x", false],
|
|
608
|
+
["^x.x", false],
|
|
609
|
+
["^x", false],
|
|
610
|
+
["^*", false],
|
|
611
|
+
["~*", false],
|
|
612
|
+
["~x.x.x", false],
|
|
613
|
+
["=x.x.x", false],
|
|
614
|
+
["=*", false],
|
|
615
|
+
["=X.X", false],
|
|
616
|
+
["^latest", false],
|
|
617
|
+
// The same operators over a real version are untouched — this is the
|
|
618
|
+
// near-miss the fix must not break.
|
|
619
|
+
["^1.2.3", true],
|
|
620
|
+
["~1.2.3", true],
|
|
621
|
+
["=1.2.3", true],
|
|
622
|
+
["^1.x", true],
|
|
623
|
+
["~1.2.x", true],
|
|
624
|
+
];
|
|
625
|
+
|
|
626
|
+
test("operator-prefixed wildcards are unbounded, real versions unaffected", () => {
|
|
627
|
+
for (const [spec, expected] of OPERATOR_PREFIXED_WILDCARDS) {
|
|
628
|
+
assert.equal(
|
|
629
|
+
isBoundedOverride(spec),
|
|
630
|
+
expected,
|
|
631
|
+
`${JSON.stringify(spec)} should be bounded=${expected}`,
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
test("a compound whose every term is a wildcard or bare lower bound is unbounded", () => {
|
|
637
|
+
// Class 2 — a wildcard lower with no upper bound anywhere. `x.x >=1.0.0`
|
|
638
|
+
// reads as a range only because it is spelled like one; both terms are open
|
|
639
|
+
// above, so the resolved set is still "every future release".
|
|
640
|
+
for (const unbounded of [
|
|
641
|
+
"x.x >=1.0.0",
|
|
642
|
+
"x.x.x >=1.0.0",
|
|
643
|
+
"* >=1.0.0",
|
|
644
|
+
"x.x >1.0.0",
|
|
645
|
+
">=1.0.0 x.x",
|
|
646
|
+
"x.x - x.x",
|
|
647
|
+
"x.x.x - *",
|
|
648
|
+
"* - x",
|
|
649
|
+
"1.0.0 - x.x.x",
|
|
650
|
+
]) {
|
|
651
|
+
assert.equal(
|
|
652
|
+
isBoundedOverride(unbounded),
|
|
653
|
+
false,
|
|
654
|
+
`${JSON.stringify(unbounded)} carries no upper bound`,
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
test("a malformed hyphen range carrying a comparator still fails closed", () => {
|
|
660
|
+
// A real hyphen range takes plain versions on both sides. Reading the
|
|
661
|
+
// right-hand side of `>=1.0.0 - 2.0.0` as the cap answers a range npm never
|
|
662
|
+
// agreed to parse, and turns a spec that failed closed into one that passes
|
|
663
|
+
// — the exact fail-open direction this lint exists to prevent.
|
|
664
|
+
for (const unbounded of [">=1.0.0 - 2.0.0", ">=1.0.0 - ^2.0.0", ">1.0.0 - 2.0.0"]) {
|
|
665
|
+
assert.equal(
|
|
666
|
+
isBoundedOverride(unbounded),
|
|
667
|
+
false,
|
|
668
|
+
`${JSON.stringify(unbounded)} is not a hyphen range and must fail closed`,
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
test("a real upper bound still closes a range with a wildcard lower", () => {
|
|
674
|
+
// The regression guard for the trap the dotted-wildcard fix already hit once:
|
|
675
|
+
// testing the wildcard against the whole specifier called these unbounded.
|
|
676
|
+
// An upper bound is an upper bound regardless of how loose the lower end is.
|
|
677
|
+
for (const bounded of [
|
|
678
|
+
"x.x <2.0.0",
|
|
679
|
+
"x.x.x - 2.0.0",
|
|
680
|
+
"* <=2.0.0",
|
|
681
|
+
"x.x >=1.0.0 <2.0.0",
|
|
682
|
+
"^x.x.x - 2.0.0",
|
|
683
|
+
]) {
|
|
684
|
+
assert.equal(
|
|
685
|
+
isBoundedOverride(bounded),
|
|
686
|
+
true,
|
|
687
|
+
`${JSON.stringify(bounded)} carries a real upper bound`,
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
test("isBoundedOverride: a || union is only as bounded as its loosest arm", () => {
|
|
693
|
+
assert.equal(isBoundedOverride("^1.0.0 || ^2.0.0"), true);
|
|
694
|
+
assert.equal(isBoundedOverride("^1.0.0 || >=2.0.0"), false);
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
test("isBoundedOverride: an npm: alias is judged on the range it carries", () => {
|
|
698
|
+
assert.equal(isBoundedOverride("npm:other-pkg@^1.2.3"), true);
|
|
699
|
+
assert.equal(isBoundedOverride("npm:@scope/other@^1.2.3"), true);
|
|
700
|
+
assert.equal(isBoundedOverride("npm:other-pkg@>=1.2.3"), false);
|
|
701
|
+
// An alias with no range at all pins nothing.
|
|
702
|
+
assert.equal(isBoundedOverride("npm:other-pkg"), false);
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
test("AC-5: findUnboundedOverrides names the package and the bound", () => {
|
|
706
|
+
const findings = findUnboundedOverrides({
|
|
707
|
+
pnpm: { overrides: { "left-pad": ">=1.3.0", semver: "^7.5.2" } },
|
|
708
|
+
});
|
|
709
|
+
assert.equal(findings.length, 1);
|
|
710
|
+
assert.equal(findings[0].package, "left-pad");
|
|
711
|
+
assert.equal(findings[0].bound, ">=1.3.0");
|
|
712
|
+
assert.equal(findings[0].field, "pnpm.overrides");
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
test("AC-5: every override field is checked, npm and pnpm and yarn alike", () => {
|
|
716
|
+
const findings = findUnboundedOverrides({
|
|
717
|
+
overrides: { a: ">=1.0.0" },
|
|
718
|
+
resolutions: { b: "*" },
|
|
719
|
+
pnpm: { overrides: { c: ">2" } },
|
|
720
|
+
});
|
|
721
|
+
assert.deepEqual(
|
|
722
|
+
findings.map((f) => `${f.field}.${f.package}`).sort(),
|
|
723
|
+
["overrides.a", "pnpm.overrides.c", "resolutions.b"],
|
|
724
|
+
);
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
test("AC-5: a nested (dependent-scoped) override is named by its full path", () => {
|
|
728
|
+
const findings = findUnboundedOverrides({
|
|
729
|
+
overrides: { "some-dep": { "left-pad": ">=1.3.0" } },
|
|
730
|
+
});
|
|
731
|
+
assert.equal(findings.length, 1);
|
|
732
|
+
assert.equal(findings[0].package, "some-dep.left-pad");
|
|
733
|
+
assert.equal(findings[0].bound, ">=1.3.0");
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
test("AC-6: an override that pins a bounded range passes", () => {
|
|
737
|
+
assert.deepEqual(
|
|
738
|
+
findUnboundedOverrides({
|
|
739
|
+
overrides: { a: "^1.2.3", b: "~2.0.0", c: "3.1.4", d: ">=1.0.0 <2.0.0" },
|
|
740
|
+
pnpm: { overrides: { e: "1.2.x" } },
|
|
741
|
+
}),
|
|
742
|
+
[],
|
|
743
|
+
);
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
test("findUnboundedOverrides: a package.json with no overrides at all is clean", () => {
|
|
747
|
+
assert.deepEqual(findUnboundedOverrides({ name: "x", dependencies: { a: ">=1.0.0" } }), []);
|
|
748
|
+
assert.deepEqual(findUnboundedOverrides(null), []);
|
|
749
|
+
assert.deepEqual(findUnboundedOverrides("not-an-object"), []);
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
test("AC-5: runCli fails on an unbounded override before pnpm ever runs", () => {
|
|
753
|
+
const dir = mkdtempSync(join(tmpdir(), "audit-check-override-"));
|
|
754
|
+
try {
|
|
755
|
+
const packageJsonPath = join(dir, "package.json");
|
|
756
|
+
writeFileSync(
|
|
757
|
+
packageJsonPath,
|
|
758
|
+
JSON.stringify({ name: "fixture", pnpm: { overrides: { "left-pad": ">=1.3.0" } } }),
|
|
759
|
+
);
|
|
760
|
+
// An absent allowlist keeps this case about the override and nothing else.
|
|
761
|
+
const exit = runCli([
|
|
762
|
+
"--package-json",
|
|
763
|
+
packageJsonPath,
|
|
764
|
+
"--allowlist",
|
|
765
|
+
join(dir, "no-such-allowlist.json"),
|
|
766
|
+
]);
|
|
767
|
+
assert.equal(exit, 1);
|
|
768
|
+
} finally {
|
|
769
|
+
rmSync(dir, { recursive: true, force: true });
|
|
770
|
+
}
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
test("AC-6: the override gate passes a bounded override, reaching the audit", () => {
|
|
774
|
+
// Executes the clean path rather than asserting around it. lintOverrides is
|
|
775
|
+
// the gate runCli delegates to, split out precisely so a PASS is provable
|
|
776
|
+
// without `pnpm audit` (which needs a real lockfile and a network).
|
|
777
|
+
const dir = mkdtempSync(join(tmpdir(), "audit-check-bounded-"));
|
|
778
|
+
try {
|
|
779
|
+
const packageJsonPath = join(dir, "package.json");
|
|
780
|
+
writeFileSync(
|
|
781
|
+
packageJsonPath,
|
|
782
|
+
JSON.stringify({
|
|
783
|
+
name: "fixture",
|
|
784
|
+
overrides: { a: "^1.3.0", b: ">=1.0.0 <2.0.0" },
|
|
785
|
+
pnpm: { overrides: { "left-pad": "~1.3.0" } },
|
|
786
|
+
}),
|
|
787
|
+
);
|
|
788
|
+
assert.equal(lintOverrides(packageJsonPath), 0);
|
|
789
|
+
assert.equal(parseArgs(["--package-json", packageJsonPath]).packageJsonPath, packageJsonPath);
|
|
790
|
+
} finally {
|
|
791
|
+
rmSync(dir, { recursive: true, force: true });
|
|
792
|
+
}
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
test("AC-5: the override gate blocks an unbounded override", () => {
|
|
796
|
+
const dir = mkdtempSync(join(tmpdir(), "audit-check-lint-"));
|
|
797
|
+
try {
|
|
798
|
+
const packageJsonPath = join(dir, "package.json");
|
|
799
|
+
writeFileSync(packageJsonPath, JSON.stringify({ overrides: { "left-pad": ">=1.3.0" } }));
|
|
800
|
+
assert.equal(lintOverrides(packageJsonPath), 1);
|
|
801
|
+
} finally {
|
|
802
|
+
rmSync(dir, { recursive: true, force: true });
|
|
803
|
+
}
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
test("the override gate is a no-op when there is no package.json to read", () => {
|
|
807
|
+
assert.equal(lintOverrides(join(tmpdir(), "audit-check-absent-xyz", "package.json")), 0);
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
test("this repo's own package.json passes the override gate", () => {
|
|
811
|
+
// The lint ships enabled by default; a false positive here would red the
|
|
812
|
+
// platform's own required check on every PR.
|
|
813
|
+
assert.deepEqual(findUnboundedOverrides(JSON.parse(readFileSync("package.json", "utf8"))), []);
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
test("runCli: an unparseable package.json is a hard error, not a skipped check", () => {
|
|
817
|
+
const dir = mkdtempSync(join(tmpdir(), "audit-check-badpkg-"));
|
|
818
|
+
try {
|
|
819
|
+
const packageJsonPath = join(dir, "package.json");
|
|
820
|
+
writeFileSync(packageJsonPath, "{ not json");
|
|
821
|
+
assert.equal(
|
|
822
|
+
runCli([
|
|
823
|
+
"--package-json",
|
|
824
|
+
packageJsonPath,
|
|
825
|
+
"--allowlist",
|
|
826
|
+
join(dir, "no-such-allowlist.json"),
|
|
827
|
+
]),
|
|
828
|
+
1,
|
|
829
|
+
);
|
|
830
|
+
} finally {
|
|
831
|
+
rmSync(dir, { recursive: true, force: true });
|
|
832
|
+
}
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
test("parseArgs defaults the package.json path alongside the allowlist path", () => {
|
|
836
|
+
const { allowlistPath, packageJsonPath } = parseArgs([], "/tmp/proj");
|
|
837
|
+
assert.equal(allowlistPath, "/tmp/proj/audit-allowlist.json");
|
|
838
|
+
assert.equal(packageJsonPath, "/tmp/proj/package.json");
|
|
839
|
+
});
|
|
@@ -40,45 +40,18 @@ import { join, resolve, dirname } from "node:path";
|
|
|
40
40
|
import { fileURLToPath } from "node:url";
|
|
41
41
|
import { tmpdir } from "node:os";
|
|
42
42
|
|
|
43
|
+
import { stepByName, runScript } from "./lib/yaml-step.mjs";
|
|
44
|
+
|
|
43
45
|
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
44
46
|
const WORKFLOW = ".github/workflows/pr-quality.yml";
|
|
45
47
|
const content = readFileSync(join(repoRoot, WORKFLOW), "utf8");
|
|
46
48
|
|
|
47
49
|
// ---------------------------------------------------------------------------
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
50
|
+
// Local extraction helper. Step-block and `run:` extraction are shared via
|
|
51
|
+
// scripts/lib/yaml-step.mjs; only the `if:` reader is specific to this suite,
|
|
52
|
+
// which is the only one asserting on step gating.
|
|
51
53
|
// ---------------------------------------------------------------------------
|
|
52
54
|
|
|
53
|
-
function stepByName(text, name) {
|
|
54
|
-
const lines = text.split("\n");
|
|
55
|
-
const nameIdx = lines.findIndex((l) => /^\s+(- )?name:\s/.test(l) && l.includes(name));
|
|
56
|
-
assert.notEqual(nameIdx, -1, `step "${name}" not found`);
|
|
57
|
-
let start = -1;
|
|
58
|
-
for (let i = nameIdx; i >= 0; i--) {
|
|
59
|
-
if (/^\s*-\s/.test(lines[i])) {
|
|
60
|
-
start = i;
|
|
61
|
-
break;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
assert.notEqual(start, -1, `opening bullet for step "${name}" not found`);
|
|
65
|
-
const bulletIndent = lines[start].match(/^(\s*)/)[1].length;
|
|
66
|
-
let end = lines.length;
|
|
67
|
-
for (let i = start + 1; i < lines.length; i++) {
|
|
68
|
-
if (/^\s*$/.test(lines[i])) continue;
|
|
69
|
-
const indent = lines[i].match(/^(\s*)/)[1].length;
|
|
70
|
-
if (indent < bulletIndent) {
|
|
71
|
-
end = i;
|
|
72
|
-
break;
|
|
73
|
-
}
|
|
74
|
-
if (indent === bulletIndent && /^\s*-\s/.test(lines[i])) {
|
|
75
|
-
end = i;
|
|
76
|
-
break;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
return lines.slice(start, end).join("\n");
|
|
80
|
-
}
|
|
81
|
-
|
|
82
55
|
/** The (first) `if:` expression on a step block, trimmed. */
|
|
83
56
|
function ifCondition(stepBlock) {
|
|
84
57
|
const m = stepBlock.match(/^\s+if:\s*(.+?)\s*$/m);
|
|
@@ -86,25 +59,6 @@ function ifCondition(stepBlock) {
|
|
|
86
59
|
return m[1].trim();
|
|
87
60
|
}
|
|
88
61
|
|
|
89
|
-
/** The dedented body of the step's `run: |` block scalar. */
|
|
90
|
-
function runScript(stepBlock) {
|
|
91
|
-
const lines = stepBlock.split("\n");
|
|
92
|
-
const start = lines.findIndex((l) => /^\s+run:\s*\|\s*$/.test(l));
|
|
93
|
-
assert.notEqual(start, -1, "`run: |` block not found");
|
|
94
|
-
const runIndent = lines[start].match(/^(\s*)/)[1].length;
|
|
95
|
-
const body = [];
|
|
96
|
-
for (let i = start + 1; i < lines.length; i++) {
|
|
97
|
-
if (/^\s*$/.test(lines[i])) {
|
|
98
|
-
body.push("");
|
|
99
|
-
continue;
|
|
100
|
-
}
|
|
101
|
-
const indent = lines[i].match(/^(\s*)/)[1].length;
|
|
102
|
-
if (indent <= runIndent) break;
|
|
103
|
-
body.push(lines[i].slice(runIndent + 2));
|
|
104
|
-
}
|
|
105
|
-
return body.join("\n");
|
|
106
|
-
}
|
|
107
|
-
|
|
108
62
|
// ---------------------------------------------------------------------------
|
|
109
63
|
// 1. GATING — coverage-gate steps gated OUT of affected mode; bypass step gated IN
|
|
110
64
|
// ---------------------------------------------------------------------------
|