@williamthorsen/release-kit 10.0.0 → 10.2.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/.readyup/kits/default.js +293 -0
- package/.readyup/kits/npm-auto-publish.js +235 -0
- package/.readyup/manifest.json +28 -0
- package/CHANGELOG.md +30 -0
- package/README.md +37 -5
- package/dist/esm/loadConfig.js +11 -7
- package/package.json +7 -4
- package/presets/labels/common.yaml +6 -8
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/** @noformat — @generated. Do not edit. Compiled by rdy. */
|
|
2
|
+
/* eslint-disable */
|
|
3
|
+
export const __readyupVersion = "0.23.0";
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
// .readyup/kits/default.ts
|
|
7
|
+
import { defineRdyKit } from "readyup";
|
|
8
|
+
import {
|
|
9
|
+
discoverWorkspaces,
|
|
10
|
+
fileContains,
|
|
11
|
+
fileDoesNotContain,
|
|
12
|
+
fileExists,
|
|
13
|
+
fileMatchesHash,
|
|
14
|
+
hasDevDependency,
|
|
15
|
+
hasMinDevDependencyVersion,
|
|
16
|
+
readFile
|
|
17
|
+
} from "readyup/check-utils";
|
|
18
|
+
|
|
19
|
+
// src/init/detectRepoType.ts
|
|
20
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
21
|
+
|
|
22
|
+
// src/typeGuards.ts
|
|
23
|
+
function isRecord(value) {
|
|
24
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// src/init/parseJsonRecord.ts
|
|
28
|
+
function parseJsonRecord(raw) {
|
|
29
|
+
let parsed;
|
|
30
|
+
try {
|
|
31
|
+
parsed = JSON.parse(raw);
|
|
32
|
+
} catch {
|
|
33
|
+
return void 0;
|
|
34
|
+
}
|
|
35
|
+
return isRecord(parsed) ? parsed : void 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// src/init/detectRepoType.ts
|
|
39
|
+
function detectRepoType() {
|
|
40
|
+
if (existsSync("pnpm-workspace.yaml")) {
|
|
41
|
+
return "monorepo";
|
|
42
|
+
}
|
|
43
|
+
if (existsSync("package.json")) {
|
|
44
|
+
const raw = readFileSync("package.json", "utf8");
|
|
45
|
+
const pkg = parseJsonRecord(raw);
|
|
46
|
+
if (pkg !== void 0 && Array.isArray(pkg.workspaces)) {
|
|
47
|
+
return "monorepo";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return "single-package";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// .readyup/kits/default.ts
|
|
54
|
+
function getMinVersion() {
|
|
55
|
+
const picked = { "version": "10.2.0" };
|
|
56
|
+
if (typeof picked.version !== "string") {
|
|
57
|
+
throw new TypeError("release-kit/package.json: 'version' must be a string");
|
|
58
|
+
}
|
|
59
|
+
return picked.version;
|
|
60
|
+
}
|
|
61
|
+
function hasPublishablePackages() {
|
|
62
|
+
return discoverWorkspaces({ filter: (w) => w.isPackage }).length > 0;
|
|
63
|
+
}
|
|
64
|
+
var CLIFF_TEMPLATE_HASH = "93b72e0b1393cd6b1fe8e2a0e303cd326fd323435951b0493396b305af32d2ec";
|
|
65
|
+
var COMMON_PRESET_HASH = "86f9e1db9000793a91168e8c6b5695311a422ee208121324549c068fe67fa184";
|
|
66
|
+
var SYNC_LABELS_WORKFLOW_HASH = "d6e2403fb551d2d415f679125989c92760444eec887644565b2e05c9bf8f4c1e";
|
|
67
|
+
var RELEASE_WORKFLOW_HASH_MONOREPO = "0a9724b7b3c5e24087fd3a8f36fed8e990d699267fcf36028ce048ab40dc2946";
|
|
68
|
+
var RELEASE_WORKFLOW_HASH_SINGLE = "a3d19bbc1ba8bb30622e53c590137b97e3179e80988c0967737b021cdaeab73f";
|
|
69
|
+
var PUBLISH_WORKFLOW_HASH_MONOREPO = "0afa9ffe914f3dc8f043e68252ebc604c8cc1a953422fcea37a909a4def370ee";
|
|
70
|
+
var PUBLISH_WORKFLOW_HASH_SINGLE = "6f31183e0a1e66be791a19266c3b028dadbd9fe010f7fc4452f3f8970c937b43";
|
|
71
|
+
var default_default = defineRdyKit({
|
|
72
|
+
checklists: [
|
|
73
|
+
{
|
|
74
|
+
name: "release-kit",
|
|
75
|
+
checks: [
|
|
76
|
+
{
|
|
77
|
+
name: "@williamthorsen/release-kit in devDependencies",
|
|
78
|
+
severity: "error",
|
|
79
|
+
check: () => hasDevDependency("@williamthorsen/release-kit"),
|
|
80
|
+
fix: "pnpm add --save-dev @williamthorsen/release-kit",
|
|
81
|
+
checks: [
|
|
82
|
+
{
|
|
83
|
+
get name() {
|
|
84
|
+
return `@williamthorsen/release-kit >= ${getMinVersion()}`;
|
|
85
|
+
},
|
|
86
|
+
severity: "error",
|
|
87
|
+
check: () => hasMinDevDependencyVersion("@williamthorsen/release-kit", getMinVersion(), {
|
|
88
|
+
exempt: (range) => range.startsWith("workspace:")
|
|
89
|
+
}),
|
|
90
|
+
get fix() {
|
|
91
|
+
return `pnpm add --save-dev @williamthorsen/release-kit@^${getMinVersion()}`;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
]
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: "release.yaml workflow exists",
|
|
98
|
+
severity: "warn",
|
|
99
|
+
check: () => fileExists(".github/workflows/release.yaml"),
|
|
100
|
+
fix: "Add .github/workflows/release.yaml using the release workflow template",
|
|
101
|
+
checks: [
|
|
102
|
+
{
|
|
103
|
+
name: "release.yaml matches template",
|
|
104
|
+
severity: "warn",
|
|
105
|
+
check: () => {
|
|
106
|
+
const hash = detectRepoType() === "monorepo" ? RELEASE_WORKFLOW_HASH_MONOREPO : RELEASE_WORKFLOW_HASH_SINGLE;
|
|
107
|
+
return fileMatchesHash(".github/workflows/release.yaml", hash);
|
|
108
|
+
},
|
|
109
|
+
fix: "Run `release-kit init --force` to regenerate release.yaml from the current template"
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
name: "release.yaml does not reference deprecated tag ref",
|
|
113
|
+
severity: "error",
|
|
114
|
+
check: () => fileDoesNotContain(".github/workflows/release.yaml", /@(release|publish)-workflow-v[0-9]/),
|
|
115
|
+
fix: "Update release.yaml to use @workflow/release-v1 (run `release-kit init --force` to regenerate, or replace the ref manually)"
|
|
116
|
+
}
|
|
117
|
+
]
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: "publish.yaml workflow exists",
|
|
121
|
+
severity: "warn",
|
|
122
|
+
skip: () => !hasPublishablePackages() ? "no publishable packages" : false,
|
|
123
|
+
check: () => fileExists(".github/workflows/publish.yaml"),
|
|
124
|
+
fix: "Add .github/workflows/publish.yaml using the publish workflow template",
|
|
125
|
+
checks: [
|
|
126
|
+
{
|
|
127
|
+
name: "publish.yaml matches template",
|
|
128
|
+
severity: "warn",
|
|
129
|
+
check: () => {
|
|
130
|
+
const hash = detectRepoType() === "monorepo" ? PUBLISH_WORKFLOW_HASH_MONOREPO : PUBLISH_WORKFLOW_HASH_SINGLE;
|
|
131
|
+
return fileMatchesHash(".github/workflows/publish.yaml", hash);
|
|
132
|
+
},
|
|
133
|
+
fix: "Run `release-kit init --force` to regenerate publish.yaml from the current template"
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
name: "publish.yaml does not reference deprecated tag ref",
|
|
137
|
+
severity: "error",
|
|
138
|
+
check: () => fileDoesNotContain(".github/workflows/publish.yaml", /@(release|publish)-workflow-v[0-9]/),
|
|
139
|
+
fix: "Update publish.yaml to use @workflow/publish-v1 (run `release-kit init --force` to regenerate, or replace the ref manually)"
|
|
140
|
+
}
|
|
141
|
+
]
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: "releaseNotes config is consistent with changelogJson",
|
|
145
|
+
severity: "warn",
|
|
146
|
+
skip: () => !fileExists(".config/release-kit.config.ts") ? "no release-kit config file" : false,
|
|
147
|
+
check: () => releaseNotesConfigIsConsistent(),
|
|
148
|
+
fix: "Either enable changelogJson.enabled or disable releaseNotes.shouldInjectIntoReadme"
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
name: "config does not use removed releaseNotes.shouldCreateGithubRelease",
|
|
152
|
+
severity: "error",
|
|
153
|
+
skip: () => !fileExists(".config/release-kit.config.ts") ? "no release-kit config file" : false,
|
|
154
|
+
check: () => fileDoesNotContain(".config/release-kit.config.ts", /shouldCreateGithubRelease/),
|
|
155
|
+
fix: "Remove 'shouldCreateGithubRelease' from .config/release-kit.config.ts. Adoption of GitHub Releases is now signaled by installing the create-github-release workflow (see release-kit README for setup)."
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
name: ".config/release-kit.config.ts uses defineConfig",
|
|
159
|
+
severity: "recommend",
|
|
160
|
+
skip: () => !fileExists(".config/release-kit.config.ts") ? "no release-kit config file" : false,
|
|
161
|
+
check: () => fileContains(".config/release-kit.config.ts", /defineConfig/),
|
|
162
|
+
fix: "Wrap your config export with defineConfig() from @williamthorsen/release-kit/config for type safety"
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
name: "releaseNotes.shouldInjectIntoReadme is true",
|
|
166
|
+
severity: "warn",
|
|
167
|
+
skip: () => !fileExists(".config/release-kit.config.ts") ? "no release-kit config file" : false,
|
|
168
|
+
check: () => releaseNotesInjectsIntoReadme(),
|
|
169
|
+
fix: "Set releaseNotes.shouldInjectIntoReadme to true in .config/release-kit.config.ts",
|
|
170
|
+
checks: [
|
|
171
|
+
{
|
|
172
|
+
name: "README contains release-notes section markers",
|
|
173
|
+
severity: "warn",
|
|
174
|
+
check: readmesHaveReleaseNotesMarkers,
|
|
175
|
+
fix: "Add `<!-- section:release-notes -->` and `<!-- /section:release-notes -->` markers to each affected README"
|
|
176
|
+
}
|
|
177
|
+
]
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
name: "git-cliff not in devDependencies",
|
|
181
|
+
severity: "recommend",
|
|
182
|
+
check: () => !hasDevDependency("git-cliff"),
|
|
183
|
+
fix: "pnpm remove git-cliff \u2014 release-kit handles changelog generation directly"
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
name: ".config/git-cliff.toml matches current template",
|
|
187
|
+
severity: "warn",
|
|
188
|
+
skip: () => !fileExists(".config/git-cliff.toml") ? "no local cliff config (using fallback)" : false,
|
|
189
|
+
check: () => fileMatchesHash(".config/git-cliff.toml", CLIFF_TEMPLATE_HASH),
|
|
190
|
+
fix: "Update .config/git-cliff.toml to match the current cliff.toml.template from release-kit, or delete it to use the bundled fallback"
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
name: "sync-labels.yaml workflow exists",
|
|
194
|
+
severity: "warn",
|
|
195
|
+
check: () => fileExists(".github/workflows/sync-labels.yaml"),
|
|
196
|
+
fix: "Run `release-kit sync-labels init` to scaffold the workflow",
|
|
197
|
+
checks: [
|
|
198
|
+
{
|
|
199
|
+
name: "sync-labels.yaml matches template",
|
|
200
|
+
severity: "warn",
|
|
201
|
+
check: () => fileMatchesHash(".github/workflows/sync-labels.yaml", SYNC_LABELS_WORKFLOW_HASH),
|
|
202
|
+
fix: "Run `release-kit sync-labels init --force` to regenerate the workflow from the current template"
|
|
203
|
+
}
|
|
204
|
+
]
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
name: "sync-labels.yaml does not reference deprecated tag ref",
|
|
208
|
+
severity: "error",
|
|
209
|
+
check: () => fileDoesNotContain(".github/workflows/sync-labels.yaml", /@sync-labels-workflow-v[0-9]/),
|
|
210
|
+
fix: "Update sync-labels.yaml to use @workflow/sync-labels-v1 (run `release-kit sync-labels init --force` to regenerate, or replace the ref manually)"
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
name: "retired .config/sync-labels.config.ts is absent",
|
|
214
|
+
severity: "error",
|
|
215
|
+
check: () => !fileExists(".config/sync-labels.config.ts"),
|
|
216
|
+
fix: "Move the labels into the repoLabels block of .config/release-kit.config.ts, then delete .config/sync-labels.config.ts"
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
name: "repoLabels block declared in .config/release-kit.config.ts",
|
|
220
|
+
severity: "recommend",
|
|
221
|
+
skip: () => !fileExists(".config/release-kit.config.ts") ? "no release-kit config file" : false,
|
|
222
|
+
check: () => fileContains(".config/release-kit.config.ts", /repoLabels/),
|
|
223
|
+
fix: "Run `release-kit sync-labels init` to seed a repoLabels block, then customize labels"
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
name: ".github/labels.yaml exists",
|
|
227
|
+
severity: "warn",
|
|
228
|
+
skip: () => !fileContains(".config/release-kit.config.ts", /repoLabels/) ? "no repoLabels config" : false,
|
|
229
|
+
check: () => fileExists(".github/labels.yaml"),
|
|
230
|
+
fix: "Run `release-kit sync-labels generate` to produce the labels file",
|
|
231
|
+
checks: [
|
|
232
|
+
{
|
|
233
|
+
name: "labels.yaml has current common preset",
|
|
234
|
+
severity: "warn",
|
|
235
|
+
check: () => labelsHaveCurrentPresetHash("common", COMMON_PRESET_HASH),
|
|
236
|
+
fix: "Run `release-kit sync-labels generate` to incorporate updated common labels"
|
|
237
|
+
}
|
|
238
|
+
]
|
|
239
|
+
}
|
|
240
|
+
]
|
|
241
|
+
}
|
|
242
|
+
]
|
|
243
|
+
});
|
|
244
|
+
function labelsHaveCurrentPresetHash(presetName, expectedHash) {
|
|
245
|
+
const content = readFile(".github/labels.yaml");
|
|
246
|
+
if (content === void 0) return false;
|
|
247
|
+
const pattern = new RegExp(`^# ${presetName} preset hash: (.+)$`, "m");
|
|
248
|
+
const match = pattern.exec(content);
|
|
249
|
+
return match !== null && match[1] === expectedHash;
|
|
250
|
+
}
|
|
251
|
+
function readmeHasReleaseNotesMarkers(content) {
|
|
252
|
+
return content.includes("<!-- section:release-notes -->") && content.includes("<!-- /section:release-notes -->");
|
|
253
|
+
}
|
|
254
|
+
function readmesHaveReleaseNotesMarkers() {
|
|
255
|
+
const failing = [];
|
|
256
|
+
for (const { dir } of discoverWorkspaces({ filter: (w) => w.isPackage })) {
|
|
257
|
+
const readmePath = dir === "." ? "README.md" : `${dir}/README.md`;
|
|
258
|
+
const content = readFile(readmePath);
|
|
259
|
+
if (content === void 0 || !readmeHasReleaseNotesMarkers(content)) {
|
|
260
|
+
failing.push(readmePath);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (failing.length === 0) return true;
|
|
264
|
+
return {
|
|
265
|
+
ok: false,
|
|
266
|
+
detail: `missing markers or README: ${failing.join(", ")}`
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
function releaseNotesConfigIsConsistent() {
|
|
270
|
+
const content = readFile(".config/release-kit.config.ts");
|
|
271
|
+
if (content === void 0) return true;
|
|
272
|
+
const changelogJsonDisabled = /changelogJson\s*:\s*\{[^}]*enabled\s*:\s*false/.test(content);
|
|
273
|
+
if (!changelogJsonDisabled) return true;
|
|
274
|
+
const hasReadmeInjection = /shouldInjectIntoReadme\s*:\s*true/.test(content);
|
|
275
|
+
return !hasReadmeInjection;
|
|
276
|
+
}
|
|
277
|
+
function releaseNotesInjectsIntoReadme() {
|
|
278
|
+
const content = readFile(".config/release-kit.config.ts");
|
|
279
|
+
if (content === void 0) return false;
|
|
280
|
+
return /shouldInjectIntoReadme\s*:\s*true/.test(content);
|
|
281
|
+
}
|
|
282
|
+
export {
|
|
283
|
+
CLIFF_TEMPLATE_HASH,
|
|
284
|
+
COMMON_PRESET_HASH,
|
|
285
|
+
PUBLISH_WORKFLOW_HASH_MONOREPO,
|
|
286
|
+
PUBLISH_WORKFLOW_HASH_SINGLE,
|
|
287
|
+
RELEASE_WORKFLOW_HASH_MONOREPO,
|
|
288
|
+
RELEASE_WORKFLOW_HASH_SINGLE,
|
|
289
|
+
SYNC_LABELS_WORKFLOW_HASH,
|
|
290
|
+
default_default as default,
|
|
291
|
+
readmeHasReleaseNotesMarkers,
|
|
292
|
+
readmesHaveReleaseNotesMarkers
|
|
293
|
+
};
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/** @noformat — @generated. Do not edit. Compiled by rdy. */
|
|
2
|
+
/* eslint-disable */
|
|
3
|
+
export const __readyupVersion = "0.23.0";
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
// .readyup/kits/npm-auto-publish.ts
|
|
7
|
+
import { execSync } from "node:child_process";
|
|
8
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { defineRdyChecklist, defineRdyKit, defineRdyStagedChecklist } from "readyup";
|
|
11
|
+
import {
|
|
12
|
+
discoverWorkspaces,
|
|
13
|
+
fileContains,
|
|
14
|
+
fileExists,
|
|
15
|
+
getJsonValue,
|
|
16
|
+
isRecord,
|
|
17
|
+
readFile,
|
|
18
|
+
readJsonFile
|
|
19
|
+
} from "readyup/check-utils";
|
|
20
|
+
var PUBLISH_WORKFLOW_FILE = "publish.yaml";
|
|
21
|
+
var repoChecklist = defineRdyStagedChecklist({
|
|
22
|
+
name: "repo",
|
|
23
|
+
preconditions: [
|
|
24
|
+
{
|
|
25
|
+
name: "publish.yaml exists",
|
|
26
|
+
check: () => fileExists(".github/workflows/publish.yaml"),
|
|
27
|
+
fix: 'Run "release-kit init" to scaffold the publish workflow, or create .github/workflows/publish.yaml manually'
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
groups: [
|
|
31
|
+
[
|
|
32
|
+
{
|
|
33
|
+
name: "id-token: write permission declared",
|
|
34
|
+
check: () => fileContains(".github/workflows/publish.yaml", /id-token:\s*write/),
|
|
35
|
+
fix: 'Add "permissions: { id-token: write, contents: read }" to .github/workflows/publish.yaml \u2014 required for OIDC-based npm authentication'
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: "No legacy token references in workflow files",
|
|
39
|
+
check: () => !hasTokenReferences(),
|
|
40
|
+
fix: "Remove NPM_TOKEN/NODE_AUTH_TOKEN references from workflow files; OIDC auth replaces token-based auth"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: "Provenance setting matches repo visibility",
|
|
44
|
+
check: checkProvenanceMatchesVisibility
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
]
|
|
48
|
+
});
|
|
49
|
+
var packagesChecklist = defineRdyChecklist({
|
|
50
|
+
name: "packages",
|
|
51
|
+
preconditions: [
|
|
52
|
+
{
|
|
53
|
+
name: 'packageManager field starts with "pnpm"',
|
|
54
|
+
check: () => {
|
|
55
|
+
const rootPkg = readJsonFile("package.json");
|
|
56
|
+
const pm = typeof rootPkg?.packageManager === "string" ? rootPkg.packageManager : "";
|
|
57
|
+
return pm.startsWith("pnpm");
|
|
58
|
+
},
|
|
59
|
+
fix: 'Set "packageManager": "pnpm@..." in root package.json'
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: "At least one workspace discovered",
|
|
63
|
+
check: () => discoverWorkspaces().length > 0,
|
|
64
|
+
fix: "Ensure pnpm-workspace.yaml lists package globs, or that a root package.json exists"
|
|
65
|
+
}
|
|
66
|
+
],
|
|
67
|
+
get checks() {
|
|
68
|
+
return discoverWorkspaces().map((workspace) => buildWorkspaceCheck(workspace));
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
var npm_auto_publish_default = defineRdyKit({
|
|
72
|
+
fixLocation: "inline",
|
|
73
|
+
checklists: [repoChecklist, packagesChecklist]
|
|
74
|
+
});
|
|
75
|
+
function buildWorkspaceCheck(workspace) {
|
|
76
|
+
const displayName = workspace.name ?? "(unnamed)";
|
|
77
|
+
const pkgJsonPath = path.join(workspace.dir, "package.json");
|
|
78
|
+
const children = [
|
|
79
|
+
{
|
|
80
|
+
name: "repository field exists",
|
|
81
|
+
check: () => workspace.packageJson.repository !== void 0 && workspace.packageJson.repository !== null,
|
|
82
|
+
fix: `Add a "repository" field to ${pkgJsonPath} pointing to the GitHub repo`
|
|
83
|
+
}
|
|
84
|
+
];
|
|
85
|
+
if (workspace.name?.startsWith("@")) {
|
|
86
|
+
children.push({
|
|
87
|
+
name: 'publishConfig.access is "public"',
|
|
88
|
+
check: () => {
|
|
89
|
+
const access = getJsonValue(workspace.packageJson, "publishConfig", "access");
|
|
90
|
+
return typeof access === "string" && access === "public";
|
|
91
|
+
},
|
|
92
|
+
fix: `Add "publishConfig": { "access": "public" } to ${pkgJsonPath}`
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
children.push(
|
|
96
|
+
{
|
|
97
|
+
name: "published to npm",
|
|
98
|
+
check: () => isPublishedToNpm(displayName),
|
|
99
|
+
fix: `Run "npm publish --access public" from ${workspace.dir} to bootstrap the package on npm`,
|
|
100
|
+
checks: [
|
|
101
|
+
{
|
|
102
|
+
name: "trusted publisher configured",
|
|
103
|
+
check: () => hasTrustedPublisher(displayName, getCachedOwnerRepo(), PUBLISH_WORKFLOW_FILE),
|
|
104
|
+
get fix() {
|
|
105
|
+
return `Run: npm trust github ${displayName} --repo ${getCachedOwnerRepo()} --file ${PUBLISH_WORKFLOW_FILE}`;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
]
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: "files field exists",
|
|
112
|
+
severity: "warn",
|
|
113
|
+
check: () => workspace.packageJson.files !== void 0,
|
|
114
|
+
fix: `Add a "files" field to ${pkgJsonPath} to control which files are included in the published tarball`
|
|
115
|
+
}
|
|
116
|
+
);
|
|
117
|
+
return {
|
|
118
|
+
name: displayName,
|
|
119
|
+
skip: () => skipIfNotPublishable(workspace),
|
|
120
|
+
check: () => true,
|
|
121
|
+
checks: children
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function checkProvenanceMatchesVisibility() {
|
|
125
|
+
const workflowPath = ".github/workflows/publish.yaml";
|
|
126
|
+
const content = readFile(workflowPath);
|
|
127
|
+
if (content === void 0) {
|
|
128
|
+
return { ok: false, detail: `Cannot read ${workflowPath} \u2014 check file permissions` };
|
|
129
|
+
}
|
|
130
|
+
const hasProvenance = parseProvenanceSetting(content);
|
|
131
|
+
let isPrivate;
|
|
132
|
+
try {
|
|
133
|
+
isPrivate = isRepoPrivate();
|
|
134
|
+
} catch {
|
|
135
|
+
return { ok: false, detail: "Install and authenticate the GitHub CLI: gh auth login" };
|
|
136
|
+
}
|
|
137
|
+
if (!isPrivate && !hasProvenance) {
|
|
138
|
+
return {
|
|
139
|
+
ok: false,
|
|
140
|
+
detail: "Set provenance: true in .github/workflows/publish.yaml \u2014 public repos should generate provenance attestations"
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
if (isPrivate && hasProvenance) {
|
|
144
|
+
return {
|
|
145
|
+
ok: false,
|
|
146
|
+
detail: "Make the GitHub repo public \u2014 OIDC publishing with provenance requires a public repo"
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
return { ok: true };
|
|
150
|
+
}
|
|
151
|
+
var cachedOwnerRepo;
|
|
152
|
+
function getCachedOwnerRepo() {
|
|
153
|
+
if (cachedOwnerRepo === void 0) {
|
|
154
|
+
cachedOwnerRepo = getOwnerRepo();
|
|
155
|
+
}
|
|
156
|
+
return cachedOwnerRepo;
|
|
157
|
+
}
|
|
158
|
+
function getOwnerRepo() {
|
|
159
|
+
const url = execSync("git remote get-url origin", {
|
|
160
|
+
encoding: "utf8"
|
|
161
|
+
}).trim();
|
|
162
|
+
const sshMatch = url.match(/git@github\.com:(.+?)(?:\.git)?$/);
|
|
163
|
+
if (sshMatch?.[1]) {
|
|
164
|
+
return sshMatch[1];
|
|
165
|
+
}
|
|
166
|
+
const httpsMatch = url.match(/github\.com\/(.+?)(?:\.git)?$/);
|
|
167
|
+
if (httpsMatch?.[1]) {
|
|
168
|
+
return httpsMatch[1];
|
|
169
|
+
}
|
|
170
|
+
throw new Error(`Cannot parse GitHub owner/repo from remote URL: ${url}`);
|
|
171
|
+
}
|
|
172
|
+
function hasTokenReferences() {
|
|
173
|
+
const workflowDir = path.resolve(process.cwd(), ".github/workflows");
|
|
174
|
+
if (!existsSync(workflowDir)) {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
const files = readdirSync(workflowDir).filter((f) => f.endsWith(".yaml") || f.endsWith(".yml"));
|
|
178
|
+
for (const file of files) {
|
|
179
|
+
const content = readFileSync(path.join(workflowDir, file), "utf8");
|
|
180
|
+
if (content.includes("NPM_TOKEN") || content.includes("NODE_AUTH_TOKEN")) {
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
function hasTrustedPublisher(packageName, expectedRepo, expectedFile) {
|
|
187
|
+
let output;
|
|
188
|
+
try {
|
|
189
|
+
output = execSync(`npm trust list ${packageName} --json`, {
|
|
190
|
+
encoding: "utf8",
|
|
191
|
+
stdio: "pipe"
|
|
192
|
+
});
|
|
193
|
+
} catch {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
let parsed;
|
|
197
|
+
try {
|
|
198
|
+
parsed = JSON.parse(output);
|
|
199
|
+
} catch {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
if (!isRecord(parsed)) {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
return parsed.type === "github" && parsed.repository === expectedRepo && parsed.file === expectedFile;
|
|
206
|
+
}
|
|
207
|
+
function isPublishedToNpm(packageName) {
|
|
208
|
+
try {
|
|
209
|
+
execSync(`npm view ${packageName} version`, {
|
|
210
|
+
encoding: "utf8",
|
|
211
|
+
stdio: "pipe"
|
|
212
|
+
});
|
|
213
|
+
return true;
|
|
214
|
+
} catch {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function isRepoPrivate() {
|
|
219
|
+
const ownerRepo = getOwnerRepo();
|
|
220
|
+
const result = execSync(`gh api repos/${ownerRepo} --jq .private`, {
|
|
221
|
+
encoding: "utf8"
|
|
222
|
+
}).trim();
|
|
223
|
+
return result === "true";
|
|
224
|
+
}
|
|
225
|
+
function parseProvenanceSetting(workflowContent) {
|
|
226
|
+
return /^[^#]*provenance:\s*['"]?true['"]?/im.test(workflowContent);
|
|
227
|
+
}
|
|
228
|
+
function skipIfNotPublishable(workspace) {
|
|
229
|
+
return workspace.isPackage ? false : "package.json#private is true";
|
|
230
|
+
}
|
|
231
|
+
export {
|
|
232
|
+
buildWorkspaceCheck,
|
|
233
|
+
npm_auto_publish_default as default,
|
|
234
|
+
skipIfNotPublishable
|
|
235
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"kits": [
|
|
4
|
+
{
|
|
5
|
+
"checklists": [
|
|
6
|
+
"release-kit"
|
|
7
|
+
],
|
|
8
|
+
"name": "default",
|
|
9
|
+
"path": "kits/default.js",
|
|
10
|
+
"readyupVersion": "0.23.0",
|
|
11
|
+
"source": "kits/default.ts",
|
|
12
|
+
"sourceHash": "afe26920",
|
|
13
|
+
"targetHash": "9b3ddc09"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"checklists": [
|
|
17
|
+
"repo",
|
|
18
|
+
"packages"
|
|
19
|
+
],
|
|
20
|
+
"name": "npm-auto-publish",
|
|
21
|
+
"path": "kits/npm-auto-publish.js",
|
|
22
|
+
"readyupVersion": "0.23.0",
|
|
23
|
+
"source": "kits/npm-auto-publish.ts",
|
|
24
|
+
"sourceHash": "71e894e6",
|
|
25
|
+
"targetHash": "c1c05255"
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
}
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## 10.2.0 — 2026-08-04
|
|
6
|
+
|
|
7
|
+
### 🎉 Features
|
|
8
|
+
|
|
9
|
+
- Add status labels and clear grouped-label descriptions (#585)
|
|
10
|
+
|
|
11
|
+
Adds two status labels (`status:blocked` and `status:on-hold`) to the common preset and removes descriptions from other scoped labels (`priority:` and `value:`) to keep scoped groups compact in the GitHub UI.
|
|
12
|
+
|
|
13
|
+
- Publish each package's readyup kit from the package that owns it (#590)
|
|
14
|
+
|
|
15
|
+
ReadyUp checks for `@williamthorsen/nmr`, `@williamthorsen/release-kit`, and `v11y-check` are now run directly against these packages, guaranteeing a version-appropriate check, instead of against their host repo. The package must be installed as a direct dependency in the calling repo.
|
|
16
|
+
|
|
17
|
+
The syntax is `rdy run --from npm:<package>`. `rdy run --packages` runs the kits contained in any packages listed in `.config/readyup.config.ts`. Requires `readyup` 0.23 or later.
|
|
18
|
+
|
|
19
|
+
## 10.1.0 — 2026-08-03
|
|
20
|
+
|
|
21
|
+
### 🎉 Features
|
|
22
|
+
|
|
23
|
+
- Publish nmr's readyup kit with a test-tier conformance check (#580)
|
|
24
|
+
|
|
25
|
+
`@williamthorsen/nmr` is now bundled with ReadyUp kits that check a repo's alignment with `nmr` requirements and recommendations.
|
|
26
|
+
|
|
27
|
+
`rdy run --from npm:@williamthorsen/nmr` checks the repo against the kits without prior configuration. Once the package is added to the ReadyUp config, `rdy run --packages` will run the `nmr` kits along with those of any other registered package. Requires `readyup` v0.23 or greater.
|
|
28
|
+
|
|
29
|
+
### ♻️ Refactoring
|
|
30
|
+
|
|
31
|
+
- Load the config with Node's TypeScript support instead of jiti (#573)
|
|
32
|
+
|
|
33
|
+
`release-kit` now uses native Node with type-stripping to read its configs. `jiti` is no longer needed as a dependency. As a result, relative imports inside `.config/release-kit.config.ts` now require an explicit file extension.
|
|
34
|
+
|
|
5
35
|
## 10.0.0 — 2026-08-03
|
|
6
36
|
|
|
7
37
|
### 🎉 Features
|
package/README.md
CHANGED
|
@@ -5,15 +5,19 @@ Version-bumping and changelog-generation toolkit for release workflows.
|
|
|
5
5
|
Provides a self-contained CLI that auto-discovers workspaces from `pnpm-workspace.yaml`, parses conventional commits, determines version bumps, updates `package.json` files, and generates changelogs from `git-cliff --context` output rendered in-process (with optional [editorial overrides](#editorial-overrides)).
|
|
6
6
|
|
|
7
7
|
<!-- section:release-notes -->
|
|
8
|
-
## Release notes — v10.
|
|
8
|
+
## Release notes — v10.2.0 (2026-08-04)
|
|
9
9
|
|
|
10
10
|
### 🎉 Features
|
|
11
11
|
|
|
12
|
-
-
|
|
12
|
+
- Add status labels and clear grouped-label descriptions (#585)
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
Adds two status labels (`status:blocked` and `status:on-hold`) to the common preset and removes descriptions from other scoped labels (`priority:` and `value:`) to keep scoped groups compact in the GitHub UI.
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
- Publish each package's readyup kit from the package that owns it (#590)
|
|
17
|
+
|
|
18
|
+
ReadyUp checks for `@williamthorsen/nmr`, `@williamthorsen/release-kit`, and `v11y-check` are now run directly against these packages, guaranteeing a version-appropriate check, instead of against their host repo. The package must be installed as a direct dependency in the calling repo.
|
|
19
|
+
|
|
20
|
+
The syntax is `rdy run --from npm:<package>`. `rdy run --packages` runs the kits contained in any packages listed in `.config/readyup.config.ts`. Requires `readyup` 0.23 or later.
|
|
17
21
|
<!-- /section:release-notes -->
|
|
18
22
|
|
|
19
23
|
## Installation
|
|
@@ -69,7 +73,7 @@ That's it for most repos. The CLI auto-discovers workspaces and applies sensible
|
|
|
69
73
|
## How it works
|
|
70
74
|
|
|
71
75
|
1. **Workspace discovery**: reads `pnpm-workspace.yaml` and resolves its `packages` globs to find workspace directories. Each directory containing a `package.json` becomes a workspace. If no workspace file is found, the repo is treated as a single-package project.
|
|
72
|
-
2. **Config loading**: loads `.config/release-kit.config.ts` (if present)
|
|
76
|
+
2. **Config loading**: loads `.config/release-kit.config.ts` (if present) and merges it with discovered defaults.
|
|
73
77
|
3. **Commit analysis**: for each workspace, finds commits since the last version tag, parses them for type and scope, and determines the appropriate version bump.
|
|
74
78
|
4. **Version bump + changelog**: bumps `package.json` versions, builds structured `ChangelogEntry[]` from `git-cliff --context`, applies any [editorial overrides](#editorial-overrides) from per-scope `.meta/changelog-overrides.json` files, and renders both `CHANGELOG.md` and `.meta/changelog.json` from that single source. `git-cliff` is invoked only for its `--context` JSON; markdown rendering happens in-process so `.meta/changelog.json` and `CHANGELOG.md` always agree.
|
|
75
79
|
5. **Release tags file**: writes computed tags to `tmp/.release-tags` for the release workflow to read when tagging and pushing.
|
|
@@ -115,6 +119,8 @@ export default defineConfig({
|
|
|
115
119
|
|
|
116
120
|
`defineConfig` is a type-safe identity function: it gives the config object validation and completion in `.ts` files and full inference in `.js` ones. The loader also accepts a plain `export default config` or `export const config = { ... }`.
|
|
117
121
|
|
|
122
|
+
Node loads the file directly, so a relative import inside it needs an explicit file extension — `./work-types.ts`, not `./work-types`.
|
|
123
|
+
|
|
118
124
|
### `ReleaseKitConfig` reference
|
|
119
125
|
|
|
120
126
|
| Field | Type | Description |
|
|
@@ -866,6 +872,32 @@ The body template is intentionally empty: release-kit reads cliff's `--context`
|
|
|
866
872
|
|
|
867
873
|
To customize, scaffold a local copy with `release-kit init --with-config` and edit `.config/git-cliff.toml`. Edit only the `[git]` section — body-template changes have no effect.
|
|
868
874
|
|
|
875
|
+
## Readiness checks
|
|
876
|
+
|
|
877
|
+
release-kit publishes two [readyup](https://www.npmjs.com/package/readyup) kits that check a consuming repo against the release it has installed: the `default` kit covers release-kit's own setup — workflows matching the current templates, config free of removed fields, sync-labels wiring — and `npm-auto-publish` covers a repo's OIDC-based npm publishing setup. Both ship inside the package, so they check against the version you installed rather than whatever a repository ref happens to point at, and a check added in a release reaches your repo on upgrade.
|
|
878
|
+
|
|
879
|
+
Add `readyup` as a devDependency, then name release-kit in its config:
|
|
880
|
+
|
|
881
|
+
```ts
|
|
882
|
+
// .config/readyup.config.ts
|
|
883
|
+
import { defineRdyConfig } from 'readyup';
|
|
884
|
+
|
|
885
|
+
export default defineRdyConfig({
|
|
886
|
+
packages: ['@williamthorsen/release-kit'],
|
|
887
|
+
});
|
|
888
|
+
```
|
|
889
|
+
|
|
890
|
+
```bash
|
|
891
|
+
rdy run --packages # every kit each listed package publishes
|
|
892
|
+
rdy run --from npm:@williamthorsen/release-kit # the default kit alone
|
|
893
|
+
rdy run --from npm:@williamthorsen/release-kit npm-auto-publish # the npm-auto-publish kit
|
|
894
|
+
rdy list --from npm:@williamthorsen/release-kit # what release-kit publishes
|
|
895
|
+
```
|
|
896
|
+
|
|
897
|
+
`--packages` is the form that survives release-kit publishing further kits. Every form needs `readyup` 0.23 or later, and `@williamthorsen/release-kit` as a _direct_ devDependency: a strict pnpm layout links nothing else into the project, so a transitive copy is unreachable.
|
|
898
|
+
|
|
899
|
+
These kits are no longer reachable through `rdy run --from github:williamthorsen/node-monorepo-tools`. Repos still using that form should switch to one of the invocations above.
|
|
900
|
+
|
|
869
901
|
## External dependencies
|
|
870
902
|
|
|
871
903
|
This package shells out to two external tools:
|
package/dist/esm/loadConfig.js
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
1
9
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
10
|
import path from 'node:path';
|
|
11
|
+
import { pathToFileURL } from 'node:url';
|
|
3
12
|
import { DEFAULT_CHANGELOG_JSON_CONFIG, DEFAULT_PROJECT_TAG_PREFIX, DEFAULT_RELEASE_NOTES_CONFIG, DEFAULT_VERSION_PATTERNS, DEFAULT_WORK_TYPES, } from "./defaults.js";
|
|
4
13
|
import { deriveWorkspaceConfig } from "./deriveWorkspaceConfig.js";
|
|
5
14
|
import { isRecord } from "./typeGuards.js";
|
|
@@ -34,13 +43,8 @@ export async function loadConfig() {
|
|
|
34
43
|
if (!existsSync(absoluteConfigPath)) {
|
|
35
44
|
return undefined;
|
|
36
45
|
}
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
const imported = await jiti.import(absoluteConfigPath);
|
|
40
|
-
if (!isRecord(imported)) {
|
|
41
|
-
throw new Error(`Config file must export an object, got ${Array.isArray(imported) ? 'array' : typeof imported}`);
|
|
42
|
-
}
|
|
43
|
-
const resolved = imported.default ?? imported.config;
|
|
46
|
+
const imported = await import(__rewriteRelativeImportExtension(pathToFileURL(absoluteConfigPath).href));
|
|
47
|
+
const resolved = isRecord(imported) ? (imported.default ?? imported.config) : undefined;
|
|
44
48
|
if (resolved === undefined) {
|
|
45
49
|
throw new Error('Config file must have a default export or a named `config` export (e.g., `export default { ... }` or `export const config = { ... }`)');
|
|
46
50
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@williamthorsen/release-kit",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.2.0",
|
|
4
4
|
"description": "Version-bumping and changelog-generation toolkit for release workflows",
|
|
5
5
|
"keywords": [],
|
|
6
6
|
"homepage": "https://github.com/williamthorsen/node-monorepo-tools/tree/main/packages/release-kit#readme",
|
|
@@ -29,6 +29,8 @@
|
|
|
29
29
|
"release-kit": "./bin/release-kit.js"
|
|
30
30
|
},
|
|
31
31
|
"files": [
|
|
32
|
+
".readyup/kits/*.js",
|
|
33
|
+
".readyup/manifest.json",
|
|
32
34
|
"bin",
|
|
33
35
|
"dist/*",
|
|
34
36
|
"cliff.toml.template",
|
|
@@ -38,15 +40,15 @@
|
|
|
38
40
|
],
|
|
39
41
|
"dependencies": {
|
|
40
42
|
"glob": "13.0.6",
|
|
41
|
-
"jiti": "2.7.0",
|
|
42
43
|
"json-stringify-pretty-compact": "4.0.0",
|
|
43
44
|
"semver": "7.8.5",
|
|
44
45
|
"yaml": "2.9.0",
|
|
45
46
|
"zod": "4.4.3",
|
|
46
|
-
"@williamthorsen/nmr-core": "0.
|
|
47
|
+
"@williamthorsen/nmr-core": "0.9.0"
|
|
47
48
|
},
|
|
48
49
|
"devDependencies": {
|
|
49
50
|
"@types/semver": "7.7.1",
|
|
51
|
+
"readyup": "0.23.0",
|
|
50
52
|
"smol-toml": "1.7.1"
|
|
51
53
|
},
|
|
52
54
|
"engines": {
|
|
@@ -57,7 +59,8 @@
|
|
|
57
59
|
"registry": "https://registry.npmjs.org"
|
|
58
60
|
},
|
|
59
61
|
"scripts": {
|
|
60
|
-
"build:post": "node scripts/copy-work-types.js",
|
|
62
|
+
"build:post": "node scripts/copy-work-types.js && rdy compile",
|
|
63
|
+
"build:pre": "rdy verify",
|
|
61
64
|
"work-types:check": "pnpm exec release-kit work-types check",
|
|
62
65
|
"work-types:sync": "pnpm exec release-kit work-types sync"
|
|
63
66
|
}
|
|
@@ -70,30 +70,28 @@
|
|
|
70
70
|
# Priority
|
|
71
71
|
- name: 'priority:critical'
|
|
72
72
|
color: b60205
|
|
73
|
-
description: Demands immediate attention
|
|
74
73
|
- name: 'priority:high'
|
|
75
74
|
color: d93f0b
|
|
76
|
-
description: Address soon
|
|
77
75
|
- name: 'priority:medium'
|
|
78
76
|
color: fbca04
|
|
79
|
-
description: No rush but don't forget
|
|
80
77
|
- name: 'priority:low'
|
|
81
78
|
color: cfd3d7
|
|
82
|
-
|
|
79
|
+
|
|
80
|
+
# Status
|
|
81
|
+
- name: 'status:blocked'
|
|
82
|
+
color: b83280
|
|
83
|
+
- name: 'status:on-hold'
|
|
84
|
+
color: fbcfe8
|
|
83
85
|
|
|
84
86
|
# Value
|
|
85
87
|
- name: 'value:must-have'
|
|
86
88
|
color: 7057ff
|
|
87
|
-
description: Essential for the application to fulfill its purpose
|
|
88
89
|
- name: 'value:should-have'
|
|
89
90
|
color: 8d83fc
|
|
90
|
-
description: Important but the application is usable without it
|
|
91
91
|
- name: 'value:nice-to-have'
|
|
92
92
|
color: a8b2f8
|
|
93
|
-
description: A nice-to-have enhancement
|
|
94
93
|
- name: 'value:questionable'
|
|
95
94
|
color: c5def5
|
|
96
|
-
description: Decision needed on whether this would add sufficient value
|
|
97
95
|
|
|
98
96
|
# Triage
|
|
99
97
|
- name: duplicate
|