@monte3l/groundwork 0.1.0-next.1 → 1.0.0-rc.3
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 +16 -8
- package/bin/m3l-groundwork.mjs +36 -2
- package/dist/assets.d.ts +10 -0
- package/dist/assets.js +14 -0
- package/dist/baseline-stage.d.ts +173 -0
- package/dist/baseline-stage.js +215 -0
- package/dist/caps.d.ts +4 -1
- package/dist/caps.js +17 -3
- package/dist/conflicts.d.ts +23 -2
- package/dist/conflicts.js +89 -11
- package/dist/customize-paths.d.ts +92 -0
- package/dist/customize-paths.js +115 -0
- package/dist/emit.d.ts +50 -1
- package/dist/emit.js +121 -13
- package/dist/fatal.d.ts +70 -0
- package/dist/fatal.js +132 -0
- package/dist/format-error.d.ts +113 -0
- package/dist/format-error.js +553 -0
- package/dist/fs-guard.d.ts +142 -0
- package/dist/fs-guard.js +222 -0
- package/dist/git.js +10 -1
- package/dist/harness/conformance.js +2 -0
- package/dist/harness/frontmatter.js +2 -13
- package/dist/harness/grade.js +37 -8
- package/dist/harness/rules.d.ts +20 -3
- package/dist/harness/rules.js +108 -6
- package/dist/harness/types.d.ts +13 -0
- package/dist/harness/types.js +3 -0
- package/dist/inventory.d.ts +77 -3
- package/dist/inventory.js +103 -12
- package/dist/jsonc.d.ts +49 -2
- package/dist/jsonc.js +140 -7
- package/dist/main.d.ts +62 -3
- package/dist/main.js +538 -67
- package/dist/merge-json.d.ts +47 -4
- package/dist/merge-json.js +120 -8
- package/dist/mode.js +12 -2
- package/dist/pack-stage.d.ts +210 -0
- package/dist/pack-stage.js +287 -0
- package/dist/packs.d.ts +19 -13
- package/dist/packs.js +231 -28
- package/dist/palette.d.ts +23 -0
- package/dist/palette.js +22 -0
- package/dist/plugin.d.ts +122 -6
- package/dist/plugin.js +687 -47
- package/dist/report.d.ts +21 -2
- package/dist/report.js +93 -5
- package/dist/staging.d.ts +176 -0
- package/dist/staging.js +375 -0
- package/dist/survey/fs-walk.d.ts +34 -2
- package/dist/survey/fs-walk.js +70 -5
- package/dist/survey/internal/blocked-path.d.ts +27 -0
- package/dist/survey/internal/blocked-path.js +129 -0
- package/dist/survey/internal/package-json.d.ts +13 -0
- package/dist/survey/internal/package-json.js +37 -0
- package/dist/survey/internal/read-guard.d.ts +178 -0
- package/dist/survey/internal/read-guard.js +276 -0
- package/dist/survey/survey-docs.d.ts +17 -2
- package/dist/survey/survey-docs.js +61 -21
- package/dist/survey/survey-harness.d.ts +20 -2
- package/dist/survey/survey-harness.js +87 -46
- package/dist/survey/survey-shape.d.ts +17 -2
- package/dist/survey/survey-shape.js +60 -46
- package/dist/survey/survey-toolchain.d.ts +16 -1
- package/dist/survey/survey-toolchain.js +69 -66
- package/dist/survey/survey.js +6 -4
- package/dist/survey/types.d.ts +79 -0
- package/dist/survey/types.js +2 -6
- package/dist/term.d.ts +80 -0
- package/dist/term.js +145 -0
- package/dist/tokens.js +2 -0
- package/dist/toolchain/conformance.js +2 -0
- package/dist/toolchain/grade.js +26 -8
- package/dist/toolchain/rules.d.ts +13 -4
- package/dist/toolchain/rules.js +16 -0
- package/dist/toolchain/tsconfig-chain.d.ts +2 -0
- package/dist/toolchain/tsconfig-chain.js +32 -8
- package/dist/toolchain/types.d.ts +16 -4
- package/dist/toolchain/types.js +3 -0
- package/package.json +4 -3
- package/plugin/skills/customize/SKILL.md +292 -18
- package/plugin/src/domain-map.ts +39 -14
- package/plugin/src/index.ts +5 -1
- package/plugin/src/kind-facet-map.ts +25 -8
- package/plugin/src/pack-map.ts +147 -25
- package/plugin/src/plugin-map.ts +236 -0
- package/templates/core/.claude/agents/Explore.md +0 -1
- package/templates/core/.claude/agents/code-implementer.md +4 -4
- package/templates/core/.claude/agents/code-reviewer.md +4 -4
- package/templates/core/.claude/agents/silent-failure-hunter.md +2 -2
- package/templates/core/.claude/agents/test-author.md +7 -5
- package/templates/core/.claude/hooks/guard-branch-isolation.mjs +56 -10
- package/templates/core/.claude/hooks/guard-double-background.mjs +13 -8
- package/templates/core/.claude/hooks/guard-git-push-signed.mjs +12 -9
- package/templates/core/.claude/hooks/guard-hub-src-writes.mjs +1890 -38
- package/templates/core/.claude/hooks/guard-js-extension.mjs +2 -1
- package/templates/core/.claude/hooks/guard-no-commonjs.mjs +7 -5
- package/templates/core/.claude/hooks/guard-secret-writes.mjs +7 -5
- package/templates/core/.claude/hooks/inject-decision-gate.mjs +12 -9
- package/templates/core/.claude/hooks/post-edit-verify.mjs +276 -98
- package/templates/core/.claude/rules/agent-dispatch.md +7 -0
- package/templates/core/.claude/rules/tests.md +2 -2
- package/templates/core/.claude/settings.json +5 -0
- package/templates/core/.claude/skills/finishing-work/SKILL.md +58 -10
- package/templates/core/.claude/skills/harness-guidance/SKILL.md +16 -7
- package/templates/core/.claude/skills/starting-work/SKILL.md +5 -0
- package/templates/core/.claude/skills/triaging-ci/SKILL.md +9 -8
- package/templates/core/.claude/skills/typescript-guidance/SKILL.md +137 -20
- package/templates/core/.claude/skills/typescript-guidance/references/area-catalog.md +135 -0
- package/templates/core/.claude/skills/typescript-guidance/references/tooling-sources.md +113 -0
- package/templates/core/.claude/skills/writing-commits/SKILL.md +2 -2
- package/templates/core/.github/dependabot.yml +18 -0
- package/templates/core/.github/workflows/ci.yml +15 -15
- package/templates/core/.github/workflows/dependency-review.yml +2 -2
- package/templates/core/.github/workflows/security-audit.yml +10 -4
- package/templates/core/.prettierignore +4 -0
- package/templates/core/CLAUDE.md +53 -3
- package/templates/core/README.md +30 -10
- package/templates/core/_gitignore +17 -0
- package/templates/core/bin/check-exports.mjs +11 -5
- package/templates/core/bin/lib/agent-roster.mjs +1 -1
- package/templates/core/bin/lib/frontmatter.mjs +4 -2
- package/templates/core/bin/lib/harness-rules.mjs +169 -20
- package/templates/core/bin/lib/protected-paths.mjs +93 -5
- package/templates/core/bin/lib/toolchain-rules.mjs +187 -26
- package/templates/core/bin/lib/verify-steps.mjs +29 -11
- package/templates/core/bin/verify.mjs +12 -0
- package/templates/core/eslint.config.js +5 -0
- package/templates/core/package.json +7 -7
- package/templates/core/vitest.config.ts +8 -1
- package/templates/packs/README.md +35 -24
- package/templates/packs/github/files/.claude/skills/reviewing-dependabot-prs/SKILL.md +133 -0
- package/templates/packs/github/files/.claude/skills/triaging-scan-alerts/SKILL.md +88 -0
- package/templates/packs/github/files/.claude/skills/watching-pr-checks/SKILL.md +94 -0
- package/templates/packs/github/files/.github/workflows/claude-pr-review.yml +267 -0
- package/templates/packs/github/files/.github/workflows/claude.yml +106 -0
- package/templates/packs/github/pack.json +19 -0
- package/templates/packs/harness-extras/files/.claude/hooks/guard-readonly-bash.mjs +1122 -65
- package/templates/packs/harness-extras/files/.claude/hooks/reinject-compact-handoff.mjs +147 -13
- package/templates/packs/{statusline → harness-extras}/files/.claude/hooks/statusline.mjs +6 -2
- package/templates/packs/harness-extras/files/.claude/hooks/write-compact-handoff.mjs +227 -44
- package/templates/packs/harness-extras/pack.json +18 -13
- package/templates/packs/publishing/files/.changeset/README.md +25 -0
- package/templates/packs/publishing/files/.changeset/config.json +7 -0
- package/templates/packs/publishing/files/.github/release-tools/package.json +9 -0
- package/templates/packs/publishing/files/.github/workflows/release.yml +293 -0
- package/templates/packs/publishing/files/REUSE.toml +28 -0
- package/templates/packs/publishing/files/bin/check-dts-deps.mjs +234 -0
- package/templates/packs/publishing/files/bin/check-license-headers.mjs +217 -0
- package/templates/packs/publishing/files/bin/check-publish-version.mjs +149 -0
- package/templates/packs/publishing/files/bin/lib/npm-publish-args.mjs +86 -0
- package/templates/packs/publishing/files/bin/pnpm-publish-shim.mjs +86 -0
- package/templates/packs/publishing/pack.json +35 -0
- package/templates/packs/{harness-extras → quality}/files/bin/check-file-budget.mjs +6 -4
- package/templates/packs/quality/pack.json +29 -0
- package/templates/packs/supply-chain/files/.github/workflows/gitleaks.yml +52 -0
- package/templates/packs/supply-chain/files/.github/workflows/scorecard.yml +48 -0
- package/templates/packs/supply-chain/files/.gitleaks.toml +2 -0
- package/templates/packs/supply-chain/pack.json +19 -0
- package/templates/packs/worktrees/files/.claude/hooks/ensure-worktree-deps.mjs +283 -0
- package/templates/packs/worktrees/files/.claude/hooks/guard-worktree-only.mjs +245 -0
- package/templates/packs/worktrees/files/.claude/hooks/repair-core-bare.mjs +335 -0
- package/templates/packs/worktrees/files/.claude/skills/working-in-worktrees/SKILL.md +139 -0
- package/templates/packs/worktrees/files/.worktreeinclude +11 -0
- package/templates/packs/worktrees/pack.json +64 -0
- package/templates/packs/statusline/pack.json +0 -31
- /package/templates/packs/{statusline → harness-extras}/files/.claude/hooks/statusline-layout.mjs +0 -0
- /package/templates/packs/{statusline → harness-extras}/files/.claude/hooks/subagent-statusline.mjs +0 -0
- /package/templates/packs/{harness-extras → quality}/files/.claude/agents/type-design-analyzer.md +0 -0
- /package/templates/packs/{harness-extras → quality}/files/bin/file-budget-baseline.json +0 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SPDX-FileCopyrightText: Copyright the __PROJECT_NAME__ contributors
|
|
3
|
+
// SPDX-License-Identifier: NOASSERTION
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Checks (and, with `--fix`, inserts) an SPDX copyright/license header on
|
|
7
|
+
* every tracked source file -- OpenSSF Best Practices' Gold-level
|
|
8
|
+
* `copyright_per_file`/`license_per_file` criteria.
|
|
9
|
+
*
|
|
10
|
+
* Two disjoint mechanisms cover every tracked file (`git ls-files`), never
|
|
11
|
+
* both for the same file:
|
|
12
|
+
* - **An inline header** (`SPDX-FileCopyrightText` + `SPDX-License-Identifier`,
|
|
13
|
+
* as a line comment) on every `.ts`/`.mjs`/`.js`/`.sh`/`.yml`/`.yaml`
|
|
14
|
+
* file, except anything a `REUSE.toml` annotation already claims (a
|
|
15
|
+
* generated file with that extension, such as a committed lockfile, has
|
|
16
|
+
* nothing meaningful to carry a hand-written header in).
|
|
17
|
+
* - **A `REUSE.toml` annotation** for everything else: data files and
|
|
18
|
+
* dotfiles with no comment syntax of their own (JSON, Markdown,
|
|
19
|
+
* `.gitignore`, `.npmrc`, `.node-version`, `.prettierignore`, `LICENSE`).
|
|
20
|
+
*
|
|
21
|
+
* `--check` (default, the `pnpm verify` gate's `license-headers` step):
|
|
22
|
+
* exits 1 and lists every header-eligible file missing the header, AND every
|
|
23
|
+
* tracked file that is neither header-eligible nor matched by a `REUSE.toml`
|
|
24
|
+
* glob (a gap in the REUSE annotation set itself, not just a missing
|
|
25
|
+
* header). `--fix`: inserts the header into every header-eligible file
|
|
26
|
+
* missing one, after a shebang line if present, and leaves every other line
|
|
27
|
+
* untouched.
|
|
28
|
+
*/
|
|
29
|
+
import process from "node:process";
|
|
30
|
+
import { execFileSync } from "node:child_process";
|
|
31
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
32
|
+
import { extname, join } from "node:path";
|
|
33
|
+
import { fileURLToPath } from "node:url";
|
|
34
|
+
|
|
35
|
+
const repoRoot = join(fileURLToPath(import.meta.url), "..", "..");
|
|
36
|
+
|
|
37
|
+
/** Extensions that carry an inline SPDX header via a `#`/`//` line comment. */
|
|
38
|
+
const HEADER_EXTENSIONS = new Set([
|
|
39
|
+
".ts",
|
|
40
|
+
".mjs",
|
|
41
|
+
".js",
|
|
42
|
+
".sh",
|
|
43
|
+
".yml",
|
|
44
|
+
".yaml",
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
const HASH_COMMENT_EXTENSIONS = new Set([".sh", ".yml", ".yaml"]);
|
|
48
|
+
|
|
49
|
+
const COPYRIGHT_MARKER = "SPDX-FileCopyrightText:";
|
|
50
|
+
const LICENSE_MARKER = "SPDX-License-Identifier:";
|
|
51
|
+
|
|
52
|
+
function headerLines(ext) {
|
|
53
|
+
const prefix = HASH_COMMENT_EXTENSIONS.has(ext) ? "#" : "//";
|
|
54
|
+
return [
|
|
55
|
+
`${prefix} SPDX-FileCopyrightText: Copyright the __PROJECT_NAME__ contributors`,
|
|
56
|
+
`${prefix} SPDX-License-Identifier: NOASSERTION`,
|
|
57
|
+
];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Every path git tracks, repo-root-relative, forward-slash-separated.
|
|
62
|
+
* `-z` NUL-delimits the output instead of newline-delimiting it, so a path
|
|
63
|
+
* containing a character `core.quotePath` would otherwise quote/escape
|
|
64
|
+
* (non-ASCII, a literal newline) comes back exactly as git stores it,
|
|
65
|
+
* matching what `readFileSync`/`writeFileSync` expect.
|
|
66
|
+
*/
|
|
67
|
+
function listTrackedFiles() {
|
|
68
|
+
return execFileSync("git", ["-C", repoRoot, "ls-files", "-z"], {
|
|
69
|
+
encoding: "utf8",
|
|
70
|
+
})
|
|
71
|
+
.split("\0")
|
|
72
|
+
.filter((line) => line.length > 0);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* A tiny, deliberately non-general glob-to-regex translator: only `**`
|
|
77
|
+
* (any number of path segments, including zero) and `*` (anything but `/`)
|
|
78
|
+
* are supported, which is all `REUSE.toml`'s own patterns use. Every other
|
|
79
|
+
* character is treated literally.
|
|
80
|
+
*/
|
|
81
|
+
function globToRegExp(glob) {
|
|
82
|
+
let pattern = "";
|
|
83
|
+
for (let i = 0; i < glob.length; i++) {
|
|
84
|
+
const ch = glob[i];
|
|
85
|
+
if (ch === "*" && glob[i + 1] === "*") {
|
|
86
|
+
if (glob[i + 2] === "/") {
|
|
87
|
+
pattern += "(?:.*/)?";
|
|
88
|
+
i += 2;
|
|
89
|
+
} else {
|
|
90
|
+
pattern += ".*";
|
|
91
|
+
i += 1;
|
|
92
|
+
}
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (ch === "*") {
|
|
96
|
+
pattern += "[^/]*";
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (/[.+^${}()|[\]\\?]/.test(ch)) {
|
|
100
|
+
pattern += `\\${ch}`;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
pattern += ch;
|
|
104
|
+
}
|
|
105
|
+
return new RegExp(`^${pattern}$`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Every `path = [...]` glob string across every `[[annotations]]` block in
|
|
110
|
+
* `REUSE.toml`. Only the array form (`path = ["a", "b"]`) is supported, not
|
|
111
|
+
* REUSE's single-string form (`path = "a"`) or single-quoted strings -- if
|
|
112
|
+
* that ever changes, a silently-dropped annotation would quietly shrink
|
|
113
|
+
* REUSE.toml's coverage, so this counts `[[annotations]]` blocks against
|
|
114
|
+
* blocks that actually yielded a `path` array and fails loudly on a
|
|
115
|
+
* mismatch rather than trusting an empty result.
|
|
116
|
+
*/
|
|
117
|
+
function readReuseGlobs() {
|
|
118
|
+
const content = readFileSync(join(repoRoot, "REUSE.toml"), "utf8");
|
|
119
|
+
const blockCount = (content.match(/^\[\[annotations]]/gm) ?? []).length;
|
|
120
|
+
const globs = [];
|
|
121
|
+
const blockPattern = /\[\[annotations]][\s\S]*?path\s*=\s*\[([\s\S]*?)]/g;
|
|
122
|
+
let parsedBlocks = 0;
|
|
123
|
+
for (const match of content.matchAll(blockPattern)) {
|
|
124
|
+
parsedBlocks += 1;
|
|
125
|
+
const arrayBody = match[1] ?? "";
|
|
126
|
+
for (const stringMatch of arrayBody.matchAll(/"([^"]*)"/g)) {
|
|
127
|
+
const glob = stringMatch[1];
|
|
128
|
+
if (glob !== undefined) globs.push(glob);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (parsedBlocks !== blockCount) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
`check-license-headers: REUSE.toml has ${blockCount} [[annotations]] block(s) but only ` +
|
|
134
|
+
`${parsedBlocks} had a parseable \`path = [...]\` array -- this parser only supports ` +
|
|
135
|
+
"that array form; check for a single-string or single-quoted `path` value.",
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
return globs;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function hasHeader(content) {
|
|
142
|
+
const firstLines = content.split("\n", 6).join("\n");
|
|
143
|
+
return (
|
|
144
|
+
firstLines.includes(COPYRIGHT_MARKER) && firstLines.includes(LICENSE_MARKER)
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function withHeaderInserted(content, ext) {
|
|
149
|
+
const lines = content.split("\n");
|
|
150
|
+
const hasShebang = lines[0]?.startsWith("#!") ?? false;
|
|
151
|
+
const insertAt = hasShebang ? 1 : 0;
|
|
152
|
+
lines.splice(insertAt, 0, ...headerLines(ext), "");
|
|
153
|
+
return lines.join("\n");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function main() {
|
|
157
|
+
const fix = process.argv.includes("--fix");
|
|
158
|
+
const tracked = listTrackedFiles();
|
|
159
|
+
const reuseGlobs = readReuseGlobs().map(globToRegExp);
|
|
160
|
+
const isReuseExempt = (path) => reuseGlobs.some((re) => re.test(path));
|
|
161
|
+
|
|
162
|
+
const headerEligible = tracked.filter(
|
|
163
|
+
(path) => HEADER_EXTENSIONS.has(extname(path)) && !isReuseExempt(path),
|
|
164
|
+
);
|
|
165
|
+
const uncovered = tracked.filter(
|
|
166
|
+
(path) => !HEADER_EXTENSIONS.has(extname(path)) && !isReuseExempt(path),
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
if (uncovered.length > 0) {
|
|
170
|
+
console.error(
|
|
171
|
+
`check-license-headers: ${uncovered.length} tracked file(s) are neither header-eligible ` +
|
|
172
|
+
"nor matched by a REUSE.toml annotation -- add a glob to REUSE.toml or an extension to " +
|
|
173
|
+
"HEADER_EXTENSIONS:",
|
|
174
|
+
);
|
|
175
|
+
for (const path of uncovered) console.error(` ${path}`);
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
let fixedCount = 0;
|
|
180
|
+
const missing = [];
|
|
181
|
+
|
|
182
|
+
for (const path of headerEligible) {
|
|
183
|
+
const absolute = join(repoRoot, path);
|
|
184
|
+
const content = readFileSync(absolute, "utf8");
|
|
185
|
+
if (hasHeader(content)) continue;
|
|
186
|
+
if (fix) {
|
|
187
|
+
writeFileSync(absolute, withHeaderInserted(content, extname(path)));
|
|
188
|
+
fixedCount += 1;
|
|
189
|
+
} else {
|
|
190
|
+
missing.push(path);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (fix) {
|
|
195
|
+
console.log(`check-license-headers: fixed ${fixedCount} file(s)`);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (missing.length > 0) {
|
|
200
|
+
console.error(
|
|
201
|
+
`check-license-headers: ${missing.length} file(s) missing an SPDX header ` +
|
|
202
|
+
"(SPDX-FileCopyrightText + SPDX-License-Identifier):",
|
|
203
|
+
);
|
|
204
|
+
for (const path of missing) console.error(` ${path}`);
|
|
205
|
+
console.error(
|
|
206
|
+
"Run `node bin/check-license-headers.mjs --fix` to insert them.",
|
|
207
|
+
);
|
|
208
|
+
process.exit(1);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
console.log(
|
|
212
|
+
`check-license-headers: ok -- ${headerEligible.length} file(s) carry an SPDX header, ` +
|
|
213
|
+
`${tracked.length - headerEligible.length} covered by REUSE.toml`,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
main();
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SPDX-FileCopyrightText: Copyright the __PROJECT_NAME__ contributors
|
|
3
|
+
// SPDX-License-Identifier: NOASSERTION
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Checks that the version this project's `package.json` currently declares
|
|
7
|
+
* has not already been published to npm -- catches a version bump that
|
|
8
|
+
* silently duplicates one already live (a stale manual edit, a rebase that
|
|
9
|
+
* dropped a changeset), surfaced here rather than as a cryptic registry
|
|
10
|
+
* rejection deep inside the release workflow's `publish` job.
|
|
11
|
+
*
|
|
12
|
+
* Deliberately NOT one of this pack's `pnpm verify` steps: with changesets,
|
|
13
|
+
* `package.json`'s version on the release branch is always the one just
|
|
14
|
+
* published between releases, so running this on every ordinary push would
|
|
15
|
+
* fail every single time until the next version PR lands. Invoke it
|
|
16
|
+
* directly, once, right before packing -- see `release.yml`'s `pack` job.
|
|
17
|
+
*
|
|
18
|
+
* A no-op (`checked: 0`, exit 0), not a failure, when:
|
|
19
|
+
* - `package.json` has `private: true` (the baseline's own default) -- there
|
|
20
|
+
* is nothing to publish, so nothing to check.
|
|
21
|
+
* - The registry lookup fails for a reason other than "package not found"
|
|
22
|
+
* (network error, timeout, registry outage, an unparseable body) -- this
|
|
23
|
+
* is a best-effort due-diligence check, not a hard dependency on the
|
|
24
|
+
* registry being reachable; a real duplicate-version publish still fails
|
|
25
|
+
* at `npm stage publish` itself, which is the actual enforcement point.
|
|
26
|
+
*
|
|
27
|
+
* A package that has never been published at all (the registry 404s) is
|
|
28
|
+
* also fine: there is no prior version to collide with. A 404 can also mean
|
|
29
|
+
* a scoped/restricted package the anonymous registry lookup can't see --
|
|
30
|
+
* this check has no npm auth token to look further, so it reports that
|
|
31
|
+
* possibility rather than asserting the package has definitely never
|
|
32
|
+
* published.
|
|
33
|
+
*
|
|
34
|
+
* A non-private `package.json` missing `name` or `version` is a real
|
|
35
|
+
* misconfiguration, not something to wave through: the version and dts-deps
|
|
36
|
+
* gates only make sense once both fields say what's actually being shipped.
|
|
37
|
+
*/
|
|
38
|
+
import process from "node:process";
|
|
39
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
40
|
+
import { join } from "node:path";
|
|
41
|
+
import { fileURLToPath } from "node:url";
|
|
42
|
+
|
|
43
|
+
const repoRoot = join(fileURLToPath(import.meta.url), "..", "..");
|
|
44
|
+
const REGISTRY_TIMEOUT_MS = 10_000;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @param {string} name
|
|
48
|
+
* @param {typeof fetch} fetchImpl
|
|
49
|
+
* @returns {Promise<{ ok: true, versions: string[] } | { ok: false, reason: string }>}
|
|
50
|
+
* `ok: false` means the lookup could not be completed (network error,
|
|
51
|
+
* timeout, a non-404 error status, an unparseable body) -- distinct from
|
|
52
|
+
* "the package has never been published" (a 404), which resolves
|
|
53
|
+
* `{ ok: true, versions: [] }`.
|
|
54
|
+
*/
|
|
55
|
+
export async function fetchPublishedVersions(name, fetchImpl) {
|
|
56
|
+
let response;
|
|
57
|
+
try {
|
|
58
|
+
response = await fetchImpl(
|
|
59
|
+
`https://registry.npmjs.org/${encodeURIComponent(name).replaceAll("%40", "@")}`,
|
|
60
|
+
{ signal: AbortSignal.timeout(REGISTRY_TIMEOUT_MS) },
|
|
61
|
+
);
|
|
62
|
+
} catch (cause) {
|
|
63
|
+
return {
|
|
64
|
+
ok: false,
|
|
65
|
+
reason: `request failed: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (response.status === 404) {
|
|
69
|
+
return { ok: true, versions: [] };
|
|
70
|
+
}
|
|
71
|
+
if (!response.ok) {
|
|
72
|
+
return {
|
|
73
|
+
ok: false,
|
|
74
|
+
reason: `registry responded with HTTP ${String(response.status)}`,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
let body;
|
|
78
|
+
try {
|
|
79
|
+
body = await response.json();
|
|
80
|
+
} catch {
|
|
81
|
+
return { ok: false, reason: "registry response body was not valid JSON" };
|
|
82
|
+
}
|
|
83
|
+
const versions = body?.versions;
|
|
84
|
+
if (versions === null || typeof versions !== "object") {
|
|
85
|
+
return {
|
|
86
|
+
ok: false,
|
|
87
|
+
reason: 'registry response had no usable "versions" field',
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return { ok: true, versions: Object.keys(versions) };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function main() {
|
|
94
|
+
const pkgPath = join(repoRoot, "package.json");
|
|
95
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
96
|
+
|
|
97
|
+
if (pkg?.private === true) {
|
|
98
|
+
console.log("check-publish-version: package.json is private -- skipping");
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const name = pkg?.name;
|
|
102
|
+
const version = pkg?.version;
|
|
103
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
104
|
+
console.error(
|
|
105
|
+
"check-publish-version: package.json has no name, but is not private -- " +
|
|
106
|
+
"either set private: true, or give it a real name",
|
|
107
|
+
);
|
|
108
|
+
process.exitCode = 1;
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (typeof version !== "string" || version.length === 0) {
|
|
112
|
+
console.error("check-publish-version: package.json has no version");
|
|
113
|
+
process.exitCode = 1;
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const result = await fetchPublishedVersions(name, fetch);
|
|
118
|
+
if (!result.ok) {
|
|
119
|
+
console.log(
|
|
120
|
+
`check-publish-version: could not check the npm registry for "${name}" -- skipping ` +
|
|
121
|
+
`(${result.reason}; best-effort check, a real duplicate version still fails at publish time)`,
|
|
122
|
+
);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (result.versions.includes(version)) {
|
|
126
|
+
console.error(
|
|
127
|
+
`check-publish-version: ${name}@${version} is already published -- bump the version ` +
|
|
128
|
+
"(or add a changeset, if this project uses one) before releasing again",
|
|
129
|
+
);
|
|
130
|
+
process.exitCode = 1;
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (result.versions.length === 0) {
|
|
134
|
+
console.log(
|
|
135
|
+
`check-publish-version: ok -- ${name}@${version} (the registry has no published versions ` +
|
|
136
|
+
"for this name -- either it has never published, or it's a scoped/restricted package an " +
|
|
137
|
+
"anonymous lookup can't see)",
|
|
138
|
+
);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
console.log(`check-publish-version: ok -- ${name}@${version} is unpublished`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (
|
|
145
|
+
process.argv[1] !== undefined &&
|
|
146
|
+
fileURLToPath(import.meta.url) === realpathSync(process.argv[1])
|
|
147
|
+
) {
|
|
148
|
+
await main();
|
|
149
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SPDX-FileCopyrightText: Copyright the __PROJECT_NAME__ contributors
|
|
3
|
+
// SPDX-License-Identifier: NOASSERTION
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Translates the one `pnpm publish` invocation changesets makes for a packed
|
|
7
|
+
* tarball into the equivalent `npm stage publish`, for `pnpm-publish-shim.mjs`.
|
|
8
|
+
*
|
|
9
|
+
* Changesets runs (see its `lib/pnpm.ts`):
|
|
10
|
+
* pnpm publish <tarball> [--json] --access <a> --tag <t> --no-git-checks [--otp <c>]
|
|
11
|
+
*
|
|
12
|
+
* `--json` and `--no-git-checks` are pnpm-only and dropped. Anything not listed
|
|
13
|
+
* is refused rather than guessed at: if changesets ever starts passing a flag
|
|
14
|
+
* this does not understand, publishing must stop, not silently do something else.
|
|
15
|
+
*
|
|
16
|
+
* Staged, not direct: a trusted publisher created since 2026-09-03 defaults to
|
|
17
|
+
* `npm stage publish` only, npm's own default (and explicit recommendation) --
|
|
18
|
+
* a maintainer must separately run `npm stage approve <id>` (2FA, never
|
|
19
|
+
* automatable) before a staged version actually goes live. `npm stage publish`
|
|
20
|
+
* accepts the same `--access`/`--tag`/`--otp`/`--provenance` flags as
|
|
21
|
+
* `npm publish` ("parity with npm publish" per npm's own CLI reference); the
|
|
22
|
+
* command itself is `npm stage publish`, not a flag on `npm publish`.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const DROPPED = new Set(["--json", "--no-git-checks"]);
|
|
26
|
+
const WITH_VALUE = new Set(["--access", "--tag", "--otp"]);
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {string[]} args the arguments following `pnpm publish`
|
|
30
|
+
* @returns {string[]} the argument vector for `npm stage publish`
|
|
31
|
+
*/
|
|
32
|
+
export function toNpmStagePublishArgs(args) {
|
|
33
|
+
let tarball;
|
|
34
|
+
const flags = [];
|
|
35
|
+
|
|
36
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
37
|
+
const arg = args[i];
|
|
38
|
+
if (DROPPED.has(arg)) continue;
|
|
39
|
+
|
|
40
|
+
if (WITH_VALUE.has(arg)) {
|
|
41
|
+
const value = args[i + 1];
|
|
42
|
+
if (value === undefined || value.startsWith("--")) {
|
|
43
|
+
throw new Error(`${arg} needs a value`);
|
|
44
|
+
}
|
|
45
|
+
flags.push(arg, value);
|
|
46
|
+
i += 1;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (arg.startsWith("-")) {
|
|
51
|
+
throw new Error(`unsupported flag ${arg}`);
|
|
52
|
+
}
|
|
53
|
+
if (tarball !== undefined) {
|
|
54
|
+
throw new Error(`expected one tarball, got a second argument: ${arg}`);
|
|
55
|
+
}
|
|
56
|
+
tarball = arg;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (tarball === undefined) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
"no tarball given: this shim only publishes packed tarballs (changesets' --from-pack-dir mode)",
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Provenance is requested explicitly rather than relied on: npm's automatic
|
|
66
|
+
// attestation for trusted publishing has been reported not to engage without it.
|
|
67
|
+
// "stage", "publish" are two separate words (the npm-stage subcommand), not
|
|
68
|
+
// "stage-publish" or a flag -- see npm's own CLI reference for npm-stage.
|
|
69
|
+
return ["stage", "publish", tarball, ...flags, "--provenance"];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* `PATH` with `dir` removed, so the shim can find the real `pnpm` behind it.
|
|
74
|
+
* @param {string | undefined} pathValue
|
|
75
|
+
* @param {string | undefined} dir
|
|
76
|
+
* @param {string} separator
|
|
77
|
+
*/
|
|
78
|
+
export function pathWithout(pathValue, dir, separator) {
|
|
79
|
+
if (pathValue === undefined || dir === undefined || dir === "") {
|
|
80
|
+
return pathValue ?? "";
|
|
81
|
+
}
|
|
82
|
+
return pathValue
|
|
83
|
+
.split(separator)
|
|
84
|
+
.filter((entry) => entry !== dir)
|
|
85
|
+
.join(separator);
|
|
86
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SPDX-FileCopyrightText: Copyright the __PROJECT_NAME__ contributors
|
|
3
|
+
// SPDX-License-Identifier: NOASSERTION
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Put ahead of the real `pnpm` on PATH by the release workflow's `publish`
|
|
7
|
+
* job. Everything passes straight through to pnpm except `publish`, which
|
|
8
|
+
* this rewrites into `npm stage publish` instead.
|
|
9
|
+
*
|
|
10
|
+
* ## Why it exists
|
|
11
|
+
*
|
|
12
|
+
* Two independent reasons this can't be `pnpm publish`, stacked:
|
|
13
|
+
* 1. `changeset publish` in a pnpm-managed project publishes through
|
|
14
|
+
* `pnpm publish`, and pnpm 12's native publish does its own OIDC
|
|
15
|
+
* exchange, which npmjs.com rejects (`403 OIDC permission denied`) even
|
|
16
|
+
* for a correctly configured trusted publisher. The npm CLI's exchange
|
|
17
|
+
* works.
|
|
18
|
+
* 2. It can't be a direct `npm publish` either: a trusted publisher created
|
|
19
|
+
* since 2026-09-03 defaults to `npm stage publish` only, npm's own
|
|
20
|
+
* explicit recommendation (see this pack's adoptNotes). A direct
|
|
21
|
+
* `npm publish` gets the exact same 403 "OIDC permission denied" as the
|
|
22
|
+
* pnpm case, for an unrelated reason: the token exchange succeeds, but
|
|
23
|
+
* the registry refuses the specific action.
|
|
24
|
+
*
|
|
25
|
+
* ## What it translates
|
|
26
|
+
*
|
|
27
|
+
* Only the exact `pnpm publish` invocation changesets makes -- everything
|
|
28
|
+
* else (install, other pnpm subcommands) passes straight through to the real
|
|
29
|
+
* `pnpm` unmodified. Routing only the publish keeps changesets in charge of
|
|
30
|
+
* everything else it does around it -- the publish plan, ordering, git tags,
|
|
31
|
+
* GitHub Releases -- instead of reimplementing any of that. A consequence
|
|
32
|
+
* worth knowing: those git tags and Releases are created the moment
|
|
33
|
+
* `npm stage publish` succeeds, which is *before* the package is actually
|
|
34
|
+
* live -- staging still needs a maintainer to run `npm stage approve <id>`
|
|
35
|
+
* (2FA, never automatable).
|
|
36
|
+
*
|
|
37
|
+
* ## What it refuses
|
|
38
|
+
*
|
|
39
|
+
* `toNpmStagePublishArgs` (see `./lib/npm-publish-args.mjs`) accepts only the
|
|
40
|
+
* flag shape changesets actually passes and throws on anything it does not
|
|
41
|
+
* recognise, rather than guessing a translation for an unfamiliar flag.
|
|
42
|
+
*
|
|
43
|
+
* `PNPM_SHIM_DIR` names this shim's own directory so it can be dropped from
|
|
44
|
+
* PATH when looking for the real pnpm (otherwise it would find itself).
|
|
45
|
+
* Delete this, and the workflow step that installs it, once pnpm's publish
|
|
46
|
+
* authenticates against npmjs.com.
|
|
47
|
+
*/
|
|
48
|
+
import process from "node:process";
|
|
49
|
+
import { spawnSync } from "node:child_process";
|
|
50
|
+
import { delimiter } from "node:path";
|
|
51
|
+
import { pathWithout, toNpmStagePublishArgs } from "./lib/npm-publish-args.mjs";
|
|
52
|
+
|
|
53
|
+
const args = process.argv.slice(2);
|
|
54
|
+
|
|
55
|
+
let command;
|
|
56
|
+
let commandArgs;
|
|
57
|
+
if (args[0] === "publish") {
|
|
58
|
+
try {
|
|
59
|
+
command = "npm";
|
|
60
|
+
commandArgs = toNpmStagePublishArgs(args.slice(1));
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error(
|
|
63
|
+
`pnpm-publish-shim: ${error instanceof Error ? error.message : String(error)}`,
|
|
64
|
+
);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
command = "pnpm";
|
|
69
|
+
commandArgs = args;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const result = spawnSync(command, commandArgs, {
|
|
73
|
+
stdio: "inherit",
|
|
74
|
+
env: {
|
|
75
|
+
...process.env,
|
|
76
|
+
PATH: pathWithout(process.env.PATH, process.env.PNPM_SHIM_DIR, delimiter),
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (result.error) {
|
|
81
|
+
console.error(
|
|
82
|
+
`pnpm-publish-shim: could not run ${command}: ${result.error.message}`,
|
|
83
|
+
);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
process.exit(result.status ?? 1);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"name": "publishing",
|
|
4
|
+
"description": "A release pipeline for a project that ships an npm package. Release half: release.yml (changesets version-PR / staged, provenance-attested npm publish via trusted publishing OIDC, including a direct check-publish-version.mjs call right before packing), .changeset/config.json + README.md, the pnpm-to-npm-stage-publish shim, and one build-group gate (check-dts-deps.mjs). Also ships check-license-headers.mjs and a REUSE.toml template as a lint-group gate (secret scanning and Scorecard are the separate `supply-chain` pack). Not adopt-capable: the release flow assumes decisions (registry access, npm trusted-publisher setup, a GitHub App for the version PR) too project-specific to install blind -- see adoptNotes.",
|
|
5
|
+
"modes": ["fresh"],
|
|
6
|
+
"budget": {
|
|
7
|
+
"agents": 0,
|
|
8
|
+
"skills": 0,
|
|
9
|
+
"hooks": 0,
|
|
10
|
+
"workflows": 1,
|
|
11
|
+
"scripts": 2
|
|
12
|
+
},
|
|
13
|
+
"wiring": {
|
|
14
|
+
"settings": {},
|
|
15
|
+
"packageScripts": {
|
|
16
|
+
"changeset": "changeset",
|
|
17
|
+
"version:packages": "changeset version && pnpm install --lockfile-only"
|
|
18
|
+
},
|
|
19
|
+
"verifySteps": [
|
|
20
|
+
{
|
|
21
|
+
"id": "dts-deps",
|
|
22
|
+
"group": "build",
|
|
23
|
+
"name": "Check .d.ts dependency declarations",
|
|
24
|
+
"cmd": ["node", "bin/check-dts-deps.mjs"]
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"id": "license-headers",
|
|
28
|
+
"group": "lint",
|
|
29
|
+
"name": "Check SPDX license headers",
|
|
30
|
+
"cmd": ["node", "bin/check-license-headers.mjs"]
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
},
|
|
34
|
+
"adoptNotes": "REQUIRED right after install, before the first pnpm verify -- two one-time steps, or pnpm verify fails immediately, not from a real regression but from two setup gaps this pack cannot close itself: (1) `pnpm add -D @changesets/cli` -- the wiring contract only extends package.json's scripts, never its dependencies (see templates/packs/README.md), so the wired `changeset`/`version:packages` scripts reference a binary knip's own unlisted-binaries check correctly flags as missing until this is installed; (2) `node bin/check-license-headers.mjs --fix`, then commit the result -- templates/core has never had a license-header gate before this pack, so every one of its own files (hooks, bin/ scripts, CI workflows, the placeholder src/tests, and the workflows of any other installed pack such as `github` or `supply-chain`) is missing the SPDX header this pack's own license-headers gate now requires, the same one-time cost this repo itself paid when it adopted the same gate (see CLAUDE.md's Definition of Done). In a hand-copied install the CLI does not substitute tokens, so replace every literal `__PROJECT_NAME__` in the copied files (REUSE.toml, the SPDX headers in bin/, the workflows) with the project's real name first. Fresh mode only: a release pipeline encodes decisions (whether the package is actually public, which registry, whether a GitHub App backs the version PR) this pack cannot survey or guess at safely, so it is staged like every other pack (at .groundwork/packs/publishing/) but never auto-installed in adopt mode -- if an adopted project wants it, copy .groundwork/packs/publishing/files/ by hand, dropping the `.staged` suffix from every name (or copy templates/packs/publishing/files/ from this repo directly) and work through the one-time setup below. check-publish-version.mjs is deliberately NOT one of this pack's wired verify steps: with changesets, package.json's version on the release branch is always the one just published between releases, so a version-already-published check running on every ordinary push (via pnpm verify / pre-push) would fail every single time until the next version PR lands -- it is instead invoked directly as a step inside release.yml's pack job, the one place \"is this version about to collide with an already-published one\" is actually the right question to ask. The two remaining verify-group gates (dts-deps, license-headers) install anywhere a bin/verify.mjs-shaped gate runner exists; wire them into the project's real one if it differs. Both check-publish-version.mjs and check-dts-deps.mjs no-op (checked: 0, not a failure) on a package.json with `private: true` (the baseline's own default) or (check-dts-deps.mjs only) with no dist/**/*.d.ts yet -- flip `private` to `false` (and set a real `name`) once the package is actually meant to publish. The pack assumes a public package: `.changeset/config.json` ships `access: public`, npm refuses `restricted` for an unscoped name, and the publish shim always passes `--provenance`, which npm ties to a public source repository -- a private scoped package needs `access: restricted` and provenance removed from bin/lib/npm-publish-args.mjs. `@changesets/changelog-github` (and its `repo` option) is a drop-in swap for `.changeset/config.json`'s plain default changelog generator, once this project's GitHub repository is known, for changelog entries that link back to the originating PR/commit -- see `.changeset/README.md`. One-time setup before release.yml's first real run (distinct from the two pnpm-verify prerequisites above), distilled from this repo's own .claude/rules/releases.md: (1) npm cannot configure a trusted publisher for a package that doesn't exist yet -- publish once by hand with a temporary token first; (2) the trusted publisher is bound to the exact workflow filename `release.yml` -- renaming or moving it breaks publishing until reconfigured on npmjs.com; (3) set the trusted publisher's allowed actions to staged-only (`npm stage publish`, npm's own default and recommendation since 2026-09-03) -- a maintainer runs `npm stage approve <id>` (2FA, never automatable) before a version actually installs; (4) create an `npm-publish` GitHub environment (via `gh api`, not committed as JSON) with a required reviewer, restricted to the release branch, so the `publish` job itself pauses before the git tag/Release/stage-publish exist; (5) if branch protection requires status checks on every PR with no bypass actor, the version-PR job needs a GitHub App installation token (`APP_CLIENT_ID`/`APP_PRIVATE_KEY` repo secrets), not the default GITHUB_TOKEN, or those checks never trigger and the PR can never merge -- if this project has no such rule, `github-token: ${{ secrets.GITHUB_TOKEN }}` is enough and the app-token step can be dropped. release.yml pins `.github/release-tools/` (the npm CLI version) but the baseline's dependabot.yml only covers the github-actions ecosystem, so that pin goes stale unless you add an `npm` entry with `directory: \"/.github/release-tools\"` to the project's own .github/dependabot.yml. Security note, as of 2026-10-01: the npm pinned in the shipped `.github/release-tools/package-lock.json` (11.20.0) bundles undici 6.28.0, ip-address 10.5.0 and brace-expansion 5.0.9, which carry open advisories; every npm release found that day (11.20.0, 11.21.0, 12.2.0) bundles the same versions, and overrides cannot change bundled dependencies. Reading the npm tarballs and lockfile, no reach was found from what the shipped workflow runs (`npm ci --ignore-scripts`, `npm stage publish <tarball>` and `npm stage list`; the tarball is packed earlier by pnpm, not npm, and the shipped workflow configures no proxy; if your runner sets one, for example HTTPS_PROXY on a self-hosted runner behind an egress proxy, the ip-address advisories may apply), and the minimatch callers on those commands (Arborist, and tuf-js under sigstore) take their patterns from your own lockfile and from signed registry metadata, but that is analysis, not a run of the code. Re-check on the day of each release and bump the pinned npm as soon as a release bundling undici >= 6.28.1, ip-address >= 10.7.1 and brace-expansion >= 5.0.12 exists. The secret-scanning and Scorecard workflows that used to ship here are now the separate `supply-chain` pack."
|
|
35
|
+
}
|
|
@@ -19,9 +19,11 @@
|
|
|
19
19
|
* oversized file is asking its reviewer to accept that debt, not silently
|
|
20
20
|
* evading the gate.
|
|
21
21
|
*
|
|
22
|
-
* `ROOTS` defaults to this
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* `ROOTS` defaults to this project's flat `src/`/`tests/` layout (not to be
|
|
23
|
+
* confused with the "baseline" ratchet file above -- this is about where
|
|
24
|
+
* your source lives, not about recorded debt). If `/customize` or a later
|
|
25
|
+
* refactor moves to a `packages/*` monorepo shape, update `ROOTS` to list
|
|
26
|
+
* each package's `src`/`tests` pair.
|
|
25
27
|
*
|
|
26
28
|
* Usage:
|
|
27
29
|
* node bin/check-file-budget.mjs # verify (fails on growth/new-over-ceiling)
|
|
@@ -338,7 +340,7 @@ if (isEntryPoint()) {
|
|
|
338
340
|
reporter.fail(
|
|
339
341
|
ref
|
|
340
342
|
? `Could not scan the tracked roots at ${ref}: ${message}`
|
|
341
|
-
: `Could not scan
|
|
343
|
+
: `Could not scan the tracked roots: ${message}`,
|
|
342
344
|
);
|
|
343
345
|
reporter.finish();
|
|
344
346
|
process.exit(1);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"name": "quality",
|
|
4
|
+
"description": "Two language-level review aids for a TypeScript project: a per-file size ratchet gate over src/ and tests/ (check-file-budget.mjs plus its committed baseline, wired as a build-group verify step) and a type-design-analyzer review agent that rates the type design of changed exports on four dimensions. No hooks, no settings, no scripts.",
|
|
5
|
+
"modes": ["fresh", "adopt"],
|
|
6
|
+
"budget": {
|
|
7
|
+
"agents": 1,
|
|
8
|
+
"skills": 0,
|
|
9
|
+
"hooks": 0,
|
|
10
|
+
"workflows": 0,
|
|
11
|
+
"scripts": 0
|
|
12
|
+
},
|
|
13
|
+
"requires": {
|
|
14
|
+
"paths": ["bin/lib/report.mjs"]
|
|
15
|
+
},
|
|
16
|
+
"wiring": {
|
|
17
|
+
"settings": {},
|
|
18
|
+
"packageScripts": {},
|
|
19
|
+
"verifySteps": [
|
|
20
|
+
{
|
|
21
|
+
"id": "file-budget",
|
|
22
|
+
"group": "build",
|
|
23
|
+
"name": "Check file budget",
|
|
24
|
+
"cmd": ["node", "bin/check-file-budget.mjs"]
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
},
|
|
28
|
+
"adoptNotes": "check-file-budget.mjs's ROOTS default assumes a flat src/+tests/ layout and its ceilings are this baseline's defaults -- in an adopted project, re-point ROOTS at the project's real source layout (or drop the gate) before wiring it. check-file-budget.mjs imports ./lib/report.mjs, a baseline file that an adopted project will not have -- copy bin/lib/report.mjs alongside the gate, or treat the gate as not installable. The verify step assumes a bin/verify.mjs-shaped gate runner; if the project has none, install the agent and report the gate as not installable rather than inventing one. The type-design-analyzer agent has no such dependency and installs anywhere a .claude/agents/ directory exists; it is a read-only review spoke (model claude-opus-5 at xhigh effort, more expensive than the baseline's reviewers, which run claude-opus-5-5 at medium effort), and nothing in the baseline's hub-and-spoke instructions dispatches it by name -- ask for it after changing exported types, or add it to the project's own CLAUDE.md review step."
|
|
29
|
+
}
|