chore(changelog): retire CHANGELOG.md as a dependency
The update dialog (server and desktop) now reads changelog/index.json and renders title, intro and groups; the release workflow builds the GitHub Release body and name from changelog/<version>.md; oc-dev, the issue-intake agent, AGENTS.md and the changelog skill no longer point at the file. CHANGELOG.md stays as a legacy copy for installs up to 1.22.1, which fetch it for update notes. The generator refreshes it while it exists and never recreates it, so deleting it after 2026-09-19 retires it for good. Generated outputs hold released versions only, so editing unreleased.md never makes them stale: agents write that file and nothing else, and oc-dev create-release does the generation. Claude-Session: https://claude.ai/code/session_01VqV56Hez25hTxXH4ipJfzH
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
#!/usr/bin/env node
|
||||
// Render the changelog outputs from `changelog/*.md`.
|
||||
// Render the changelog outputs from `changelog/*.md`. `oc-dev create-release`
|
||||
// is the normal caller; agents only edit `changelog/unreleased.md`.
|
||||
//
|
||||
// node scripts/changelog/build.mjs write CHANGELOG.md, packages/vscode/CHANGELOG.md, changelog/index.json
|
||||
// node scripts/changelog/build.mjs write packages/vscode/CHANGELOG.md, changelog/index.json (and CHANGELOG.md while it exists)
|
||||
// node scripts/changelog/build.mjs --check exit 1 when any output differs from what is committed
|
||||
// node scripts/changelog/build.mjs --release 1.2.3 [--date YYYY-MM-DD]
|
||||
// move unreleased.md to 1.2.3.md (dated today by default), then write
|
||||
@@ -29,7 +30,9 @@ try {
|
||||
console.log(`Promoted changelog/unreleased.md to ${path.relative(repoRoot, created)}`);
|
||||
}
|
||||
|
||||
const outputs = renderOutputs(loadReleases(changelogDirectory));
|
||||
// The legacy app changelog is refreshed while it exists and never recreated.
|
||||
const legacyAppChangelog = fs.existsSync(path.join(repoRoot, 'CHANGELOG.md'));
|
||||
const outputs = renderOutputs(loadReleases(changelogDirectory), { legacyAppChangelog });
|
||||
const stale = [];
|
||||
for (const [relativePath, content] of Object.entries(outputs)) {
|
||||
const target = path.join(repoRoot, relativePath);
|
||||
|
||||
+33
-28
@@ -1,19 +1,21 @@
|
||||
// Source of truth for release notes: one Markdown file per release under
|
||||
// `changelog/`, plus `changelog/unreleased.md` for what is not shipped yet.
|
||||
// This module parses those files, validates their shape, and renders the
|
||||
// three generated outputs: `CHANGELOG.md` (app), `packages/vscode/CHANGELOG.md`
|
||||
// (extension, read by the Marketplace as is), and `changelog/index.json`.
|
||||
// generated outputs: `packages/vscode/CHANGELOG.md` (extension, read by the
|
||||
// Marketplace as is) and `changelog/index.json` (website and update dialog).
|
||||
// Only released versions are rendered; `unreleased.md` is read straight from
|
||||
// the source by whoever needs it, so editing it never makes an output stale.
|
||||
//
|
||||
// The generated Markdown keeps today's `## [x.y.z] - YYYY-MM-DD` headers: the
|
||||
// update dialog, the release workflow, and the release script match them by
|
||||
// regex. Groups render as `### New` / `### Improvements` / `### Fixes` /
|
||||
// `### Misc` inside each release.
|
||||
// `CHANGELOG.md` is legacy: app versions up to 1.22.1 fetch it from `main` for
|
||||
// their update notes. It is refreshed only while it exists and is never
|
||||
// recreated, so deleting it retires it for good.
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
export const GROUPS = ['New', 'Improvements', 'Fixes', 'Misc'];
|
||||
const GENERATED_BANNER = '<!-- Generated from changelog/*.md by `bun run changelog:build`. Edit those files, not this one. -->';
|
||||
const GENERATED_BANNER = '<!-- Generated from changelog/*.md by `oc-dev create-release`. Edit those files, not this one. -->';
|
||||
const LEGACY_BANNER = '<!-- Legacy copy for app versions up to 1.22.1, which fetch this file for their update notes. Generated from changelog/*.md while it exists; delete it after 2026-09-19 and nothing will recreate it. -->';
|
||||
export const SURFACES = ['App', 'VS Code'];
|
||||
|
||||
const VERSION_PATTERN = /^\d+\.\d+\.\d+$/;
|
||||
@@ -172,23 +174,20 @@ const renderSection = (release, groups, intro) => {
|
||||
return parts.join('\n').replace(/\n+$/, '\n');
|
||||
};
|
||||
|
||||
/** `CHANGELOG.md`: the app notes, unreleased first, every release after. */
|
||||
export const renderAppChangelog = ({ unreleased, releases }) => {
|
||||
const sections = [];
|
||||
sections.push(renderSection(unreleased ?? { version: null }, unreleased?.app, unreleased?.intro ?? []));
|
||||
for (const release of releases) sections.push(renderSection(release, release.app, release.intro));
|
||||
return `# Changelog\n\n${GENERATED_BANNER}\n\nAll notable changes to this project will be documented in this file.\n\n${sections.join('\n')}`;
|
||||
};
|
||||
/** `CHANGELOG.md` (legacy): every released version's app notes. */
|
||||
export const renderAppChangelog = ({ releases }) =>
|
||||
`# Changelog\n\n${LEGACY_BANNER}\n\n${releases.map((release) => renderSection(release, release.app, release.intro)).join('\n')}`;
|
||||
|
||||
/** `packages/vscode/CHANGELOG.md`: only releases that carry a VS Code section. */
|
||||
export const renderVsCodeChangelog = ({ unreleased, releases }) => {
|
||||
const sections = [];
|
||||
sections.push(renderSection(unreleased ?? { version: null }, unreleased?.vscode, []));
|
||||
for (const release of releases) {
|
||||
if (!release.vscode) continue;
|
||||
sections.push(renderSection(release, release.vscode, []));
|
||||
}
|
||||
return `${GENERATED_BANNER}\n\n${sections.join('\n')}`;
|
||||
export const renderVsCodeChangelog = ({ releases }) =>
|
||||
`${GENERATED_BANNER}\n\n${releases.filter((release) => release.vscode).map((release) => renderSection(release, release.vscode, [])).join('\n')}`;
|
||||
|
||||
/** GitHub Release body for one release: intro and groups, no version header. */
|
||||
export const renderReleaseNotes = (release) => {
|
||||
const parts = [];
|
||||
if (release.intro.length > 0) parts.push(release.intro.join('\n'), '');
|
||||
parts.push(renderGroups(release.app));
|
||||
return `${parts.join('\n').trim()}\n`;
|
||||
};
|
||||
|
||||
const groupsToJson = (groups) => {
|
||||
@@ -208,12 +207,18 @@ export const renderIndex = ({ releases }) => `${JSON.stringify(releases.map((rel
|
||||
vscode: groupsToJson(release.vscode),
|
||||
})), null, 2)}\n`;
|
||||
|
||||
/** Every generated file, keyed by path relative to the repo root. */
|
||||
export const renderOutputs = (loaded) => ({
|
||||
'CHANGELOG.md': renderAppChangelog(loaded),
|
||||
'packages/vscode/CHANGELOG.md': renderVsCodeChangelog(loaded),
|
||||
'changelog/index.json': renderIndex(loaded),
|
||||
});
|
||||
/**
|
||||
* Every generated file, keyed by path relative to the repo root. The legacy
|
||||
* `CHANGELOG.md` is included only on request (the build passes whether the
|
||||
* file still exists).
|
||||
*/
|
||||
export const renderOutputs = (loaded, { legacyAppChangelog = false } = {}) => {
|
||||
const outputs = {};
|
||||
if (legacyAppChangelog) outputs['CHANGELOG.md'] = renderAppChangelog(loaded);
|
||||
outputs['packages/vscode/CHANGELOG.md'] = renderVsCodeChangelog(loaded);
|
||||
outputs['changelog/index.json'] = renderIndex(loaded);
|
||||
return outputs;
|
||||
};
|
||||
|
||||
export const UNRELEASED_TEMPLATE = `---
|
||||
title:
|
||||
|
||||
@@ -4,9 +4,9 @@ import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
|
||||
import { loadReleases, parseRelease, promoteUnreleased, renderAppChangelog, renderIndex, renderVsCodeChangelog } from './lib.mjs';
|
||||
import { loadReleases, parseRelease, promoteUnreleased, renderAppChangelog, renderIndex, renderOutputs, renderReleaseNotes, renderVsCodeChangelog } from './lib.mjs';
|
||||
|
||||
const banner = '<!-- Generated from changelog/*.md by `bun run changelog:build`. Edit those files, not this one. -->';
|
||||
const banner = '<!-- Generated from changelog/*.md by `oc-dev create-release`. Edit those files, not this one. -->';
|
||||
|
||||
const release = `---
|
||||
version: 1.2.3
|
||||
@@ -51,26 +51,16 @@ test('rejects shapes the generator cannot render', () => {
|
||||
assert.throws(() => parseRelease('---\nversion: 1.2\ndate: 2026-01-31\n---\n', 'f.md'), /is not x\.y\.z/);
|
||||
});
|
||||
|
||||
test('renders groups in canonical order with today\'s headers and skips versions without a VS Code section', () => {
|
||||
test('renders released versions only, groups in canonical order, and skips versions without a VS Code section', () => {
|
||||
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'changelog-'));
|
||||
fs.writeFileSync(path.join(directory, '1.2.3.md'), release);
|
||||
fs.writeFileSync(path.join(directory, '1.2.4.md'), '---\nversion: 1.2.4\ndate: 2026-02-01\ntitle: Faster\n---\n\n## App\n\n### Improvements\n- Faster.\n');
|
||||
fs.writeFileSync(path.join(directory, 'unreleased.md'), '## App\n\n### Fixes\n- Pending fix.\n\n## VS Code\n');
|
||||
fs.writeFileSync(path.join(directory, 'unreleased.md'), '---\ntitle: Pending\n---\n\n## App\n\n### Fixes\n- Pending fix.\n\n## VS Code\n');
|
||||
const loaded = loadReleases(directory);
|
||||
|
||||
assert.equal(renderAppChangelog(loaded), `# Changelog
|
||||
|
||||
${banner}
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixes
|
||||
|
||||
- Pending fix.
|
||||
|
||||
## [1.2.4] - 2026-02-01
|
||||
const app = renderAppChangelog(loaded);
|
||||
assert.match(app, /^# Changelog\n\n<!-- Legacy copy for app versions up to 1\.22\.1/);
|
||||
assert.equal(app.slice(app.indexOf('## [1.2.4]')), `## [1.2.4] - 2026-02-01
|
||||
|
||||
### Improvements
|
||||
|
||||
@@ -88,16 +78,26 @@ A short intro.
|
||||
|
||||
- Chat: huge patches open without freezing the page (thanks to @someone).
|
||||
`);
|
||||
assert.equal(app.includes('Unreleased'), false);
|
||||
|
||||
assert.equal(renderVsCodeChangelog(loaded), `${banner}
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.2.3] - 2026-01-31
|
||||
|
||||
### New
|
||||
|
||||
- Comments on code.
|
||||
`);
|
||||
|
||||
assert.equal(renderReleaseNotes(loaded.releases[1]), `A short intro.
|
||||
|
||||
### New
|
||||
|
||||
- **Comments:** select text and comment on it.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Chat: huge patches open without freezing the page (thanks to @someone).
|
||||
`);
|
||||
|
||||
const index = JSON.parse(renderIndex(loaded));
|
||||
@@ -106,6 +106,9 @@ A short intro.
|
||||
assert.equal(index[1].title, 'Comments everywhere');
|
||||
assert.deepEqual(index[1].vscode, { new: ['Comments on code.'], improvements: [], fixes: [], misc: [] });
|
||||
assert.equal(index[0].vscode, null);
|
||||
|
||||
assert.deepEqual(Object.keys(renderOutputs(loaded)), ['packages/vscode/CHANGELOG.md', 'changelog/index.json']);
|
||||
assert.deepEqual(Object.keys(renderOutputs(loaded, { legacyAppChangelog: true })), ['CHANGELOG.md', 'packages/vscode/CHANGELOG.md', 'changelog/index.json']);
|
||||
});
|
||||
|
||||
test('loadReleases refuses a file whose name and version disagree, and a release without a title', () => {
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env node
|
||||
// GitHub Release body and name for one version, from `changelog/<version>.md`.
|
||||
//
|
||||
// node scripts/changelog/release-notes.mjs 1.2.3 artifacts/release-notes.md
|
||||
//
|
||||
// Writes the body to the given file and prints the release title on stdout.
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { loadReleases, renderReleaseNotes } from './lib.mjs';
|
||||
|
||||
const [version, outFile] = process.argv.slice(2);
|
||||
if (!version || !outFile) {
|
||||
console.error('usage: release-notes.mjs <version> <out-file>');
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
||||
try {
|
||||
const { releases } = loadReleases(path.join(repoRoot, 'changelog'));
|
||||
const release = releases.find((entry) => entry.version === version);
|
||||
if (!release) throw new Error(`changelog/${version}.md not found; run "oc-dev create-release" before tagging`);
|
||||
fs.mkdirSync(path.dirname(outFile), { recursive: true });
|
||||
fs.writeFileSync(outFile, renderReleaseNotes(release));
|
||||
process.stdout.write(`${release.title}\n`);
|
||||
} catch (error) {
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
process.exit(1);
|
||||
}
|
||||
+4
-2
@@ -175,7 +175,9 @@ function step(label, fn) {
|
||||
}
|
||||
|
||||
// The release notes source plus the files generated from it; all go into the release commit.
|
||||
const RELEASE_CHANGELOG_FILES = ['changelog', 'CHANGELOG.md', 'packages/vscode/CHANGELOG.md'];
|
||||
// CHANGELOG.md is legacy (older installs read it for update notes); it is
|
||||
// refreshed only while it exists.
|
||||
const RELEASE_CHANGELOG_FILES = ['changelog', 'packages/vscode/CHANGELOG.md', ...(fs.existsSync('CHANGELOG.md') ? ['CHANGELOG.md'] : [])];
|
||||
|
||||
function printReleaseNextSteps(version) {
|
||||
log.success(`Release v${version} prepared locally`);
|
||||
@@ -602,7 +604,7 @@ async function createRelease(options) {
|
||||
}
|
||||
if (!/^\d+\.\d+\.\d+(-[a-zA-Z0-9.]+)?$/.test(version)) throw new Error('Invalid version format. Use semver, e.g. 1.4.7 or 1.4.7-beta.1');
|
||||
// Turns changelog/unreleased.md into changelog/<version>.md dated today and
|
||||
// regenerates CHANGELOG.md; fails when nothing was written for the release.
|
||||
// regenerates the outputs; fails when nothing was written for the release.
|
||||
step('Promoting the changelog', () => run('node', ['scripts/changelog/build.mjs', '--release', version]));
|
||||
step('Validating codebase', () => run('bun', ['run', 'release:prepare']));
|
||||
step(`Bumping version to ${version}`, () => run('node', ['scripts/bump-version.mjs', version]));
|
||||
|
||||
Reference in New Issue
Block a user