@williamthorsen/release-kit 5.0.0 → 5.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/CHANGELOG.md +149 -49
- package/README.md +275 -78
- package/cliff.toml.template +26 -17
- package/dist/esm/.cache +1 -1
- package/dist/esm/assertCleanWorkingTree.js +1 -1
- package/dist/esm/bin/release-kit.js +97 -4
- package/dist/esm/buildChangelogEntries.d.ts +4 -0
- package/dist/esm/buildChangelogEntries.js +173 -0
- package/dist/esm/buildDependencyGraph.d.ts +1 -0
- package/dist/esm/buildDependencyGraph.js +8 -1
- package/dist/esm/buildReleaseSummary.js +9 -1
- package/dist/esm/buildSyntheticChangelogEntry.d.ts +5 -0
- package/dist/esm/buildSyntheticChangelogEntry.js +13 -0
- package/dist/esm/changelogJsonFile.d.ts +4 -0
- package/dist/esm/changelogJsonFile.js +68 -0
- package/dist/esm/checkWorkTypesDrift.d.ts +11 -0
- package/dist/esm/checkWorkTypesDrift.js +110 -0
- package/dist/esm/collectPolicyViolations.d.ts +6 -0
- package/dist/esm/collectPolicyViolations.js +15 -0
- package/dist/esm/createGithubRelease.d.ts +12 -2
- package/dist/esm/createGithubRelease.js +12 -8
- package/dist/esm/createGithubReleaseCommand.js +10 -6
- package/dist/esm/decideRelease.d.ts +28 -0
- package/dist/esm/decideRelease.js +44 -0
- package/dist/esm/defaults.d.ts +8 -0
- package/dist/esm/defaults.js +43 -20
- package/dist/esm/deriveWorkspaceConfig.js +3 -0
- package/dist/esm/determineBumpFromCommits.d.ts +6 -1
- package/dist/esm/determineBumpFromCommits.js +9 -3
- package/dist/esm/generateChangelogs.js +14 -29
- package/dist/esm/index.d.ts +2 -43
- package/dist/esm/index.js +0 -82
- package/dist/esm/init/templates.js +2 -2
- package/dist/esm/loadConfig.d.ts +10 -1
- package/dist/esm/loadConfig.js +110 -24
- package/dist/esm/parseCommitMessage.d.ts +8 -2
- package/dist/esm/parseCommitMessage.js +32 -3
- package/dist/esm/prepareCommand.js +51 -9
- package/dist/esm/publish.d.ts +0 -1
- package/dist/esm/publish.js +3 -3
- package/dist/esm/publishCommand.js +31 -3
- package/dist/esm/releasePrepare.js +109 -41
- package/dist/esm/releasePrepareMono.js +156 -87
- package/dist/esm/releasePrepareProject.d.ts +9 -0
- package/dist/esm/releasePrepareProject.js +121 -0
- package/dist/esm/renderReleaseNotes.js +2 -1
- package/dist/esm/reportPrepare.js +88 -24
- package/dist/esm/resolveCommandTags.js +16 -6
- package/dist/esm/resolveReleaseTags.d.ts +8 -1
- package/dist/esm/resolveReleaseTags.js +11 -7
- package/dist/esm/runGitCliff.d.ts +2 -0
- package/dist/esm/runGitCliff.js +27 -0
- package/dist/esm/stripEmojiPrefix.d.ts +1 -0
- package/dist/esm/stripEmojiPrefix.js +7 -0
- package/dist/esm/syncWorkTypes.d.ts +10 -0
- package/dist/esm/syncWorkTypes.js +90 -0
- package/dist/esm/types.d.ts +72 -14
- package/dist/esm/validateConfig.js +26 -0
- package/dist/esm/validateOnlyExcludesStrandedDependents.d.ts +14 -0
- package/dist/esm/validateOnlyExcludesStrandedDependents.js +109 -0
- package/dist/esm/work-types.json +127 -0
- package/dist/esm/work-types.schema.json +73 -0
- package/dist/esm/workTypesData.d.ts +14 -0
- package/dist/esm/workTypesData.js +59 -0
- package/dist/esm/workTypesUtils.d.ts +5 -0
- package/dist/esm/workTypesUtils.js +16 -0
- package/package.json +9 -3
- package/presets/labels/common.yaml +9 -6
- package/schemas/label-map.json +24 -0
- package/dist/esm/generateChangelogJson.d.ts +0 -7
- package/dist/esm/generateChangelogJson.js +0 -232
- package/dist/esm/version.d.ts +0 -1
- package/dist/esm/version.js +0 -4
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { parseArgs, translateParseError } from "@williamthorsen/nmr-core";
|
|
2
|
+
import { parseArgs, readPackageVersion, translateParseError } from "@williamthorsen/nmr-core";
|
|
3
|
+
import { checkWorkTypesDrift } from "../checkWorkTypesDrift.js";
|
|
3
4
|
import { commitCommand } from "../commitCommand.js";
|
|
4
5
|
import { createGithubReleaseCommand } from "../createGithubReleaseCommand.js";
|
|
5
6
|
import { initCommand } from "../init/initCommand.js";
|
|
@@ -10,8 +11,9 @@ import { showTagPrefixesCommand } from "../showTagPrefixesCommand.js";
|
|
|
10
11
|
import { generateCommand } from "../sync-labels/generateCommand.js";
|
|
11
12
|
import { syncLabelsInitCommand } from "../sync-labels/initCommand.js";
|
|
12
13
|
import { syncLabelsCommand } from "../sync-labels/syncCommand.js";
|
|
14
|
+
import { syncWorkTypes } from "../syncWorkTypes.js";
|
|
13
15
|
import { tagCommand } from "../tagCommand.js";
|
|
14
|
-
|
|
16
|
+
const VERSION = readPackageVersion(import.meta.url);
|
|
15
17
|
function showUsage() {
|
|
16
18
|
console.info(`
|
|
17
19
|
Usage: release-kit <command> [options]
|
|
@@ -26,6 +28,7 @@ Commands:
|
|
|
26
28
|
show-tag-prefixes Show derived and declared legacy tag prefixes per workspace
|
|
27
29
|
init Initialize release-kit in the current repository
|
|
28
30
|
sync-labels Manage GitHub label synchronization
|
|
31
|
+
work-types Check for or sync work-type taxonomy drift against the upstream canonical
|
|
29
32
|
|
|
30
33
|
Options:
|
|
31
34
|
--dry-run Preview changes without writing files
|
|
@@ -171,6 +174,49 @@ legacy tag prefixes. Surfaces any release-shaped tags whose prefix is neither a
|
|
|
171
174
|
derived prefix nor declared in \`legacyIdentities\`, with a copy-pasteable
|
|
172
175
|
config snippet.
|
|
173
176
|
|
|
177
|
+
Options:
|
|
178
|
+
--help, -h Show this help message
|
|
179
|
+
`);
|
|
180
|
+
}
|
|
181
|
+
function showWorkTypesHelp() {
|
|
182
|
+
console.info(`
|
|
183
|
+
Usage: release-kit work-types <subcommand>
|
|
184
|
+
|
|
185
|
+
Manage the canonical work-types taxonomy used by changelog and release-notes generation.
|
|
186
|
+
|
|
187
|
+
Subcommands:
|
|
188
|
+
check Compare the local work-types.json against the upstream codeassembly canonical
|
|
189
|
+
sync Overwrite the local work-types.json with the upstream contents
|
|
190
|
+
|
|
191
|
+
Exit codes (check):
|
|
192
|
+
0 Match (or upstream missing \u2014 transitional warning printed)
|
|
193
|
+
1 Drift detected
|
|
194
|
+
2 Network error
|
|
195
|
+
3 Schema mismatch
|
|
196
|
+
|
|
197
|
+
Options:
|
|
198
|
+
--help, -h Show this help message
|
|
199
|
+
`);
|
|
200
|
+
}
|
|
201
|
+
function showWorkTypesCheckHelp() {
|
|
202
|
+
console.info(`
|
|
203
|
+
Usage: release-kit work-types check
|
|
204
|
+
|
|
205
|
+
Compare the local work-types.json against the upstream codeassembly canonical and report
|
|
206
|
+
drift. Exit 0 on match, 1 on drift, 0 + warning when upstream is missing (transitional),
|
|
207
|
+
2 on network error, 3 on schema mismatch.
|
|
208
|
+
|
|
209
|
+
Options:
|
|
210
|
+
--help, -h Show this help message
|
|
211
|
+
`);
|
|
212
|
+
}
|
|
213
|
+
function showWorkTypesSyncHelp() {
|
|
214
|
+
console.info(`
|
|
215
|
+
Usage: release-kit work-types sync
|
|
216
|
+
|
|
217
|
+
Fetch the upstream work-types.json, validate its top-level shape, and overwrite the local
|
|
218
|
+
copy with the upstream content (formatted with 2-space indent + trailing newline).
|
|
219
|
+
|
|
174
220
|
Options:
|
|
175
221
|
--help, -h Show this help message
|
|
176
222
|
`);
|
|
@@ -179,11 +225,13 @@ function showPublishHelp() {
|
|
|
179
225
|
console.info(`
|
|
180
226
|
Usage: release-kit publish [options]
|
|
181
227
|
|
|
182
|
-
Publish packages that have release tags on HEAD.
|
|
228
|
+
Publish packages that have release tags on HEAD. Operates only on workspaces where
|
|
229
|
+
package.json#private is absent or false. Without --tags, unpublishable workspaces are
|
|
230
|
+
silently filtered out. With --tags, naming an unpublishable tag is an error.
|
|
183
231
|
|
|
184
232
|
Options:
|
|
185
233
|
--dry-run Preview without publishing
|
|
186
|
-
--no-git-checks Skip
|
|
234
|
+
--no-git-checks Skip the clean-working-tree check
|
|
187
235
|
--tags=tag1,tag2 Only publish the named tags (comma-separated, full tag names)
|
|
188
236
|
--provenance Generate provenance statement (requires OIDC, not supported by classic yarn)
|
|
189
237
|
--help, -h Show this help message
|
|
@@ -341,6 +389,51 @@ if (command === "sync-labels") {
|
|
|
341
389
|
showSyncLabelsHelp();
|
|
342
390
|
process.exit(1);
|
|
343
391
|
}
|
|
392
|
+
if (command === "work-types") {
|
|
393
|
+
const subcommand = flags[0];
|
|
394
|
+
const subflags = flags.slice(1);
|
|
395
|
+
if (subcommand === "--help" || subcommand === "-h" || subcommand === void 0) {
|
|
396
|
+
showWorkTypesHelp();
|
|
397
|
+
process.exit(0);
|
|
398
|
+
}
|
|
399
|
+
if (subcommand === "check") {
|
|
400
|
+
if (subflags.some((f) => f === "--help" || f === "-h")) {
|
|
401
|
+
showWorkTypesCheckHelp();
|
|
402
|
+
process.exit(0);
|
|
403
|
+
}
|
|
404
|
+
if (subflags.length > 0) {
|
|
405
|
+
console.error(`Error: Unknown option: ${subflags[0]}`);
|
|
406
|
+
process.exit(1);
|
|
407
|
+
}
|
|
408
|
+
const result = await checkWorkTypesDrift();
|
|
409
|
+
if (result.exitCode === 0) {
|
|
410
|
+
console.info(result.message);
|
|
411
|
+
} else {
|
|
412
|
+
console.error(result.message);
|
|
413
|
+
}
|
|
414
|
+
process.exit(result.exitCode);
|
|
415
|
+
}
|
|
416
|
+
if (subcommand === "sync") {
|
|
417
|
+
if (subflags.some((f) => f === "--help" || f === "-h")) {
|
|
418
|
+
showWorkTypesSyncHelp();
|
|
419
|
+
process.exit(0);
|
|
420
|
+
}
|
|
421
|
+
if (subflags.length > 0) {
|
|
422
|
+
console.error(`Error: Unknown option: ${subflags[0]}`);
|
|
423
|
+
process.exit(1);
|
|
424
|
+
}
|
|
425
|
+
const result = await syncWorkTypes();
|
|
426
|
+
if (result.exitCode === 0) {
|
|
427
|
+
console.info(result.message);
|
|
428
|
+
} else {
|
|
429
|
+
console.error(result.message);
|
|
430
|
+
}
|
|
431
|
+
process.exit(result.exitCode);
|
|
432
|
+
}
|
|
433
|
+
console.error(`Error: Unknown subcommand: ${subcommand}`);
|
|
434
|
+
showWorkTypesHelp();
|
|
435
|
+
process.exit(1);
|
|
436
|
+
}
|
|
344
437
|
console.error(`Error: Unknown command: ${command}`);
|
|
345
438
|
showUsage();
|
|
346
439
|
process.exit(1);
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { GenerateChangelogOptions } from './generateChangelogs.ts';
|
|
2
|
+
import type { ChangelogEntry, ReleaseConfig } from './types.ts';
|
|
3
|
+
export declare function stripGroupDecorations(group: string): string;
|
|
4
|
+
export declare function buildChangelogEntries(config: Pick<ReleaseConfig, 'cliffConfigPath' | 'changelogJson'>, tag: string, options?: GenerateChangelogOptions): ChangelogEntry[];
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { extractVersion } from "./changelogJsonUtils.js";
|
|
2
|
+
import { DEFAULT_WORK_TYPES } from "./defaults.js";
|
|
3
|
+
import { COMMIT_PREPROCESSOR_PATTERNS } from "./parseCommitMessage.js";
|
|
4
|
+
import { resolveCliffConfigPath } from "./resolveCliffConfigPath.js";
|
|
5
|
+
import { runGitCliff } from "./runGitCliff.js";
|
|
6
|
+
import { stripEmojiPrefix } from "./stripEmojiPrefix.js";
|
|
7
|
+
import { isRecord, isUnknownArray } from "./typeGuards.js";
|
|
8
|
+
const HTML_COMMENT_PREFIX_PATTERN = /^<!--[^>]*-->/;
|
|
9
|
+
const CANONICAL_SECTION_ORDER = new Map(
|
|
10
|
+
Object.values(DEFAULT_WORK_TYPES).map((config, index) => [stripGroupDecorations(config.header), index])
|
|
11
|
+
);
|
|
12
|
+
function canonicalSectionPriority(title) {
|
|
13
|
+
const index = CANONICAL_SECTION_ORDER.get(stripGroupDecorations(title));
|
|
14
|
+
return index ?? Number.POSITIVE_INFINITY;
|
|
15
|
+
}
|
|
16
|
+
function stripGroupDecorations(group) {
|
|
17
|
+
return stripEmojiPrefix(group.replace(HTML_COMMENT_PREFIX_PATTERN, ""));
|
|
18
|
+
}
|
|
19
|
+
function buildChangelogEntries(config, tag, options) {
|
|
20
|
+
const resolvedConfigPath = resolveCliffConfigPath(config.cliffConfigPath, import.meta.url);
|
|
21
|
+
const cliffArgs = ["--context", "--tag", tag];
|
|
22
|
+
if (options?.tagPattern !== void 0) {
|
|
23
|
+
cliffArgs.push("--tag-pattern", options.tagPattern);
|
|
24
|
+
}
|
|
25
|
+
for (const includePath of options?.includePaths ?? []) {
|
|
26
|
+
cliffArgs.push("--include-path", includePath);
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
const contextJson = runGitCliff(resolvedConfigPath, cliffArgs, ["pipe", "pipe", "inherit"]);
|
|
30
|
+
const releases = parseCliffContext(contextJson);
|
|
31
|
+
const devOnlySections = new Set(config.changelogJson.devOnlySections);
|
|
32
|
+
return transformReleases(releases, devOnlySections);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
`Failed to build changelog entries for tag ${tag}: ${error instanceof Error ? error.message : String(error)}`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function parseCliffContext(json) {
|
|
40
|
+
const parsed = JSON.parse(json);
|
|
41
|
+
if (!isUnknownArray(parsed)) {
|
|
42
|
+
throw new TypeError("Expected git-cliff --context output to be an array");
|
|
43
|
+
}
|
|
44
|
+
return parsed.map(toCliffContextRelease);
|
|
45
|
+
}
|
|
46
|
+
function toCliffContextRelease(value) {
|
|
47
|
+
if (!isRecord(value)) {
|
|
48
|
+
return {};
|
|
49
|
+
}
|
|
50
|
+
const release = {};
|
|
51
|
+
if (typeof value.version === "string") {
|
|
52
|
+
release.version = value.version;
|
|
53
|
+
}
|
|
54
|
+
if (typeof value.timestamp === "number") {
|
|
55
|
+
release.timestamp = value.timestamp;
|
|
56
|
+
}
|
|
57
|
+
if (isUnknownArray(value.commits)) {
|
|
58
|
+
release.commits = value.commits.map(toCliffContextCommit);
|
|
59
|
+
}
|
|
60
|
+
return release;
|
|
61
|
+
}
|
|
62
|
+
function toCliffContextCommit(value) {
|
|
63
|
+
if (!isRecord(value)) {
|
|
64
|
+
return { message: "" };
|
|
65
|
+
}
|
|
66
|
+
const commit = {
|
|
67
|
+
message: typeof value.message === "string" ? value.message : ""
|
|
68
|
+
};
|
|
69
|
+
if (typeof value.group === "string") {
|
|
70
|
+
commit.group = value.group;
|
|
71
|
+
}
|
|
72
|
+
return commit;
|
|
73
|
+
}
|
|
74
|
+
function transformReleases(releases, devOnlySections) {
|
|
75
|
+
const entries = [];
|
|
76
|
+
const devOnlyNormalised = new Set([...devOnlySections].map(stripGroupDecorations));
|
|
77
|
+
for (const release of releases) {
|
|
78
|
+
if (release.version === void 0) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const version = extractVersion(release.version);
|
|
82
|
+
const date = release.timestamp !== void 0 ? new Date(release.timestamp * 1e3).toISOString().slice(0, 10) : "unreleased";
|
|
83
|
+
const sectionMap = /* @__PURE__ */ new Map();
|
|
84
|
+
for (const commit of release.commits ?? []) {
|
|
85
|
+
const group = stripCommentPrefix(commit.group ?? "Other");
|
|
86
|
+
const description = extractDescription(commit.message);
|
|
87
|
+
const body = extractBody(commit.message);
|
|
88
|
+
const breaking = subjectHasBreakingMarker(commit.message);
|
|
89
|
+
let items = sectionMap.get(group);
|
|
90
|
+
if (items === void 0) {
|
|
91
|
+
items = [];
|
|
92
|
+
sectionMap.set(group, items);
|
|
93
|
+
}
|
|
94
|
+
const item = { description };
|
|
95
|
+
if (body !== void 0) {
|
|
96
|
+
item.body = body;
|
|
97
|
+
}
|
|
98
|
+
if (breaking) {
|
|
99
|
+
item.breaking = true;
|
|
100
|
+
}
|
|
101
|
+
items.push(item);
|
|
102
|
+
}
|
|
103
|
+
const sections = [];
|
|
104
|
+
for (const [title, items] of sectionMap) {
|
|
105
|
+
if (items.length === 0) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
sections.push({
|
|
109
|
+
title,
|
|
110
|
+
audience: devOnlyNormalised.has(stripGroupDecorations(title)) ? "dev" : "all",
|
|
111
|
+
items
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
sections.sort((a, b) => canonicalSectionPriority(a.title) - canonicalSectionPriority(b.title));
|
|
115
|
+
if (sections.length > 0) {
|
|
116
|
+
entries.push({ version, date, sections });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return entries;
|
|
120
|
+
}
|
|
121
|
+
function stripCommentPrefix(group) {
|
|
122
|
+
return group.replace(HTML_COMMENT_PREFIX_PATTERN, "");
|
|
123
|
+
}
|
|
124
|
+
function subjectHasBreakingMarker(message) {
|
|
125
|
+
let subject = message.split("\n", 1)[0] ?? "";
|
|
126
|
+
for (const pattern of COMMIT_PREPROCESSOR_PATTERNS) {
|
|
127
|
+
subject = subject.replace(pattern, "");
|
|
128
|
+
}
|
|
129
|
+
return /^(?:[^|]+\|)?\w+(?:\([^)]+\))?!:/.test(subject);
|
|
130
|
+
}
|
|
131
|
+
function extractDescription(message) {
|
|
132
|
+
const firstLine = message.split("\n")[0] ?? message;
|
|
133
|
+
const afterColon = firstLine.split(": ").slice(1).join(": ");
|
|
134
|
+
if (afterColon.length > 0) {
|
|
135
|
+
return afterColon.charAt(0).toUpperCase() + afterColon.slice(1);
|
|
136
|
+
}
|
|
137
|
+
return firstLine;
|
|
138
|
+
}
|
|
139
|
+
const TRAILER_PATTERNS = [
|
|
140
|
+
/^Signed-off-by:/i,
|
|
141
|
+
/^Co-authored-by:/i,
|
|
142
|
+
/^(Closes|Fixes|Resolves)\s+#\d+\s*$/i,
|
|
143
|
+
/^https?:\/\/\S+\/pull\/\d+\/?\s*$/
|
|
144
|
+
];
|
|
145
|
+
function extractBody(message) {
|
|
146
|
+
const lines = message.split("\n").slice(1);
|
|
147
|
+
let start = 0;
|
|
148
|
+
while (start < lines.length && (lines[start] ?? "").trim() === "") {
|
|
149
|
+
start += 1;
|
|
150
|
+
}
|
|
151
|
+
let end = lines.length;
|
|
152
|
+
while (end > start) {
|
|
153
|
+
const line = lines[end - 1] ?? "";
|
|
154
|
+
const trimmed = line.trim();
|
|
155
|
+
if (trimmed === "") {
|
|
156
|
+
end -= 1;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (TRAILER_PATTERNS.some((pattern) => pattern.test(trimmed))) {
|
|
160
|
+
end -= 1;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
if (end <= start) {
|
|
166
|
+
return void 0;
|
|
167
|
+
}
|
|
168
|
+
return lines.slice(start, end).join("\n").trim();
|
|
169
|
+
}
|
|
170
|
+
export {
|
|
171
|
+
buildChangelogEntries,
|
|
172
|
+
stripGroupDecorations
|
|
173
|
+
};
|
|
@@ -3,5 +3,6 @@ export interface DependencyGraph {
|
|
|
3
3
|
packageNameToDir: Map<string, string>;
|
|
4
4
|
dirToPackageName: Map<string, string>;
|
|
5
5
|
dependentsOf: Map<string, WorkspaceConfig[]>;
|
|
6
|
+
dependenciesOf: Map<string, Set<string>>;
|
|
6
7
|
}
|
|
7
8
|
export declare function buildDependencyGraph(workspaces: readonly WorkspaceConfig[]): DependencyGraph;
|
|
@@ -6,6 +6,7 @@ function buildDependencyGraph(workspaces) {
|
|
|
6
6
|
const packageNameToDir = /* @__PURE__ */ new Map();
|
|
7
7
|
const dirToPackageName = /* @__PURE__ */ new Map();
|
|
8
8
|
const dependentsOf = /* @__PURE__ */ new Map();
|
|
9
|
+
const dependenciesOf = /* @__PURE__ */ new Map();
|
|
9
10
|
const workspacePackages = /* @__PURE__ */ new Map();
|
|
10
11
|
for (const workspace of workspaces) {
|
|
11
12
|
const primaryPackageFile = workspace.packageFiles[0];
|
|
@@ -32,9 +33,15 @@ function buildDependencyGraph(workspaces) {
|
|
|
32
33
|
} else {
|
|
33
34
|
existing.push(workspace);
|
|
34
35
|
}
|
|
36
|
+
const forward = dependenciesOf.get(workspace.dir);
|
|
37
|
+
if (forward === void 0) {
|
|
38
|
+
dependenciesOf.set(workspace.dir, /* @__PURE__ */ new Set([depName]));
|
|
39
|
+
} else {
|
|
40
|
+
forward.add(depName);
|
|
41
|
+
}
|
|
35
42
|
}
|
|
36
43
|
}
|
|
37
|
-
return { packageNameToDir, dirToPackageName, dependentsOf };
|
|
44
|
+
return { packageNameToDir, dirToPackageName, dependentsOf, dependenciesOf };
|
|
38
45
|
}
|
|
39
46
|
function readPackageJsonSubset(filePath) {
|
|
40
47
|
let content;
|
|
@@ -2,7 +2,7 @@ import { stripScope } from "./stripScope.js";
|
|
|
2
2
|
function buildReleaseSummary(result) {
|
|
3
3
|
const sections = [];
|
|
4
4
|
for (const workspace of result.workspaces) {
|
|
5
|
-
if (workspace.status !== "released"
|
|
5
|
+
if (workspace.status !== "released") {
|
|
6
6
|
continue;
|
|
7
7
|
}
|
|
8
8
|
const commits = workspace.commits;
|
|
@@ -15,6 +15,14 @@ function buildReleaseSummary(result) {
|
|
|
15
15
|
}
|
|
16
16
|
sections.push(lines.join("\n"));
|
|
17
17
|
}
|
|
18
|
+
const project = result.project;
|
|
19
|
+
if (project !== void 0 && project.status === "released" && project.commits.length > 0) {
|
|
20
|
+
const lines = [project.tag];
|
|
21
|
+
for (const commit of project.commits) {
|
|
22
|
+
lines.push(`- ${stripScope(commit.message)}`);
|
|
23
|
+
}
|
|
24
|
+
sections.push(lines.join("\n"));
|
|
25
|
+
}
|
|
18
26
|
return sections.join("\n\n");
|
|
19
27
|
}
|
|
20
28
|
export {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
function buildSyntheticChangelogEntry(propagatedFrom, version, date) {
|
|
2
|
+
const items = propagatedFrom.map((dep) => ({
|
|
3
|
+
description: `Bumped \`${dep.packageName}\` to ${dep.newVersion}`
|
|
4
|
+
}));
|
|
5
|
+
return {
|
|
6
|
+
version,
|
|
7
|
+
date,
|
|
8
|
+
sections: [{ title: "Dependency updates", audience: "dev", items }]
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export {
|
|
12
|
+
buildSyntheticChangelogEntry
|
|
13
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ChangelogEntry, ReleaseConfig } from './types.ts';
|
|
2
|
+
export declare function resolveChangelogJsonPath(config: Pick<ReleaseConfig, 'changelogJson'>, changelogPath: string): string;
|
|
3
|
+
export declare function writeChangelogJson(filePath: string, entries: ChangelogEntry[]): string;
|
|
4
|
+
export declare function upsertChangelogJson(filePath: string, entries: ChangelogEntry[]): string;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import stringify from "json-stringify-pretty-compact";
|
|
4
|
+
import semver from "semver";
|
|
5
|
+
import { isChangelogEntry } from "./changelogJsonUtils.js";
|
|
6
|
+
import { isUnknownArray } from "./typeGuards.js";
|
|
7
|
+
function resolveChangelogJsonPath(config, changelogPath) {
|
|
8
|
+
return join(changelogPath, config.changelogJson.outputPath);
|
|
9
|
+
}
|
|
10
|
+
function writeChangelogJson(filePath, entries) {
|
|
11
|
+
const sorted = sortNewestFirst(entries);
|
|
12
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
13
|
+
writeFileSync(filePath, stringify(sorted, { maxLength: 100 }) + "\n", "utf8");
|
|
14
|
+
return filePath;
|
|
15
|
+
}
|
|
16
|
+
function upsertChangelogJson(filePath, entries) {
|
|
17
|
+
const existing = readExistingEntries(filePath);
|
|
18
|
+
const merged = mergeEntries(entries, existing);
|
|
19
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
20
|
+
writeFileSync(filePath, stringify(merged, { maxLength: 100 }) + "\n", "utf8");
|
|
21
|
+
return filePath;
|
|
22
|
+
}
|
|
23
|
+
function sortNewestFirst(entries) {
|
|
24
|
+
return [...entries].sort((a, b) => compareVersionsDescending(a.version, b.version));
|
|
25
|
+
}
|
|
26
|
+
function readExistingEntries(filePath) {
|
|
27
|
+
if (!existsSync(filePath)) {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
const content = readFileSync(filePath, "utf8");
|
|
32
|
+
const parsed = JSON.parse(content);
|
|
33
|
+
if (!isUnknownArray(parsed)) {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
return parsed.filter(isChangelogEntry);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.warn(
|
|
39
|
+
`Warning: could not parse existing ${filePath}: ${error instanceof Error ? error.message : String(error)}; treating as empty`
|
|
40
|
+
);
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function mergeEntries(newEntries, existingEntries) {
|
|
45
|
+
const versionMap = /* @__PURE__ */ new Map();
|
|
46
|
+
for (const entry of existingEntries) {
|
|
47
|
+
versionMap.set(entry.version, entry);
|
|
48
|
+
}
|
|
49
|
+
for (const entry of newEntries) {
|
|
50
|
+
versionMap.set(entry.version, entry);
|
|
51
|
+
}
|
|
52
|
+
return sortNewestFirst(versionMap.values());
|
|
53
|
+
}
|
|
54
|
+
function compareVersionsDescending(a, b) {
|
|
55
|
+
const aValid = semver.valid(a);
|
|
56
|
+
const bValid = semver.valid(b);
|
|
57
|
+
if (aValid && bValid) return semver.rcompare(aValid, bValid);
|
|
58
|
+
if (aValid) return -1;
|
|
59
|
+
if (bValid) return 1;
|
|
60
|
+
if (a > b) return -1;
|
|
61
|
+
if (a < b) return 1;
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
export {
|
|
65
|
+
resolveChangelogJsonPath,
|
|
66
|
+
upsertChangelogJson,
|
|
67
|
+
writeChangelogJson
|
|
68
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const UPSTREAM_WORK_TYPES_URL = "https://raw.githubusercontent.com/williamthorsen/codeassembly/main/packages/agents/content/skills/_data/work-types.json";
|
|
2
|
+
export interface DriftCheckResult {
|
|
3
|
+
exitCode: 0 | 1 | 2 | 3;
|
|
4
|
+
message: string;
|
|
5
|
+
}
|
|
6
|
+
export interface CheckWorkTypesDriftDependencies {
|
|
7
|
+
localPath?: string;
|
|
8
|
+
fetch?: typeof globalThis.fetch;
|
|
9
|
+
upstreamUrl?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function checkWorkTypesDrift(dependencies?: CheckWorkTypesDriftDependencies): Promise<DriftCheckResult>;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { errorMessage, hasExpectedTopLevelShape } from "./workTypesUtils.js";
|
|
5
|
+
const UPSTREAM_WORK_TYPES_URL = "https://raw.githubusercontent.com/williamthorsen/codeassembly/main/packages/agents/content/skills/_data/work-types.json";
|
|
6
|
+
function resolveDefaultLocalPath() {
|
|
7
|
+
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
return resolve(moduleDir, "work-types.json");
|
|
9
|
+
}
|
|
10
|
+
async function checkWorkTypesDrift(dependencies = {}) {
|
|
11
|
+
const localPath = dependencies.localPath ?? resolveDefaultLocalPath();
|
|
12
|
+
const fetcher = dependencies.fetch ?? globalThis.fetch;
|
|
13
|
+
const url = dependencies.upstreamUrl ?? UPSTREAM_WORK_TYPES_URL;
|
|
14
|
+
const localContent = readFileSync(localPath, "utf8");
|
|
15
|
+
let localJson;
|
|
16
|
+
try {
|
|
17
|
+
localJson = JSON.parse(localContent);
|
|
18
|
+
} catch (error) {
|
|
19
|
+
return {
|
|
20
|
+
exitCode: 3,
|
|
21
|
+
message: `Local work-types.json is not valid JSON: ${errorMessage(error)}`
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
let response;
|
|
25
|
+
try {
|
|
26
|
+
response = await fetcher(url);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
return {
|
|
29
|
+
exitCode: 2,
|
|
30
|
+
message: `Network error fetching upstream work-types.json: ${errorMessage(error)}`
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
if (response.status === 404) {
|
|
34
|
+
return {
|
|
35
|
+
exitCode: 0,
|
|
36
|
+
message: `Upstream work-types.json not yet published at ${url}; skipping drift check.`
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
if (!response.ok) {
|
|
40
|
+
return {
|
|
41
|
+
exitCode: 2,
|
|
42
|
+
message: `Failed to fetch upstream work-types.json: HTTP ${response.status} ${response.statusText}`
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const upstreamText = await response.text();
|
|
46
|
+
let upstreamJson;
|
|
47
|
+
try {
|
|
48
|
+
upstreamJson = JSON.parse(upstreamText);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
return {
|
|
51
|
+
exitCode: 3,
|
|
52
|
+
message: `Upstream work-types.json is not valid JSON: ${errorMessage(error)}`
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
if (!hasExpectedTopLevelShape(upstreamJson)) {
|
|
56
|
+
return {
|
|
57
|
+
exitCode: 3,
|
|
58
|
+
message: "Upstream work-types.json does not match the expected schema shape (missing `tiers` or `types`)."
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const normalisedLocal = stripLocalOnlyFields(localJson);
|
|
62
|
+
if (deepEqual(normalisedLocal, upstreamJson)) {
|
|
63
|
+
return {
|
|
64
|
+
exitCode: 0,
|
|
65
|
+
message: "Local work-types.json matches upstream."
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
exitCode: 1,
|
|
70
|
+
message: `Drift detected. Local and upstream work-types.json differ. Run \`nmr work-types:sync\` to update from upstream.
|
|
71
|
+
Local: ${localPath}
|
|
72
|
+
Upstream: ${url}`
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function stripLocalOnlyFields(value) {
|
|
76
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
const record = { ...value };
|
|
80
|
+
delete record.$schema;
|
|
81
|
+
return record;
|
|
82
|
+
}
|
|
83
|
+
function deepEqual(a, b) {
|
|
84
|
+
if (a === b) return true;
|
|
85
|
+
if (typeof a !== typeof b) return false;
|
|
86
|
+
if (a === null || b === null) return a === b;
|
|
87
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
88
|
+
if (a.length !== b.length) return false;
|
|
89
|
+
for (let i = 0; i < a.length; i++) {
|
|
90
|
+
if (!deepEqual(a[i], b[i])) return false;
|
|
91
|
+
}
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
if (typeof a === "object" && typeof b === "object" && !Array.isArray(a) && !Array.isArray(b)) {
|
|
95
|
+
const aRecord = { ...a };
|
|
96
|
+
const bRecord = { ...b };
|
|
97
|
+
const aKeys = Object.keys(aRecord);
|
|
98
|
+
const bKeys = Object.keys(bRecord);
|
|
99
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
100
|
+
for (const key of aKeys) {
|
|
101
|
+
if (!deepEqual(aRecord[key], bRecord[key])) return false;
|
|
102
|
+
}
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
export {
|
|
108
|
+
UPSTREAM_WORK_TYPES_URL,
|
|
109
|
+
checkWorkTypesDrift
|
|
110
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
function createPolicyViolationCollector() {
|
|
2
|
+
const violations = [];
|
|
3
|
+
const onPolicyViolation = (commit, type, surface) => {
|
|
4
|
+
violations.push({
|
|
5
|
+
commitHash: commit.hash,
|
|
6
|
+
commitSubject: commit.message.split("\n", 1)[0] ?? "",
|
|
7
|
+
type,
|
|
8
|
+
surface
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
return { violations, onPolicyViolation };
|
|
12
|
+
}
|
|
13
|
+
export {
|
|
14
|
+
createPolicyViolationCollector
|
|
15
|
+
};
|
|
@@ -4,10 +4,20 @@ export interface CreateGithubReleaseOptions {
|
|
|
4
4
|
dryRun: boolean;
|
|
5
5
|
sectionOrder?: string[];
|
|
6
6
|
}
|
|
7
|
-
export
|
|
7
|
+
export type CreateReleaseSkipReason = 'no-entry' | 'no-audience-content' | 'empty-body';
|
|
8
|
+
export type CreateReleaseResult = {
|
|
9
|
+
status: 'created';
|
|
10
|
+
} | {
|
|
11
|
+
status: 'skipped';
|
|
12
|
+
reason: CreateReleaseSkipReason;
|
|
13
|
+
};
|
|
14
|
+
export declare function createGithubRelease(options: CreateGithubReleaseOptions): CreateReleaseResult;
|
|
8
15
|
export interface CreateGithubReleasesOutcome {
|
|
9
16
|
created: string[];
|
|
10
|
-
skipped:
|
|
17
|
+
skipped: Array<{
|
|
18
|
+
tag: string;
|
|
19
|
+
reason: CreateReleaseSkipReason;
|
|
20
|
+
}>;
|
|
11
21
|
}
|
|
12
22
|
export declare function createGithubReleases(tags: Array<{
|
|
13
23
|
tag: string;
|