@cosmicdrift/kumiko-guards 0.364.0 → 0.365.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/package.json +2 -2
- package/src/_lib/push-root.ts +5 -3
- package/src/_lib/security-baseline.ts +14 -3
- package/src/changes.json +12 -0
- package/src/cli.ts +1 -1
- package/src/guard-lib-test-coverage.ts +1 -1
- package/src/guard-no-direct-fs.ts +0 -1
- package/src/guard-public-exports-changelog.ts +137 -15
- package/src/guard-test-timeouts.ts +5 -1
- package/src/pre-push.sh +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-guards",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.365.0",
|
|
4
4
|
"description": "AST-based security guards for Kumiko repos: direct-fs/fetch, tenant escalation, admin-API, escape hatches and related checks, run over a shared ts-morph project.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"kumiko-pre-push": "./src/pre-push.sh"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@cosmicdrift/kumiko-repo-manifest": "0.
|
|
31
|
+
"@cosmicdrift/kumiko-repo-manifest": "0.365.0",
|
|
32
32
|
"ts-morph": "^28.0.0"
|
|
33
33
|
},
|
|
34
34
|
"publishConfig": {
|
package/src/_lib/push-root.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { basename } from "node:path";
|
|
2
2
|
import { findLocalRepo, localRepoDirName } from "./roots";
|
|
3
3
|
|
|
4
|
-
// A worktree's folder name proves nothing (`kumiko-*` siblings share prefixes), so
|
|
4
|
+
// A worktree's folder name proves nothing (`kumiko-*` siblings share prefixes), so a path that
|
|
5
|
+
// resolves to a repo is judged by its canonical checkout's directory name; the folder name only
|
|
6
|
+
// counts for paths that are not a repo (yet).
|
|
5
7
|
function pushRootMatchesCliScope(pushRepoRoot: string, cliScope: string): boolean {
|
|
6
|
-
if (basename(pushRepoRoot) === cliScope) return true;
|
|
7
8
|
const local = findLocalRepo(pushRepoRoot);
|
|
8
|
-
|
|
9
|
+
if (local === undefined) return basename(pushRepoRoot) === cliScope;
|
|
10
|
+
return local.name === cliScope || localRepoDirName(local) === cliScope;
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
// A push root left over from an earlier hook run or a debug session must not redirect a guard at the wrong repo, so it only counts when KUMIKO_CLI_SCOPE names it.
|
|
@@ -27,7 +27,13 @@ export type SecurityBaselineLoad =
|
|
|
27
27
|
readonly findings: SecurityBaseline["findings"];
|
|
28
28
|
readonly hardFail: readonly string[];
|
|
29
29
|
}
|
|
30
|
-
| {
|
|
30
|
+
| {
|
|
31
|
+
readonly kind: "invalid";
|
|
32
|
+
readonly file: string;
|
|
33
|
+
readonly reason: string;
|
|
34
|
+
/** Prose shown after the reason when the reason alone is only a code. */
|
|
35
|
+
readonly detail?: string;
|
|
36
|
+
};
|
|
31
37
|
|
|
32
38
|
// Compared only against the baseline file's `repo` field — a scoped npm
|
|
33
39
|
// package name (`@x/y`) is no longer a path segment, so it needs no
|
|
@@ -127,7 +133,12 @@ export function loadSecurityBaseline(repo: string, repoDir: string): SecurityBas
|
|
|
127
133
|
}
|
|
128
134
|
const parsed = parseSecurityBaseline(raw);
|
|
129
135
|
if (!parsed) {
|
|
130
|
-
return {
|
|
136
|
+
return {
|
|
137
|
+
kind: "invalid",
|
|
138
|
+
file,
|
|
139
|
+
reason: "unexpected_baseline_shape",
|
|
140
|
+
detail: "expected format/repo/generated/total/findings and a valid hardFail",
|
|
141
|
+
};
|
|
131
142
|
}
|
|
132
143
|
if (parsed.repo !== repo) {
|
|
133
144
|
return {
|
|
@@ -202,7 +213,7 @@ function invalidBaselineViolation(
|
|
|
202
213
|
return {
|
|
203
214
|
file: baseline.file,
|
|
204
215
|
line: 1,
|
|
205
|
-
message: `Security baseline unreadable or invalid: ${baseline.reason}. Fix the file; a broken baseline must not release any findings.`,
|
|
216
|
+
message: `Security baseline unreadable or invalid: ${baseline.reason}${baseline.detail === undefined ? "" : ` (${baseline.detail})`}. Fix the file; a broken baseline must not release any findings.`,
|
|
206
217
|
};
|
|
207
218
|
}
|
|
208
219
|
|
package/src/changes.json
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.365.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "public-exports-changelog --base=<ref> checks drops against the baseline of the merge-base and matches exported names as whole identifiers",
|
|
6
|
+
"detail": "Check mode with --base=<ref> reads .kumiko-public-exports-baseline.json from the merge-base and sends every drop against it through the changelog/changeset ratchet, so a PR can no longer hide a removal by editing or deleting the baseline. An unknown ref is an error; a base without a baseline is a bootstrap. A changelog entry or open changeset covers a drop only when every dropped name appears in it as a whole identifier (getUserById no longer covers getUser). An unparsable changes.json is reported on stderr with its path. check-wt.sh, the worktree pre-push branch and the legacy check step pass the base."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.365.0",
|
|
10
|
+
"type": "fix",
|
|
11
|
+
"title": "guard-test-timeouts ignores identifier test bodies and timeout-free options; push-root no longer trusts a lookalike folder name",
|
|
12
|
+
"detail": "An identifier passed as a test argument only counts as a timeout when its type is a number or has a timeout property, so Playwright's test(title, details, body) and options like { retry: 2 } pass. A push root that resolves to a git repo is judged by its canonical checkout name, not by its folder name. An unexpected security baseline shape keeps the reason unexpected_baseline_shape and adds a readable detail."
|
|
13
|
+
},
|
|
2
14
|
{
|
|
3
15
|
"version": "0.364.0",
|
|
4
16
|
"type": "fix",
|
package/src/cli.ts
CHANGED
|
@@ -36,7 +36,7 @@ const SUBCOMMAND_FLAGS: Record<Subcommand, readonly string[]> = {
|
|
|
36
36
|
ui: UI_GUARD_FLAGS,
|
|
37
37
|
checks: REPO_CHECK_FLAGS,
|
|
38
38
|
"comment-lang": [...COMMENT_LANG_FLAGS, "--base=<ref>"],
|
|
39
|
-
"public-exports-changelog": PUBLIC_EXPORTS_CHANGELOG_FLAGS,
|
|
39
|
+
"public-exports-changelog": [...PUBLIC_EXPORTS_CHANGELOG_FLAGS, "--base=<ref>"],
|
|
40
40
|
licenses: DEPENDENCY_CHECK_FLAGS,
|
|
41
41
|
security: DEPENDENCY_CHECK_FLAGS,
|
|
42
42
|
outdated: DEPENDENCY_CHECK_FLAGS,
|
|
@@ -74,7 +74,7 @@ function exportedCallables(sf: SourceFile): Export[] {
|
|
|
74
74
|
return out;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
// NodeNext
|
|
77
|
+
// NodeNext imports write ".js"/".jsx" although the source is ".ts"/".tsx"
|
|
78
78
|
// (and likewise .mjs/.cjs for .mts/.cts) — the extension must therefore be
|
|
79
79
|
// stripped for all four pair variants when comparing module paths, otherwise
|
|
80
80
|
// the link breaks.
|
|
@@ -121,7 +121,6 @@ const ALLOWLIST: readonly AllowEntry[] = [
|
|
|
121
121
|
{ repo: "kumiko-framework", pattern: /^packages\/create-kumiko-app\/src\/manifest\.ts$/ },
|
|
122
122
|
|
|
123
123
|
// Sample apps: codegen/screenshot-mount helper, no request input.
|
|
124
|
-
{ repo: "kumiko-framework", pattern: /^samples\/apps\/use-all-bundled\/schema\/generate\.ts$/ },
|
|
125
124
|
{
|
|
126
125
|
repo: "kumiko-framework",
|
|
127
126
|
pattern: /^samples\/apps\/showcase\/src\/app\/mount-public-screenshots\.ts$/,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
1
2
|
import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
3
|
import { join, resolve } from "node:path";
|
|
3
4
|
import { pathToFileURL } from "node:url";
|
|
@@ -19,6 +20,12 @@ import { frameworkTsConfigPath, resolveRepoRoots } from "./_lib/roots";
|
|
|
19
20
|
// write is refused.
|
|
20
21
|
// Without an old baseline, `--write-baseline` is a bootstrap freeze.
|
|
21
22
|
//
|
|
23
|
+
// The committed baseline is a file the PR itself can edit (or delete and
|
|
24
|
+
// bootstrap-freeze), so check mode with `--base=<ref>` additionally reads the
|
|
25
|
+
// baseline from the merge-base with <ref> and runs every drop against THAT
|
|
26
|
+
// baseline through the same ratchet gate as `--write-baseline`. Only when the
|
|
27
|
+
// base has no baseline either is there nothing to protect.
|
|
28
|
+
//
|
|
22
29
|
// Scope: only the two packages with a changes.json home. packages/framework
|
|
23
30
|
// has ONE changelog (src/changes.json); packages/bundled-features has one PER
|
|
24
31
|
// FEATURE (src/<feature>/changes.json, feature = first export-key segment).
|
|
@@ -48,6 +55,7 @@ const OUT_OF_SCOPE_PACKAGES = [
|
|
|
48
55
|
"renderer",
|
|
49
56
|
] as const;
|
|
50
57
|
|
|
58
|
+
const BASE_FLAG_PREFIX = "--base=";
|
|
51
59
|
const BASELINE_FILE = ".kumiko-public-exports-baseline.json";
|
|
52
60
|
const REFREEZE_COMMAND = "bun packages/guards/src/cli.ts public-exports-changelog --write-baseline";
|
|
53
61
|
const BASELINE_FORMAT_VERSION = 1;
|
|
@@ -204,6 +212,19 @@ export function compareVersionsLocal(a: string, b: string): number {
|
|
|
204
212
|
return 0;
|
|
205
213
|
}
|
|
206
214
|
|
|
215
|
+
function escapeRegExp(value: string): string {
|
|
216
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Identifier boundaries, not substrings: `getUserById` must not cover a dropped `getUser`.
|
|
220
|
+
export function mentionsExportName(text: string, name: string): boolean {
|
|
221
|
+
return new RegExp(`(?<![A-Za-z0-9_$])${escapeRegExp(name)}(?![A-Za-z0-9_$])`).test(text);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function everyNameMentioned(texts: readonly string[], names: readonly string[]): boolean {
|
|
225
|
+
return names.every((name) => texts.some((text) => mentionsExportName(text, name)));
|
|
226
|
+
}
|
|
227
|
+
|
|
207
228
|
export function isCoveredByChangelog(
|
|
208
229
|
entries: readonly ChangelogEntryShape[],
|
|
209
230
|
asOfVersion: string,
|
|
@@ -211,15 +232,16 @@ export function isCoveredByChangelog(
|
|
|
211
232
|
): boolean {
|
|
212
233
|
// >= not >: package.json only bumps at release, so the entry `kumiko changes
|
|
213
234
|
// add` writes in the removing PR carries the SAME version as asOfVersion.
|
|
214
|
-
// With exportNames,
|
|
215
|
-
// a single breaking entry cannot blanket-cover unrelated drops.
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
235
|
+
// With exportNames, every dropped export must be named by some qualifying
|
|
236
|
+
// entry, so a single breaking entry cannot blanket-cover unrelated drops.
|
|
237
|
+
const qualifying = entries.filter(
|
|
238
|
+
(e) => e.type === "breaking" && compareVersionsLocal(e.version, asOfVersion) >= 0,
|
|
239
|
+
);
|
|
240
|
+
if (qualifying.length === 0) return false;
|
|
241
|
+
return everyNameMentioned(
|
|
242
|
+
qualifying.map((e) => `${e.title ?? ""} ${e.detail ?? ""}`),
|
|
243
|
+
exportNames,
|
|
244
|
+
);
|
|
223
245
|
}
|
|
224
246
|
|
|
225
247
|
function toChangelogEntry(e: unknown): ChangelogEntryShape[] {
|
|
@@ -241,7 +263,10 @@ function readChangelogEntries(absChangelogPath: string): ChangelogEntryShape[] {
|
|
|
241
263
|
let raw: unknown;
|
|
242
264
|
try {
|
|
243
265
|
raw = JSON.parse(readFileSync(absChangelogPath, "utf-8"));
|
|
244
|
-
} catch {
|
|
266
|
+
} catch (e) {
|
|
267
|
+
console.error(
|
|
268
|
+
` ${absChangelogPath}: changelog unreadable (${e instanceof Error ? e.message : String(e)}); its entries cannot cover a dropped export until the JSON is fixed.`,
|
|
269
|
+
);
|
|
245
270
|
return [];
|
|
246
271
|
}
|
|
247
272
|
return Array.isArray(raw) ? raw.flatMap(toChangelogEntry) : [];
|
|
@@ -395,6 +420,9 @@ export function evaluatePublicExportsChangelog(args: {
|
|
|
395
420
|
readonly mode: EvaluateMode;
|
|
396
421
|
/** Tests pass an explicit path; production resolves `<framework root>/.kumiko-public-exports-baseline.json`. */
|
|
397
422
|
readonly baselinePath?: string;
|
|
423
|
+
/** Baseline as committed on the PR's merge-base; its drops go through the ratchet gate. */
|
|
424
|
+
readonly baseBaseline?: BaselinePayload;
|
|
425
|
+
readonly pendingBreakingChanges?: PendingBreakingChanges;
|
|
398
426
|
}): EvaluateResult {
|
|
399
427
|
logOutOfScopeNotice();
|
|
400
428
|
const scans = scanAllPackages(args.cwd, args.project);
|
|
@@ -427,9 +455,74 @@ export function evaluatePublicExportsChangelog(args: {
|
|
|
427
455
|
}
|
|
428
456
|
|
|
429
457
|
const findings = findingsAgainstBaseline(parsed.baseline, scans);
|
|
458
|
+
if (args.baseBaseline) {
|
|
459
|
+
const alreadyReported = new Set(findings.map(findingIdentity));
|
|
460
|
+
const ratchet = uncoveredDrops(
|
|
461
|
+
args.cwd,
|
|
462
|
+
scans,
|
|
463
|
+
args.baseBaseline,
|
|
464
|
+
args.pendingBreakingChanges ?? new Map(),
|
|
465
|
+
);
|
|
466
|
+
findings.push(...ratchet.filter((f) => !alreadyReported.has(findingIdentity(f))));
|
|
467
|
+
}
|
|
430
468
|
return { scans, baselineStatus: "ok", findings };
|
|
431
469
|
}
|
|
432
470
|
|
|
471
|
+
function findingIdentity(f: UncoveredFinding): string {
|
|
472
|
+
return `${f.pkg}|${f.key}|${f.names.join(",")}`;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// ── Baseline of the merge-base ───────────────────────────────────────────
|
|
476
|
+
|
|
477
|
+
export type BaseBaselineRead =
|
|
478
|
+
| { readonly kind: "absent" }
|
|
479
|
+
| { readonly kind: "ok"; readonly baseline: BaselinePayload }
|
|
480
|
+
| { readonly kind: "error"; readonly detail: string };
|
|
481
|
+
|
|
482
|
+
function git(args: readonly string[], cwd: string): { ok: true; out: string } | { ok: false } {
|
|
483
|
+
const res = spawnSync("git", args, { cwd, encoding: "utf-8" });
|
|
484
|
+
return res.status === 0 ? { ok: true, out: res.stdout } : { ok: false };
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
export function readBaseBaseline(cwd: string, baseRef: string): BaseBaselineRead {
|
|
488
|
+
if (baseRef === "" || baseRef.startsWith("-")) {
|
|
489
|
+
return {
|
|
490
|
+
kind: "error",
|
|
491
|
+
detail: `--base must be a ref name such as origin/main (got "${baseRef}"); an empty value or a leading "-" would be read as a git option.`,
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
const mergeBase = git(["merge-base", baseRef, "HEAD"], cwd);
|
|
495
|
+
if (!mergeBase.ok) {
|
|
496
|
+
return {
|
|
497
|
+
kind: "error",
|
|
498
|
+
detail: `git merge-base ${baseRef} HEAD failed — is ${baseRef} known locally? On shallow checkouts fetch the full history or the ref.`,
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
const commit = mergeBase.out.trim();
|
|
502
|
+
const listed = git(["ls-tree", "--name-only", commit, "--", BASELINE_FILE], cwd);
|
|
503
|
+
if (!listed.ok) return { kind: "error", detail: `git ls-tree ${commit} failed.` };
|
|
504
|
+
if (listed.out.trim() === "") return { kind: "absent" };
|
|
505
|
+
const shown = git(["show", `${commit}:./${BASELINE_FILE}`], cwd);
|
|
506
|
+
if (!shown.ok) return { kind: "error", detail: `git show ${commit}:${BASELINE_FILE} failed.` };
|
|
507
|
+
let raw: unknown;
|
|
508
|
+
try {
|
|
509
|
+
raw = JSON.parse(shown.out);
|
|
510
|
+
} catch (e) {
|
|
511
|
+
console.error(
|
|
512
|
+
` Baseline on ${baseRef} unreadable (${e instanceof Error ? e.message : String(e)}); nothing to ratchet against.`,
|
|
513
|
+
);
|
|
514
|
+
return { kind: "absent" };
|
|
515
|
+
}
|
|
516
|
+
const parsed = parseBaseline(raw);
|
|
517
|
+
if (parsed.kind !== "ok") {
|
|
518
|
+
console.error(
|
|
519
|
+
` Baseline on ${baseRef} is ${parsed.kind} (${parsed.detail}); nothing to ratchet against.`,
|
|
520
|
+
);
|
|
521
|
+
return { kind: "absent" };
|
|
522
|
+
}
|
|
523
|
+
return { kind: "ok", baseline: parsed.baseline };
|
|
524
|
+
}
|
|
525
|
+
|
|
433
526
|
// ── Open changesets ───────────────────────────────────────────────────────
|
|
434
527
|
|
|
435
528
|
type ChangesetParser = (markdown: string, source: string) => readonly unknown[];
|
|
@@ -502,15 +595,15 @@ export async function loadPendingBreakingChanges(
|
|
|
502
595
|
return pendingBreakingChangesIn(join(frameworkRootAbsPath, ".changeset"), parse);
|
|
503
596
|
}
|
|
504
597
|
|
|
505
|
-
// A feature-wide breaking block must not cover unrelated drops:
|
|
506
|
-
// to
|
|
598
|
+
// A feature-wide breaking block must not cover unrelated drops: every dropped
|
|
599
|
+
// export has to be named by some block of the feature.
|
|
507
600
|
export function isCoveredByPendingChangeset(
|
|
508
601
|
pending: PendingBreakingChanges,
|
|
509
602
|
feature: string,
|
|
510
603
|
droppedNames: readonly string[],
|
|
511
604
|
): boolean {
|
|
512
605
|
const texts = pending.get(feature) ?? [];
|
|
513
|
-
return texts.
|
|
606
|
+
return texts.length > 0 && everyNameMentioned(texts, droppedNames);
|
|
514
607
|
}
|
|
515
608
|
|
|
516
609
|
// ── Write baseline ────────────────────────────────────────────────────────
|
|
@@ -653,13 +746,40 @@ function reportViolations(violations: readonly GuardViolation[]): void {
|
|
|
653
746
|
]);
|
|
654
747
|
}
|
|
655
748
|
|
|
749
|
+
// undefined = the ratchet inputs could not be read; the diagnosis is printed already.
|
|
750
|
+
async function baseRatchetFor(
|
|
751
|
+
scanCwd: string,
|
|
752
|
+
mode: EvaluateMode,
|
|
753
|
+
baseRef: string | undefined,
|
|
754
|
+
): Promise<
|
|
755
|
+
{ baseBaseline?: BaselinePayload; pendingBreakingChanges?: PendingBreakingChanges } | undefined
|
|
756
|
+
> {
|
|
757
|
+
if (baseRef === undefined || mode !== "check") return {};
|
|
758
|
+
const base = readBaseBaseline(scanCwd, baseRef);
|
|
759
|
+
if (base.kind === "error") {
|
|
760
|
+
console.error(` ${base.detail}`);
|
|
761
|
+
return undefined;
|
|
762
|
+
}
|
|
763
|
+
if (base.kind === "absent") return {};
|
|
764
|
+
try {
|
|
765
|
+
const pendingBreakingChanges = await loadPendingBreakingChanges(scanCwd);
|
|
766
|
+
return { baseBaseline: base.baseline, pendingBreakingChanges };
|
|
767
|
+
} catch (e) {
|
|
768
|
+
console.error(
|
|
769
|
+
` Cannot read open changesets: ${e instanceof Error ? e.message : String(e)}. Fix the changeset file named above.`,
|
|
770
|
+
);
|
|
771
|
+
return undefined;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
656
775
|
export async function runPublicExportsChangelogCli(
|
|
657
776
|
argv: readonly string[],
|
|
658
777
|
cwd: string = process.cwd(),
|
|
659
778
|
): Promise<number> {
|
|
779
|
+
const baseArg = argv.find((arg) => arg.startsWith(BASE_FLAG_PREFIX));
|
|
660
780
|
const flagsError = cliFlagsError(
|
|
661
781
|
"public-exports-changelog",
|
|
662
|
-
argv,
|
|
782
|
+
argv.filter((arg) => !arg.startsWith(BASE_FLAG_PREFIX)),
|
|
663
783
|
PUBLIC_EXPORTS_CHANGELOG_FLAGS,
|
|
664
784
|
);
|
|
665
785
|
if (flagsError !== undefined) {
|
|
@@ -696,7 +816,9 @@ export async function runPublicExportsChangelogCli(
|
|
|
696
816
|
}
|
|
697
817
|
|
|
698
818
|
const mode: EvaluateMode = argv.includes("--no-baseline") ? "no-baseline" : "check";
|
|
699
|
-
const
|
|
819
|
+
const ratchet = await baseRatchetFor(scanCwd, mode, baseArg?.slice(BASE_FLAG_PREFIX.length));
|
|
820
|
+
if (ratchet === undefined) return 1;
|
|
821
|
+
const result = evaluatePublicExportsChangelog({ cwd: scanCwd, project, mode, ...ratchet });
|
|
700
822
|
report(result);
|
|
701
823
|
const baselineBroken =
|
|
702
824
|
result.baselineStatus === "format-drift" || result.baselineStatus === "missing";
|
|
@@ -177,9 +177,13 @@ function declarationBaseCallee(callee: Node): Node {
|
|
|
177
177
|
return Node.isCallExpression(callee) ? declarationBaseCallee(callee.getExpression()) : callee;
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
+
// An identifier can be the Playwright test body (`test(title, details, body)`) or an options
|
|
181
|
+
// variable like `{ retry: 2 }`, so it only counts when its type is a number or carries `timeout`.
|
|
180
182
|
function isTimeoutArgument(arg: Node): boolean {
|
|
181
183
|
if (Node.isObjectLiteralExpression(arg)) return hasTimeoutProperty(arg);
|
|
182
|
-
|
|
184
|
+
if (Node.isArrowFunction(arg) || Node.isFunctionExpression(arg)) return false;
|
|
185
|
+
const type = arg.getType();
|
|
186
|
+
return type.isNumber() || type.isNumberLiteral() || type.getProperty("timeout") !== undefined;
|
|
183
187
|
}
|
|
184
188
|
|
|
185
189
|
function declarationTimeoutReason(
|
package/src/pre-push.sh
CHANGED
|
@@ -195,6 +195,7 @@ if [ "$(git rev-parse --path-format=absolute --git-dir)" != "$GIT_COMMON_DIR" ]
|
|
|
195
195
|
run_guard_bin guards kumiko-guards guards
|
|
196
196
|
run_guard_bin checks kumiko-guards checks
|
|
197
197
|
run_guard_bin comment-lang kumiko-guards comment-lang --touched --base=origin/main
|
|
198
|
+
run_guard_bin public-exports-changelog kumiko-guards public-exports-changelog --base=origin/main
|
|
198
199
|
if [ -n "$FAILED" ]; then
|
|
199
200
|
echo "[pre-push] worktree check failed:$FAILED" >&2
|
|
200
201
|
exit 1
|