@rheged-studio/changelog-core 1.1.1
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/LICENSE +21 -0
- package/README.md +92 -0
- package/changelog-contract.md +129 -0
- package/config.json +9 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +106 -0
- package/dist/commands/add-links.d.ts +8 -0
- package/dist/commands/add-links.d.ts.map +1 -0
- package/dist/commands/add-links.js +258 -0
- package/dist/commands/backfill-commits.d.ts +19 -0
- package/dist/commands/backfill-commits.d.ts.map +1 -0
- package/dist/commands/backfill-commits.js +236 -0
- package/dist/commands/check-completeness.d.ts +13 -0
- package/dist/commands/check-completeness.d.ts.map +1 -0
- package/dist/commands/check-completeness.js +164 -0
- package/dist/commands/enrich.d.ts +17 -0
- package/dist/commands/enrich.d.ts.map +1 -0
- package/dist/commands/enrich.js +194 -0
- package/dist/commands/finalise.d.ts +25 -0
- package/dist/commands/finalise.d.ts.map +1 -0
- package/dist/commands/finalise.js +249 -0
- package/dist/commands/set-affected-packages.d.ts +13 -0
- package/dist/commands/set-affected-packages.d.ts.map +1 -0
- package/dist/commands/set-affected-packages.js +207 -0
- package/dist/commands/validate.d.ts +6 -0
- package/dist/commands/validate.d.ts.map +1 -0
- package/dist/commands/validate.js +293 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/lib/blank.d.ts +5 -0
- package/dist/lib/blank.d.ts.map +1 -0
- package/dist/lib/blank.js +6 -0
- package/dist/lib/changelog.d.ts +6 -0
- package/dist/lib/changelog.d.ts.map +1 -0
- package/dist/lib/changelog.js +47 -0
- package/dist/lib/cli-entry.d.ts +10 -0
- package/dist/lib/cli-entry.d.ts.map +1 -0
- package/dist/lib/cli-entry.js +22 -0
- package/dist/lib/commit-count.d.ts +16 -0
- package/dist/lib/commit-count.d.ts.map +1 -0
- package/dist/lib/commit-count.js +72 -0
- package/dist/lib/config.d.ts +25 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +156 -0
- package/dist/lib/derive-packages.d.ts +10 -0
- package/dist/lib/derive-packages.d.ts.map +1 -0
- package/dist/lib/derive-packages.js +42 -0
- package/dist/lib/enrich.d.ts +29 -0
- package/dist/lib/enrich.d.ts.map +1 -0
- package/dist/lib/enrich.js +54 -0
- package/dist/lib/frontmatter.d.ts +11 -0
- package/dist/lib/frontmatter.d.ts.map +1 -0
- package/dist/lib/frontmatter.js +355 -0
- package/dist/lib/stamp.d.ts +10 -0
- package/dist/lib/stamp.d.ts.map +1 -0
- package/dist/lib/stamp.js +35 -0
- package/dist/lib/vendor/issue-keys.d.ts +16 -0
- package/dist/lib/vendor/issue-keys.d.ts.map +1 -0
- package/dist/lib/vendor/issue-keys.js +29 -0
- package/package.json +124 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
// Release-time finalisation of changelog entries — in-repo via
|
|
2
|
+
// `reusable-changelog-enrich.yml` mode `finalise` / `changelog-core finalise`
|
|
3
|
+
// after release-please bumps package.json (A-801 retired the central orchestrator
|
|
4
|
+
// finalise). Reads the just-bumped version from package.json.
|
|
5
|
+
//
|
|
6
|
+
// For every entry that isn't finalised yet (empty `version`):
|
|
7
|
+
// 1. resolve its merged PR from the `branch` field via `gh` and enrich
|
|
8
|
+
// (merged_at / commit / pr / stats);
|
|
9
|
+
// 2. stamp `version` with the just-bumped package.json version;
|
|
10
|
+
// 3. rewrite bare Linear IDs to links.
|
|
11
|
+
//
|
|
12
|
+
// The pure `finaliseEntry(raw, version, resolvePr)` is unit-testable with a fake
|
|
13
|
+
// resolver; main() wires the real `gh`/`git` resolver and walks the directory.
|
|
14
|
+
//
|
|
15
|
+
// Zero-dep: composes the package's own modules (lib/enrich, lib/stamp,
|
|
16
|
+
// add-links) and the frontmatter parser — no gray-matter, no tsx — so it runs
|
|
17
|
+
// under bare `node`. The Linear workspace/issue keys come from config.json via
|
|
18
|
+
// add-links, not hardcoded constants.
|
|
19
|
+
import { blank } from "../lib/blank.js";
|
|
20
|
+
import { isCliEntry } from "../lib/cli-entry.js";
|
|
21
|
+
import { nonMergeCommitCount, realRunner } from "../lib/commit-count.js";
|
|
22
|
+
import { loadConfig } from "../lib/config.js";
|
|
23
|
+
import { enrichFrontmatter } from "../lib/enrich.js";
|
|
24
|
+
import { parseFrontmatter } from "../lib/frontmatter.js";
|
|
25
|
+
import { readPackageVersion, stampVersion } from "../lib/stamp.js";
|
|
26
|
+
import { rewriteBody, splitFrontmatter } from "./add-links.js";
|
|
27
|
+
import { readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
28
|
+
import { join } from "node:path";
|
|
29
|
+
import { argv } from "node:process";
|
|
30
|
+
/**
|
|
31
|
+
* Finalise one entry's raw markdown for release. Returns the rewritten markdown,
|
|
32
|
+
* or null when nothing changed (already finalised).
|
|
33
|
+
*/
|
|
34
|
+
export function finaliseEntry(raw, version, resolvePr) {
|
|
35
|
+
const fm = parseFrontmatter(raw).data;
|
|
36
|
+
if (!blank(fm.version)) {
|
|
37
|
+
return null; // already shipped in a release
|
|
38
|
+
}
|
|
39
|
+
let next = raw;
|
|
40
|
+
const branch = typeof fm.branch === "string" ? fm.branch : "";
|
|
41
|
+
// Include blank(fm.stats) so a hand-authored entry that pre-fills
|
|
42
|
+
// merged_at/commit/pr but leaves stats blank still gets stats from the PR.
|
|
43
|
+
// Also treat a populated-but-commits-less stats block as enrichable: an entry
|
|
44
|
+
// finalised in the window between `stats` first existing (A-380) and
|
|
45
|
+
// `stats.commits` being added (A-560) has every other field set, so without
|
|
46
|
+
// the `stats.commits` check needsEnrich is false, enrich is skipped, the entry
|
|
47
|
+
// is version-stamped, and the later version short-circuit makes the missing
|
|
48
|
+
// `commits` un-backfillable through finalise forever (A-579).
|
|
49
|
+
const stats = typeof fm.stats === "object" &&
|
|
50
|
+
fm.stats !== null &&
|
|
51
|
+
!Array.isArray(fm.stats)
|
|
52
|
+
? fm.stats
|
|
53
|
+
: null;
|
|
54
|
+
const needsEnrich = blank(fm.merged_at) ||
|
|
55
|
+
blank(fm.commit) ||
|
|
56
|
+
blank(fm.pr) ||
|
|
57
|
+
blank(fm.stats) ||
|
|
58
|
+
blank(stats?.commits);
|
|
59
|
+
if (branch && needsEnrich) {
|
|
60
|
+
const pr = resolvePr(branch);
|
|
61
|
+
if (pr) {
|
|
62
|
+
next = enrichFrontmatter(next, {
|
|
63
|
+
additions: pr.additions,
|
|
64
|
+
branch,
|
|
65
|
+
changedFiles: pr.changedFiles,
|
|
66
|
+
commits: pr.commits,
|
|
67
|
+
deletions: pr.deletions,
|
|
68
|
+
mergedAt: pr.mergedAt,
|
|
69
|
+
mergeSha: pr.mergeSha,
|
|
70
|
+
prNumber: pr.prNumber,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
next = stampVersion(next, version) ?? next;
|
|
75
|
+
const { body, fm: fmText } = splitFrontmatter(next);
|
|
76
|
+
next = fmText + rewriteBody(body);
|
|
77
|
+
return next === raw ? null : next;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Build a PR resolver backed by `gh` (injectable runner for tests).
|
|
81
|
+
*/
|
|
82
|
+
export function makeResolver(run) {
|
|
83
|
+
function resolve(branch) {
|
|
84
|
+
const json = run("gh", [
|
|
85
|
+
"pr",
|
|
86
|
+
"list",
|
|
87
|
+
"--head",
|
|
88
|
+
branch,
|
|
89
|
+
"--state",
|
|
90
|
+
"merged",
|
|
91
|
+
"--limit",
|
|
92
|
+
"1",
|
|
93
|
+
"--json",
|
|
94
|
+
"number,mergedAt,additions,deletions,changedFiles,mergeCommit",
|
|
95
|
+
]);
|
|
96
|
+
const list = JSON.parse(json);
|
|
97
|
+
if (!Array.isArray(list) || list.length === 0) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
const pr = list[0];
|
|
101
|
+
const mergeSha = pr.mergeCommit?.oid ?? "";
|
|
102
|
+
// Commit count is resolved separately (a second API call). Keep it
|
|
103
|
+
// independently best-effort: a failure here leaves commits null but must NOT
|
|
104
|
+
// discard the stats we already resolved, so it gets its own try/catch rather
|
|
105
|
+
// than riding the outer one (which would null the whole ResolvedPr).
|
|
106
|
+
let commits = null;
|
|
107
|
+
if (pr.number !== undefined && pr.number !== null) {
|
|
108
|
+
try {
|
|
109
|
+
commits = nonMergeCommitCount(run, pr.number);
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
113
|
+
console.warn(`⚠️ Could not resolve commit count for PR #${pr.number}: ${message}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// Absent numeric fields stay null (not ""), so the enrich guard skips them
|
|
117
|
+
// rather than parsing "" into NaN.
|
|
118
|
+
return {
|
|
119
|
+
additions: pr.additions === undefined ? null : String(pr.additions),
|
|
120
|
+
changedFiles: pr.changedFiles === undefined ? null : String(pr.changedFiles),
|
|
121
|
+
commits,
|
|
122
|
+
deletions: pr.deletions === undefined ? null : String(pr.deletions),
|
|
123
|
+
mergedAt: pr.mergedAt ?? "",
|
|
124
|
+
mergeSha,
|
|
125
|
+
prNumber: String(pr.number ?? ""),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
return (branch) => {
|
|
129
|
+
// Enrichment is best-effort metadata: a gh/git failure here must NOT abort
|
|
130
|
+
// the release-please release-PR build and block the release. On any error,
|
|
131
|
+
// warn and return null — the entry still gets version-stamped, just without
|
|
132
|
+
// PR metadata.
|
|
133
|
+
try {
|
|
134
|
+
return resolve(branch);
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
138
|
+
console.warn(`⚠️ Could not resolve PR for branch ${branch}: ${message}`);
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
const USAGE = `finalise — release-time enrich + version-stamp the dated changelog/ entries
|
|
144
|
+
|
|
145
|
+
In-repo via \`reusable-changelog-enrich.yml\` mode \`finalise\` / \`changelog-core finalise\`
|
|
146
|
+
after release-please bumps package.json. Reads the just-bumped version, then for every
|
|
147
|
+
un-finalised entry: resolves its merged PR via \`gh\` to enrich (merged_at/commit/pr/stats),
|
|
148
|
+
stamps \`version\`, and rewrites bare Linear IDs to links. WRITES to changelog/ files.
|
|
149
|
+
|
|
150
|
+
Usage:
|
|
151
|
+
changelog-core finalise Finalise every un-finalised entry (writes; needs gh)
|
|
152
|
+
changelog-core finalise --self-test Run the built-in offline smoke test
|
|
153
|
+
changelog-core finalise --help Show this message (alias: -h)`;
|
|
154
|
+
// Offline smoke-test resolver — a fixed fake PR, no gh/git.
|
|
155
|
+
function fakeResolver() {
|
|
156
|
+
return {
|
|
157
|
+
additions: "10",
|
|
158
|
+
changedFiles: "2",
|
|
159
|
+
commits: "4",
|
|
160
|
+
deletions: "3",
|
|
161
|
+
mergedAt: "2026-01-02T00:00:00Z",
|
|
162
|
+
mergeSha: "abcdef1234567890",
|
|
163
|
+
prNumber: "42",
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
// Offline smoke test: exercise the pure finaliseEntry with a fake resolver — no
|
|
167
|
+
// gh, no git, no real package.json, no filesystem writes.
|
|
168
|
+
function selfTest() {
|
|
169
|
+
const cases = [];
|
|
170
|
+
const unfinalised = `---
|
|
171
|
+
title: Sample
|
|
172
|
+
created_at: '2026-01-01T00:00:00Z'
|
|
173
|
+
merged_at:
|
|
174
|
+
branch: a-1-sample
|
|
175
|
+
pr:
|
|
176
|
+
commit:
|
|
177
|
+
version:
|
|
178
|
+
category: feature
|
|
179
|
+
breaking: false
|
|
180
|
+
stats:
|
|
181
|
+
files_changed:
|
|
182
|
+
loc_added:
|
|
183
|
+
loc_removed:
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Added
|
|
187
|
+
|
|
188
|
+
- A thing.
|
|
189
|
+
`;
|
|
190
|
+
const finalised = finaliseEntry(unfinalised, "1.2.3", () => fakeResolver());
|
|
191
|
+
cases.push({
|
|
192
|
+
name: "an un-finalised entry is rewritten",
|
|
193
|
+
ok: typeof finalised === "string" && finalised !== unfinalised,
|
|
194
|
+
});
|
|
195
|
+
cases.push({
|
|
196
|
+
name: "the bumped version is stamped",
|
|
197
|
+
ok: typeof finalised === "string" && finalised.includes("version: 1.2.3"),
|
|
198
|
+
});
|
|
199
|
+
// An already-stamped entry is a no-op (returns null).
|
|
200
|
+
const alreadyDone = (finalised ?? unfinalised).replace(/\n$/, "\n");
|
|
201
|
+
cases.push({
|
|
202
|
+
name: "an already-finalised entry returns null (no rewrite)",
|
|
203
|
+
ok: finaliseEntry(alreadyDone, "1.2.3", () => fakeResolver()) === null,
|
|
204
|
+
});
|
|
205
|
+
let failed = 0;
|
|
206
|
+
for (const { name, ok } of cases) {
|
|
207
|
+
if (ok) {
|
|
208
|
+
console.log(` ok ${name}`);
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
failed += 1;
|
|
212
|
+
console.log(` FAIL ${name}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
console.log(`\n${cases.length - failed}/${cases.length} passed`);
|
|
216
|
+
process.exit(failed === 0 ? 0 : 1);
|
|
217
|
+
}
|
|
218
|
+
export function main() {
|
|
219
|
+
const args = argv.slice(2);
|
|
220
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
221
|
+
console.log(USAGE);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (args.includes("--self-test")) {
|
|
225
|
+
selfTest();
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const config = loadConfig();
|
|
229
|
+
const version = readPackageVersion(readFileSync("package.json", "utf8"));
|
|
230
|
+
const resolvePr = makeResolver(realRunner);
|
|
231
|
+
const files = readdirSync(config.changelogDir)
|
|
232
|
+
.filter((name) => name.endsWith(".md") && name !== "README.md")
|
|
233
|
+
.map((name) => join(config.changelogDir, name));
|
|
234
|
+
let finalised = 0;
|
|
235
|
+
for (const file of files) {
|
|
236
|
+
const next = finaliseEntry(readFileSync(file, "utf8"), version, resolvePr);
|
|
237
|
+
if (next !== null) {
|
|
238
|
+
writeFileSync(file, next);
|
|
239
|
+
finalised++;
|
|
240
|
+
console.log(`finalised ${version}: ${file}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
console.log(`Changelog finalisation complete. ${finalised} entr${finalised === 1 ? "y" : "ies"} finalised with ${version}.`);
|
|
244
|
+
}
|
|
245
|
+
// Only run the filesystem pass when invoked as a CLI, not when imported (e.g.
|
|
246
|
+
// by unit tests exercising finaliseEntry/makeResolver).
|
|
247
|
+
if (isCliEntry(import.meta.filename)) {
|
|
248
|
+
main();
|
|
249
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { FrontmatterData } from "../lib/frontmatter.js";
|
|
2
|
+
/**
|
|
3
|
+
* Rebuild an entry's frontmatter with `affected_packages` set, in canonical
|
|
4
|
+
* field order (`affected_packages` immediately before `stats`).
|
|
5
|
+
*
|
|
6
|
+
* Guards against a destructive overwrite: if `data` is empty or lacks the
|
|
7
|
+
* expected `branch` key — which is what `parseFrontmatter` returns when the
|
|
8
|
+
* entry has no parseable frontmatter — throw rather than clobber the file with
|
|
9
|
+
* just `affected_packages`.
|
|
10
|
+
*/
|
|
11
|
+
export declare function buildAffectedPackagesFrontmatter(data: FrontmatterData, packages: string[]): FrontmatterData;
|
|
12
|
+
export declare function main(): void;
|
|
13
|
+
//# sourceMappingURL=set-affected-packages.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"set-affected-packages.d.ts","sourceRoot":"","sources":["../../src/commands/set-affected-packages.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAK7D;;;;;;;;GAQG;AACH,wBAAgB,gCAAgC,CAC9C,IAAI,EAAE,eAAe,EACrB,QAAQ,EAAE,MAAM,EAAE,GACjB,eAAe,CA6BjB;AAiGD,wBAAgB,IAAI,IAAI,IAAI,CA+E3B"}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
// Merge-time half of changelog enrichment.
|
|
2
|
+
//
|
|
3
|
+
// `affected_packages` is knowable before merge — it's just the set of workspace
|
|
4
|
+
// packages the branch diff touches — so the changelog step computes it at write
|
|
5
|
+
// time rather than waiting for a privileged post-merge step. This script reads
|
|
6
|
+
// the branch diff against the base, maps it through the shared
|
|
7
|
+
// `derivePackagesFromPaths` rule, and writes the result into the changelog entry
|
|
8
|
+
// for the current branch.
|
|
9
|
+
//
|
|
10
|
+
// The post-merge-only fields (`merged_at`, `commit`, and authoritative `stats`)
|
|
11
|
+
// are deliberately NOT touched here — they're owned by the release-orchestrator
|
|
12
|
+
// and stay blank until it fills them.
|
|
13
|
+
//
|
|
14
|
+
// Env overrides (both optional):
|
|
15
|
+
// BASE_REF — base to diff against (default: origin/<baseBranch> from config.json)
|
|
16
|
+
// BRANCH_NAME — entry lookup key (default: current branch via git)
|
|
17
|
+
import { findEntryByBranch } from "../lib/changelog.js";
|
|
18
|
+
import { isCliEntry } from "../lib/cli-entry.js";
|
|
19
|
+
import { loadConfig } from "../lib/config.js";
|
|
20
|
+
import { derivePackagesFromPaths } from "../lib/derive-packages.js";
|
|
21
|
+
import { parseFrontmatter, stringifyFrontmatter } from "../lib/frontmatter.js";
|
|
22
|
+
import { execFileSync } from "node:child_process";
|
|
23
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
24
|
+
import { argv, env } from "node:process";
|
|
25
|
+
/**
|
|
26
|
+
* Rebuild an entry's frontmatter with `affected_packages` set, in canonical
|
|
27
|
+
* field order (`affected_packages` immediately before `stats`).
|
|
28
|
+
*
|
|
29
|
+
* Guards against a destructive overwrite: if `data` is empty or lacks the
|
|
30
|
+
* expected `branch` key — which is what `parseFrontmatter` returns when the
|
|
31
|
+
* entry has no parseable frontmatter — throw rather than clobber the file with
|
|
32
|
+
* just `affected_packages`.
|
|
33
|
+
*/
|
|
34
|
+
export function buildAffectedPackagesFrontmatter(data, packages) {
|
|
35
|
+
if (!data || typeof data !== "object" || !("branch" in data)) {
|
|
36
|
+
throw new Error("Refusing to write affected_packages: entry frontmatter is empty or " +
|
|
37
|
+
"missing the `branch` key — parsing likely failed. Writing would " +
|
|
38
|
+
"overwrite the entry with only affected_packages.");
|
|
39
|
+
}
|
|
40
|
+
const fm = {};
|
|
41
|
+
for (const [key, value] of Object.entries(data)) {
|
|
42
|
+
if (key === "affected_packages") {
|
|
43
|
+
continue; // re-inserted in its canonical slot below
|
|
44
|
+
}
|
|
45
|
+
if (key === "stats") {
|
|
46
|
+
fm.affected_packages = packages;
|
|
47
|
+
}
|
|
48
|
+
fm[key] = value;
|
|
49
|
+
}
|
|
50
|
+
if (!("affected_packages" in fm)) {
|
|
51
|
+
// No `stats` key to anchor against; append (a missing `stats` is itself a
|
|
52
|
+
// contract violation the validator will flag).
|
|
53
|
+
fm.affected_packages = packages;
|
|
54
|
+
}
|
|
55
|
+
return fm;
|
|
56
|
+
}
|
|
57
|
+
function git(args) {
|
|
58
|
+
let out;
|
|
59
|
+
try {
|
|
60
|
+
out = execFileSync("git", args, { encoding: "utf8" });
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
// Most likely cause when run standalone: BASE_REF isn't fetched. In a ship
|
|
64
|
+
// flow the base is fetched before this runs.
|
|
65
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
66
|
+
console.error(`git ${args.join(" ")} failed: ${message}`);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
return out.trim();
|
|
70
|
+
}
|
|
71
|
+
function currentBranch() {
|
|
72
|
+
const fromEnvironment = env.BRANCH_NAME?.trim();
|
|
73
|
+
if (fromEnvironment) {
|
|
74
|
+
return fromEnvironment;
|
|
75
|
+
}
|
|
76
|
+
return git(["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
77
|
+
}
|
|
78
|
+
function changedPaths(base) {
|
|
79
|
+
// Three-dot: files changed on the branch since it diverged from base, so
|
|
80
|
+
// unrelated churn that landed on base meanwhile doesn't leak in.
|
|
81
|
+
const out = git(["diff", "--name-only", `${base}...HEAD`]);
|
|
82
|
+
return out ? out.split("\n") : [];
|
|
83
|
+
}
|
|
84
|
+
const USAGE = `set-affected-packages — write affected_packages into the current branch's changelog entry
|
|
85
|
+
|
|
86
|
+
Monorepo-only (config.json affectedPackages: true; a no-op otherwise). Diffs the
|
|
87
|
+
branch against the base, maps the changed paths through the shared package rule,
|
|
88
|
+
and writes the derived list into the entry's frontmatter (canonical field order).
|
|
89
|
+
|
|
90
|
+
Env overrides: BASE_REF (default origin/<baseBranch>), BRANCH_NAME (default git HEAD).
|
|
91
|
+
|
|
92
|
+
Usage:
|
|
93
|
+
changelog-core set-affected-packages Derive + write affected_packages (writes; needs git)
|
|
94
|
+
changelog-core set-affected-packages --check Report what would change; write nothing (alias: --dry-run)
|
|
95
|
+
changelog-core set-affected-packages --self-test Run the built-in offline smoke test
|
|
96
|
+
changelog-core set-affected-packages --help Show this message (alias: -h)`;
|
|
97
|
+
// Offline smoke test: exercise the pure buildAffectedPackagesFrontmatter — no
|
|
98
|
+
// git, no filesystem.
|
|
99
|
+
function selfTest() {
|
|
100
|
+
const cases = [];
|
|
101
|
+
const data = {
|
|
102
|
+
branch: "a-1-sample",
|
|
103
|
+
stats: { files_changed: null },
|
|
104
|
+
title: "Sample",
|
|
105
|
+
};
|
|
106
|
+
const rebuilt = buildAffectedPackagesFrontmatter(data, ["skills/changelog"]);
|
|
107
|
+
cases.push({
|
|
108
|
+
name: "affected_packages is set from the derived list",
|
|
109
|
+
ok: JSON.stringify(rebuilt.affected_packages) ===
|
|
110
|
+
JSON.stringify(["skills/changelog"]),
|
|
111
|
+
});
|
|
112
|
+
cases.push({
|
|
113
|
+
name: "affected_packages is inserted immediately before stats",
|
|
114
|
+
ok: Object.keys(rebuilt).indexOf("affected_packages") ===
|
|
115
|
+
Object.keys(rebuilt).indexOf("stats") - 1,
|
|
116
|
+
});
|
|
117
|
+
let guardThrew = false;
|
|
118
|
+
try {
|
|
119
|
+
buildAffectedPackagesFrontmatter({}, ["x"]);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
guardThrew = true;
|
|
123
|
+
}
|
|
124
|
+
cases.push({
|
|
125
|
+
name: "refuses to write when frontmatter lacks the branch key",
|
|
126
|
+
ok: guardThrew,
|
|
127
|
+
});
|
|
128
|
+
let failed = 0;
|
|
129
|
+
for (const { name, ok } of cases) {
|
|
130
|
+
if (ok) {
|
|
131
|
+
console.log(` ok ${name}`);
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
failed += 1;
|
|
135
|
+
console.log(` FAIL ${name}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
console.log(`\n${cases.length - failed}/${cases.length} passed`);
|
|
139
|
+
process.exit(failed === 0 ? 0 : 1);
|
|
140
|
+
}
|
|
141
|
+
export function main() {
|
|
142
|
+
const args = argv.slice(2);
|
|
143
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
144
|
+
console.log(USAGE);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (args.includes("--self-test")) {
|
|
148
|
+
selfTest();
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
// --check (alias --dry-run): report what would change and write nothing.
|
|
152
|
+
// Exit 0 when already up to date, 1 when a rewrite is needed — prettier-style,
|
|
153
|
+
// so CI can gate on it.
|
|
154
|
+
const check = args.some((argument) => argument === "--check" || argument === "--dry-run");
|
|
155
|
+
const config = loadConfig();
|
|
156
|
+
// `affected_packages` is monorepo-only. Single-package repos leave
|
|
157
|
+
// `affectedPackages: false` (the default) so entries stay clean — the field
|
|
158
|
+
// is write-only and redundant there, and the validator treats it as
|
|
159
|
+
// optional-when-present. No-op in both normal and --check modes so CI never
|
|
160
|
+
// demands a rewrite. Genuine monorepos opt in via `affectedPackages: true`.
|
|
161
|
+
if (!config.affectedPackages) {
|
|
162
|
+
console.log("affectedPackages is off (config.json sets it false, or omits it and it " +
|
|
163
|
+
"defaults to false) — affected_packages not emitted. Nothing to set.");
|
|
164
|
+
process.exit(0);
|
|
165
|
+
}
|
|
166
|
+
const BASE_REF = env.BASE_REF?.trim() || `origin/${config.baseBranch}`;
|
|
167
|
+
const branch = currentBranch();
|
|
168
|
+
const file = findEntryByBranch(branch, config.changelogDir);
|
|
169
|
+
if (!file) {
|
|
170
|
+
console.log(`No changelog entry found for branch '${branch}'. Nothing to set.`);
|
|
171
|
+
process.exit(0);
|
|
172
|
+
}
|
|
173
|
+
const packages = derivePackagesFromPaths(changedPaths(BASE_REF), {
|
|
174
|
+
changelogDir: config.changelogDir,
|
|
175
|
+
fallbackPackage: config.fallbackPackage,
|
|
176
|
+
packageRoots: config.packageRoots,
|
|
177
|
+
});
|
|
178
|
+
const raw = readFileSync(file, "utf8");
|
|
179
|
+
const parsed = parseFrontmatter(raw);
|
|
180
|
+
// Always overwrite (not fill-only like the post-merge fields): re-running must
|
|
181
|
+
// re-derive affected_packages from the latest branch diff as commits are added.
|
|
182
|
+
//
|
|
183
|
+
// Rebuild in canonical field order via the guarded helper, which refuses to
|
|
184
|
+
// write when the parse yielded empty/branch-less data (a destructive overwrite).
|
|
185
|
+
const fm = buildAffectedPackagesFrontmatter(parsed.data, packages);
|
|
186
|
+
const next = stringifyFrontmatter(parsed.content, fm);
|
|
187
|
+
if (check) {
|
|
188
|
+
if (next === raw) {
|
|
189
|
+
console.log(`affected_packages already up to date on ${file}`);
|
|
190
|
+
console.log(` affected_packages=${JSON.stringify(packages)}`);
|
|
191
|
+
process.exit(0);
|
|
192
|
+
}
|
|
193
|
+
console.log(`[check] would set affected_packages on ${file}`);
|
|
194
|
+
console.log(` branch=${branch} base=${BASE_REF}`);
|
|
195
|
+
console.log(` affected_packages=${JSON.stringify(packages)}`);
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
|
198
|
+
writeFileSync(file, next);
|
|
199
|
+
console.log(`Set affected_packages on ${file}`);
|
|
200
|
+
console.log(` branch=${branch} base=${BASE_REF}`);
|
|
201
|
+
console.log(` affected_packages=${JSON.stringify(packages)}`);
|
|
202
|
+
}
|
|
203
|
+
// Only run when invoked as a CLI, not when imported (e.g. by unit tests
|
|
204
|
+
// exercising `buildAffectedPackagesFrontmatter`).
|
|
205
|
+
if (isCliEntry(import.meta.filename)) {
|
|
206
|
+
main();
|
|
207
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/commands/validate.ts"],"names":[],"mappings":"AAqEA;;GAEG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAkKjE;AA4FD,wBAAgB,IAAI,IAAI,IAAI,CAgC3B"}
|