forked from colbymchenry/codegraph
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathprepare-release.test.ts
More file actions
221 lines (198 loc) · 9.18 KB
/
Copy pathprepare-release.test.ts
File metadata and controls
221 lines (198 loc) · 9.18 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
/**
* Unit tests for `scripts/prepare-release.mjs`.
*
* The script reads CHANGELOG.md and package.json from `process.cwd()`,
* so the tests run it via `node` in a temp directory after staging
* those files. Real script, real fs — keeps the test honest about what
* the workflow will actually do.
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { execFileSync } from 'node:child_process';
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as os from 'node:os';
const SCRIPT = path.resolve(__dirname, '..', 'scripts', 'prepare-release.mjs');
function run(cwd: string, ...args: string[]) {
const out = execFileSync('node', [SCRIPT, ...args], { cwd, encoding: 'utf8' });
return out.trim();
}
function setup(changelog: string, version = '1.2.3') {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'prepare-release-'));
fs.writeFileSync(path.join(dir, 'CHANGELOG.md'), changelog);
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ name: 'x', version }));
return dir;
}
const HEADER = `# Changelog
Some intro.
`;
describe('prepare-release.mjs', () => {
let dir: string;
afterEach(() => {
if (dir && fs.existsSync(dir)) fs.rmSync(dir, { recursive: true, force: true });
});
describe('Case A: [version] block does not yet exist', () => {
it('renames [Unreleased] to [version] - <today> and adds a fresh empty [Unreleased]', () => {
dir = setup(
HEADER +
`## [Unreleased]\n\n### Added\n- New feature foo\n- New feature bar\n\n### Fixed\n- Fixed thing\n\n## [1.2.2] - 2026-01-01\n\n### Added\n- Old entry\n`,
);
const out = run(dir);
expect(out).toMatch(/renamed \[Unreleased\] to \[1\.2\.3\]/);
const result = fs.readFileSync(path.join(dir, 'CHANGELOG.md'), 'utf8');
// [Unreleased] is now empty and at the top.
expect(result).toMatch(/## \[Unreleased\]\n\n\n## \[1\.2\.3\]/);
// [1.2.3] gets a date.
expect(result).toMatch(/## \[1\.2\.3\] - \d{4}-\d{2}-\d{2}/);
// Promoted content lives under [1.2.3].
const v123Section = result.split('## [1.2.3]')[1].split('## [1.2.2]')[0];
expect(v123Section).toContain('### Added');
expect(v123Section).toContain('- New feature foo');
expect(v123Section).toContain('- New feature bar');
expect(v123Section).toContain('### Fixed');
expect(v123Section).toContain('- Fixed thing');
// [1.2.2] is intact.
expect(result).toContain('## [1.2.2] - 2026-01-01');
expect(result).toContain('- Old entry');
});
});
describe('Case B: [version] already exists AND [Unreleased] has content', () => {
it('merges Unreleased sub-sections into the matching [version] sub-sections', () => {
// The v0.9.5 scenario verbatim: sparse [0.9.5] with two Fixed
// entries, full [Unreleased] above it with Added + more Fixed.
dir = setup(
HEADER +
`## [Unreleased]\n\n### Added\n- Big feature 1\n- Big feature 2\n\n### Fixed\n- Watcher fix\n- Worktree fix\n\n## [1.2.3] - 2026-02-02\n\n### Fixed\n- Old fix A\n- Old fix B\n\n## [1.2.2] - 2026-01-01\n`,
);
const out = run(dir);
expect(out).toMatch(/merged \d+ Unreleased entries/);
const result = fs.readFileSync(path.join(dir, 'CHANGELOG.md'), 'utf8');
// [Unreleased] is emptied.
const unrelSection = result.split('## [Unreleased]')[1].split('## [1.2.3]')[0];
expect(unrelSection.trim()).toBe('');
// [1.2.3] now has BOTH the original Fixed entries AND the
// Unreleased Fixed entries, plus the new Added sub-section.
const v123Section = result.split('## [1.2.3]')[1].split('## [1.2.2]')[0];
expect(v123Section).toContain('### Added');
expect(v123Section).toContain('- Big feature 1');
expect(v123Section).toContain('- Big feature 2');
expect(v123Section).toContain('### Fixed');
expect(v123Section).toContain('- Old fix A');
expect(v123Section).toContain('- Old fix B');
expect(v123Section).toContain('- Watcher fix');
expect(v123Section).toContain('- Worktree fix');
// Date on [1.2.3] is preserved (we don't re-stamp it).
expect(result).toContain('## [1.2.3] - 2026-02-02');
});
it('appends sub-sections that exist only in [Unreleased] to the [version] block', () => {
dir = setup(
HEADER +
`## [Unreleased]\n\n### Security\n- CVE patch\n\n## [1.2.3] - 2026-02-02\n\n### Fixed\n- Old fix\n`,
);
run(dir);
const result = fs.readFileSync(path.join(dir, 'CHANGELOG.md'), 'utf8');
const v123 = result.split('## [1.2.3]')[1];
expect(v123).toContain('### Fixed');
expect(v123).toContain('- Old fix');
expect(v123).toContain('### Security');
expect(v123).toContain('- CVE patch');
});
});
describe('Case C: [Unreleased] has no entries', () => {
it('is a no-op when [Unreleased] is empty', () => {
dir = setup(HEADER + `## [Unreleased]\n\n## [1.2.3] - 2026-02-02\n\n### Fixed\n- thing\n`);
const before = fs.readFileSync(path.join(dir, 'CHANGELOG.md'), 'utf8');
const out = run(dir);
expect(out).toMatch(/nothing to do/);
const after = fs.readFileSync(path.join(dir, 'CHANGELOG.md'), 'utf8');
expect(after).toBe(before);
});
it('is a no-op when [Unreleased] has only sub-section headings with no bullets', () => {
dir = setup(
HEADER + `## [Unreleased]\n\n### Added\n\n### Fixed\n\n## [1.2.3] - 2026-02-02\n`,
);
const before = fs.readFileSync(path.join(dir, 'CHANGELOG.md'), 'utf8');
const out = run(dir);
expect(out).toMatch(/nothing to do/);
const after = fs.readFileSync(path.join(dir, 'CHANGELOG.md'), 'utf8');
expect(after).toBe(before);
});
});
describe('idempotency', () => {
it('running twice produces the same output as running once', () => {
dir = setup(
HEADER +
`## [Unreleased]\n\n### Added\n- Thing A\n\n## [1.2.2] - 2026-01-01\n\n### Added\n- Old\n`,
);
run(dir); // first run promotes
const afterFirst = fs.readFileSync(path.join(dir, 'CHANGELOG.md'), 'utf8');
const out2 = run(dir); // second run should be a no-op
const afterSecond = fs.readFileSync(path.join(dir, 'CHANGELOG.md'), 'utf8');
expect(out2).toMatch(/nothing to do/);
expect(afterSecond).toBe(afterFirst);
});
});
describe('version source', () => {
it('reads the target version from package.json by default', () => {
dir = setup(HEADER + `## [Unreleased]\n\n### Added\n- x\n`, '9.9.9');
run(dir);
const result = fs.readFileSync(path.join(dir, 'CHANGELOG.md'), 'utf8');
expect(result).toContain('## [9.9.9]');
});
it('accepts an explicit version argument that overrides package.json', () => {
dir = setup(HEADER + `## [Unreleased]\n\n### Added\n- x\n`, '9.9.9');
run(dir, '5.5.5');
const result = fs.readFileSync(path.join(dir, 'CHANGELOG.md'), 'utf8');
expect(result).toContain('## [5.5.5]');
expect(result).not.toContain('## [9.9.9]');
});
});
describe('link reference', () => {
it('appends a `[version]: https://...` link reference at EOF when promoting (Case A)', () => {
dir = setup(HEADER + `## [Unreleased]\n\n### Added\n- x\n\n## [1.2.2] - 2026-01-01\n`);
run(dir);
const result = fs.readFileSync(path.join(dir, 'CHANGELOG.md'), 'utf8');
expect(result).toContain(
'[1.2.3]: https://github.com/colbymchenry/codegraph/releases/tag/v1.2.3',
);
});
it('appends a link reference when merging into an existing [version] (Case B)', () => {
dir = setup(
HEADER + `## [Unreleased]\n\n### Added\n- new\n\n## [1.2.3] - 2026-02-02\n\n### Fixed\n- prior\n`,
);
run(dir);
const result = fs.readFileSync(path.join(dir, 'CHANGELOG.md'), 'utf8');
expect(result).toContain(
'[1.2.3]: https://github.com/colbymchenry/codegraph/releases/tag/v1.2.3',
);
});
it('does not double-add an existing link reference', () => {
const ref = '[1.2.3]: https://github.com/colbymchenry/codegraph/releases/tag/v1.2.3';
dir = setup(
HEADER +
`## [Unreleased]\n\n### Added\n- x\n\n## [1.2.2] - 2026-01-01\n\n${ref}\n`,
);
run(dir);
const result = fs.readFileSync(path.join(dir, 'CHANGELOG.md'), 'utf8');
const occurrences = result.split(ref).length - 1;
expect(occurrences).toBe(1);
});
});
describe('extractor integration', () => {
it('the resulting [version] block is what extract-release-notes.mjs would surface', () => {
// Run prepare, then extract — confirm the output contains all the
// promoted entries.
dir = setup(
HEADER +
`## [Unreleased]\n\n### Added\n- Feature A\n- Feature B\n\n### Fixed\n- Bug fix\n\n## [1.2.2] - 2026-01-01\n`,
);
run(dir);
const extractor = path.resolve(__dirname, '..', 'scripts', 'extract-release-notes.mjs');
const notes = execFileSync('node', [extractor, '1.2.3'], { cwd: dir, encoding: 'utf8' });
expect(notes).toContain('### Added');
expect(notes).toContain('Feature A');
expect(notes).toContain('Feature B');
expect(notes).toContain('### Fixed');
expect(notes).toContain('Bug fix');
});
});
});