feat(changelog): one source file per release, generated outputs
Release notes now live in changelog/<version>.md (front matter with version and date, then ## App and ## VS Code sections grouped into New, Improvements, Fixes, Misc) plus changelog/unreleased.md for what has not shipped. `bun run changelog:build` renders CHANGELOG.md, packages/vscode/CHANGELOG.md, and changelog/index.json from them; `changelog:check` fails when the outputs are behind and runs in CI and in release:prepare. `oc-dev create-release` promotes unreleased.md to the versioned file dated today and rebuilds. The existing history was split mechanically: every bullet kept, sorted into groups by keyword, five hand-typed headers with one-digit days normalised to YYYY-MM-DD (the update dialog matched none of them). The generated files keep today's release headers, which the update dialog, the release workflow, and the website match by regex. Claude-Session: https://claude.ai/code/session_01VqV56Hez25hTxXH4ipJfzH
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env node
|
||||
// Render the changelog outputs from `changelog/*.md`.
|
||||
//
|
||||
// node scripts/changelog/build.mjs write CHANGELOG.md, packages/vscode/CHANGELOG.md, changelog/index.json
|
||||
// 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
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { loadReleases, promoteUnreleased, renderOutputs } from './lib.mjs';
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
||||
const changelogDirectory = path.join(repoRoot, 'changelog');
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const readFlag = (name) => {
|
||||
const index = args.indexOf(name);
|
||||
return index >= 0 ? args[index + 1] ?? null : null;
|
||||
};
|
||||
const check = args.includes('--check');
|
||||
const releaseVersion = readFlag('--release');
|
||||
|
||||
try {
|
||||
if (releaseVersion) {
|
||||
const date = readFlag('--date') ?? new Date().toISOString().slice(0, 10);
|
||||
const created = promoteUnreleased(changelogDirectory, releaseVersion, date);
|
||||
console.log(`Promoted changelog/unreleased.md to ${path.relative(repoRoot, created)}`);
|
||||
}
|
||||
|
||||
const outputs = renderOutputs(loadReleases(changelogDirectory));
|
||||
const stale = [];
|
||||
for (const [relativePath, content] of Object.entries(outputs)) {
|
||||
const target = path.join(repoRoot, relativePath);
|
||||
const current = fs.existsSync(target) ? fs.readFileSync(target, 'utf8') : null;
|
||||
if (current === content) continue;
|
||||
if (check) {
|
||||
stale.push(relativePath);
|
||||
continue;
|
||||
}
|
||||
fs.writeFileSync(target, content);
|
||||
console.log(`Wrote ${relativePath}`);
|
||||
}
|
||||
if (check && stale.length > 0) {
|
||||
console.error(`Generated changelog files are out of date: ${stale.join(', ')}. Run "bun run changelog:build" and commit the result.`);
|
||||
process.exit(1);
|
||||
}
|
||||
if (check) console.log('Changelog outputs are up to date.');
|
||||
} catch (error) {
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
// 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`.
|
||||
//
|
||||
// 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.
|
||||
|
||||
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. -->';
|
||||
export const SURFACES = ['App', 'VS Code'];
|
||||
|
||||
const VERSION_PATTERN = /^\d+\.\d+\.\d+$/;
|
||||
const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
||||
|
||||
/**
|
||||
* @typedef {{ [group: string]: string[] }} Groups group name → bullet lines without the leading "- "
|
||||
* @typedef {{ version: string | null, date: string | null, title: string | null, intro: string[], app: Groups | null, vscode: Groups | null, file: string }} Release
|
||||
*/
|
||||
|
||||
const fail = (file, line, message) => {
|
||||
throw new Error(`${file}:${line}: ${message}`);
|
||||
};
|
||||
|
||||
/** Minimal front matter: `key: value` lines between `---` fences. */
|
||||
const parseFrontMatter = (lines, file) => {
|
||||
if (lines[0] !== '---') return { meta: {}, bodyStart: 0 };
|
||||
const end = lines.indexOf('---', 1);
|
||||
if (end < 0) fail(file, 1, 'front matter is not closed');
|
||||
const meta = {};
|
||||
for (let index = 1; index < end; index += 1) {
|
||||
const raw = lines[index];
|
||||
if (!raw.trim()) continue;
|
||||
const separator = raw.indexOf(':');
|
||||
if (separator < 0) fail(file, index + 1, `expected "key: value", got ${JSON.stringify(raw)}`);
|
||||
meta[raw.slice(0, separator).trim()] = raw.slice(separator + 1).trim();
|
||||
}
|
||||
return { meta, bodyStart: end + 1 };
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse one release file. Shape:
|
||||
*
|
||||
* --- (absent for unreleased.md)
|
||||
* version: 1.2.3
|
||||
* date: 2026-01-31
|
||||
* title: optional
|
||||
* ---
|
||||
* optional intro paragraph(s)
|
||||
* ## App
|
||||
* ### New | Improvements | Fixes | Misc
|
||||
* - bullet
|
||||
* ## VS Code
|
||||
* ### ...
|
||||
*/
|
||||
export const parseRelease = (text, file) => {
|
||||
const lines = text.replace(/\r\n/g, '\n').split('\n');
|
||||
const { meta, bodyStart } = parseFrontMatter(lines, file);
|
||||
const release = {
|
||||
version: meta.version ?? null,
|
||||
date: meta.date ?? null,
|
||||
title: meta.title || null,
|
||||
intro: [],
|
||||
app: null,
|
||||
vscode: null,
|
||||
file,
|
||||
};
|
||||
if (release.version !== null && !VERSION_PATTERN.test(release.version)) fail(file, 1, `version ${JSON.stringify(release.version)} is not x.y.z`);
|
||||
if (release.date !== null && !DATE_PATTERN.test(release.date)) fail(file, 1, `date ${JSON.stringify(release.date)} is not YYYY-MM-DD`);
|
||||
|
||||
let surface = null; // 'app' | 'vscode'
|
||||
let group = null;
|
||||
for (let index = bodyStart; index < lines.length; index += 1) {
|
||||
const line = lines[index];
|
||||
const number = index + 1;
|
||||
if (line.startsWith('## ')) {
|
||||
const name = line.slice(3).trim();
|
||||
const position = SURFACES.indexOf(name);
|
||||
if (position < 0) fail(file, number, `unknown section ${JSON.stringify(name)}; expected one of ${SURFACES.join(', ')}`);
|
||||
surface = position === 0 ? 'app' : 'vscode';
|
||||
if (release[surface]) fail(file, number, `section ${name} appears twice`);
|
||||
release[surface] = {};
|
||||
group = null;
|
||||
continue;
|
||||
}
|
||||
if (line.startsWith('### ')) {
|
||||
const name = line.slice(4).trim();
|
||||
if (!surface) fail(file, number, `group ${JSON.stringify(name)} appears before any ## App or ## VS Code section`);
|
||||
if (!GROUPS.includes(name)) fail(file, number, `unknown group ${JSON.stringify(name)}; expected one of ${GROUPS.join(', ')}`);
|
||||
if (release[surface][name]) fail(file, number, `group ${name} appears twice in ${surface === 'app' ? 'App' : 'VS Code'}`);
|
||||
release[surface][name] = [];
|
||||
group = name;
|
||||
continue;
|
||||
}
|
||||
if (line.startsWith('- ') || line.startsWith('* ')) {
|
||||
if (!surface || !group) fail(file, number, 'a bullet must sit under a ### group inside ## App or ## VS Code');
|
||||
const bullet = line.slice(2).trim();
|
||||
if (!bullet) fail(file, number, 'empty bullet');
|
||||
release[surface][group].push(bullet);
|
||||
continue;
|
||||
}
|
||||
if (!line.trim()) continue;
|
||||
if (surface === null) {
|
||||
release.intro.push(line.trimEnd());
|
||||
continue;
|
||||
}
|
||||
fail(file, number, `unexpected text inside a section; only "- " bullets belong under a group: ${JSON.stringify(line)}`);
|
||||
}
|
||||
return release;
|
||||
};
|
||||
|
||||
const compareVersionsDesc = (a, b) => {
|
||||
const pa = a.split('.').map(Number);
|
||||
const pb = b.split('.').map(Number);
|
||||
for (let index = 0; index < 3; index += 1) {
|
||||
if (pa[index] !== pb[index]) return pb[index] - pa[index];
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
/** Read `changelog/`: every `x.y.z.md` plus `unreleased.md`, newest first. */
|
||||
export const loadReleases = (directory) => {
|
||||
const releases = [];
|
||||
let unreleased = null;
|
||||
for (const name of fs.readdirSync(directory)) {
|
||||
if (!name.endsWith('.md') || name === 'README.md') continue;
|
||||
const file = path.join(directory, name);
|
||||
const release = parseRelease(fs.readFileSync(file, 'utf8'), path.relative(process.cwd(), file));
|
||||
if (name === 'unreleased.md') {
|
||||
if (release.version || release.date) fail(release.file, 1, 'unreleased.md carries no version or date');
|
||||
unreleased = release;
|
||||
continue;
|
||||
}
|
||||
const stem = name.slice(0, -3);
|
||||
if (!release.version || !release.date) fail(release.file, 1, 'a release file needs version and date in its front matter');
|
||||
if (release.version !== stem) fail(release.file, 1, `version ${release.version} does not match the file name ${stem}`);
|
||||
releases.push(release);
|
||||
}
|
||||
releases.sort((a, b) => compareVersionsDesc(a.version, b.version));
|
||||
const seen = new Set();
|
||||
for (const release of releases) {
|
||||
if (seen.has(release.version)) fail(release.file, 1, `version ${release.version} appears twice`);
|
||||
seen.add(release.version);
|
||||
}
|
||||
return { unreleased, releases };
|
||||
};
|
||||
|
||||
const renderGroups = (groups) => {
|
||||
const blocks = [];
|
||||
for (const name of GROUPS) {
|
||||
const bullets = groups?.[name];
|
||||
if (!bullets || bullets.length === 0) continue;
|
||||
blocks.push(`### ${name}\n\n${bullets.map((bullet) => `- ${bullet}`).join('\n')}\n`);
|
||||
}
|
||||
return blocks.join('\n');
|
||||
};
|
||||
|
||||
const renderHeader = (release) => (release.version ? `## [${release.version}] - ${release.date}` : '## [Unreleased]');
|
||||
|
||||
const renderSection = (release, groups, intro) => {
|
||||
const parts = [renderHeader(release), ''];
|
||||
if (intro.length > 0) parts.push(intro.join('\n'), '');
|
||||
const body = renderGroups(groups);
|
||||
if (body) parts.push(body);
|
||||
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')}`;
|
||||
};
|
||||
|
||||
/** `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')}`;
|
||||
};
|
||||
|
||||
const groupsToJson = (groups) => {
|
||||
if (!groups) return null;
|
||||
const out = {};
|
||||
for (const name of GROUPS) out[name.toLowerCase()] = groups[name] ?? [];
|
||||
return out;
|
||||
};
|
||||
|
||||
/** `changelog/index.json`: released versions only, newest first. */
|
||||
export const renderIndex = ({ releases }) => `${JSON.stringify(releases.map((release) => ({
|
||||
version: release.version,
|
||||
date: release.date,
|
||||
title: release.title,
|
||||
intro: release.intro.join('\n') || null,
|
||||
app: groupsToJson(release.app),
|
||||
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),
|
||||
});
|
||||
|
||||
export const UNRELEASED_TEMPLATE = `## App
|
||||
|
||||
## VS Code
|
||||
`;
|
||||
|
||||
/**
|
||||
* Turn `unreleased.md` into `<version>.md` dated `date`, and reset
|
||||
* `unreleased.md` to the empty template. Refuses an empty unreleased file:
|
||||
* a release with nothing to say is a mistake, not a release.
|
||||
*/
|
||||
export const promoteUnreleased = (directory, version, date) => {
|
||||
if (!VERSION_PATTERN.test(version)) throw new Error(`version ${JSON.stringify(version)} is not x.y.z`);
|
||||
if (!DATE_PATTERN.test(date)) throw new Error(`date ${JSON.stringify(date)} is not YYYY-MM-DD`);
|
||||
const source = path.join(directory, 'unreleased.md');
|
||||
const target = path.join(directory, `${version}.md`);
|
||||
if (fs.existsSync(target)) throw new Error(`${path.relative(process.cwd(), target)} already exists`);
|
||||
const text = fs.readFileSync(source, 'utf8');
|
||||
const release = parseRelease(text, path.relative(process.cwd(), source));
|
||||
const bullets = [...Object.values(release.app ?? {}), ...Object.values(release.vscode ?? {})].flat();
|
||||
if (bullets.length === 0) throw new Error('changelog/unreleased.md has no bullets; write the release notes before releasing');
|
||||
const body = text.replace(/^---\n[\s\S]*?\n---\n/, '');
|
||||
fs.writeFileSync(target, `---\nversion: ${version}\ndate: ${date}\n---\n\n${body.replace(/^\n+/, '')}`);
|
||||
fs.writeFileSync(source, UNRELEASED_TEMPLATE);
|
||||
return target;
|
||||
};
|
||||
@@ -0,0 +1,125 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
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';
|
||||
|
||||
const banner = '<!-- Generated from changelog/*.md by `bun run changelog:build`. Edit those files, not this one. -->';
|
||||
|
||||
const release = `---
|
||||
version: 1.2.3
|
||||
date: 2026-01-31
|
||||
title: Comments everywhere
|
||||
---
|
||||
|
||||
A short intro.
|
||||
|
||||
## App
|
||||
|
||||
### Fixes
|
||||
- Chat: huge patches open without freezing the page (thanks to @someone).
|
||||
|
||||
### New
|
||||
- **Comments:** select text and comment on it.
|
||||
|
||||
## VS Code
|
||||
|
||||
### New
|
||||
- Comments on code.
|
||||
`;
|
||||
|
||||
test('parses front matter, intro, sections, and groups', () => {
|
||||
const parsed = parseRelease(release, 'changelog/1.2.3.md');
|
||||
assert.equal(parsed.version, '1.2.3');
|
||||
assert.equal(parsed.date, '2026-01-31');
|
||||
assert.equal(parsed.title, 'Comments everywhere');
|
||||
assert.deepEqual(parsed.intro, ['A short intro.']);
|
||||
assert.deepEqual(parsed.app, {
|
||||
Fixes: ['Chat: huge patches open without freezing the page (thanks to @someone).'],
|
||||
New: ['**Comments:** select text and comment on it.'],
|
||||
});
|
||||
assert.deepEqual(parsed.vscode, { New: ['Comments on code.'] });
|
||||
});
|
||||
|
||||
test('rejects shapes the generator cannot render', () => {
|
||||
assert.throws(() => parseRelease('## App\n### Nope\n- x\n', 'f.md'), /unknown group "Nope"/);
|
||||
assert.throws(() => parseRelease('## Desktop\n', 'f.md'), /unknown section "Desktop"/);
|
||||
assert.throws(() => parseRelease('- orphan\n', 'f.md'), /must sit under a ### group/);
|
||||
assert.throws(() => parseRelease('## App\n### New\nstray text\n', 'f.md'), /unexpected text inside a section/);
|
||||
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', () => {
|
||||
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\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');
|
||||
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
|
||||
|
||||
### Improvements
|
||||
|
||||
- Faster.
|
||||
|
||||
## [1.2.3] - 2026-01-31
|
||||
|
||||
A short intro.
|
||||
|
||||
### New
|
||||
|
||||
- **Comments:** select text and comment on it.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Chat: huge patches open without freezing the page (thanks to @someone).
|
||||
`);
|
||||
|
||||
assert.equal(renderVsCodeChangelog(loaded), `${banner}
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.2.3] - 2026-01-31
|
||||
|
||||
### New
|
||||
|
||||
- Comments on code.
|
||||
`);
|
||||
|
||||
const index = JSON.parse(renderIndex(loaded));
|
||||
assert.equal(index.length, 2);
|
||||
assert.equal(index[0].version, '1.2.4');
|
||||
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);
|
||||
});
|
||||
|
||||
test('loadReleases refuses a file whose name and version disagree', () => {
|
||||
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'changelog-'));
|
||||
fs.writeFileSync(path.join(directory, '9.9.9.md'), release);
|
||||
assert.throws(() => loadReleases(directory), /does not match the file name 9\.9\.9/);
|
||||
});
|
||||
|
||||
test('promoteUnreleased dates the release, resets the template, and refuses an empty release', () => {
|
||||
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'changelog-'));
|
||||
fs.writeFileSync(path.join(directory, 'unreleased.md'), '## App\n\n### New\n- Something shipped.\n\n## VS Code\n');
|
||||
const created = promoteUnreleased(directory, '2.0.0', '2026-03-01');
|
||||
assert.equal(path.basename(created), '2.0.0.md');
|
||||
assert.match(fs.readFileSync(created, 'utf8'), /^---\nversion: 2\.0\.0\ndate: 2026-03-01\n---\n\n## App/);
|
||||
assert.equal(fs.readFileSync(path.join(directory, 'unreleased.md'), 'utf8'), '## App\n\n## VS Code\n');
|
||||
assert.throws(() => promoteUnreleased(directory, '2.0.1', '2026-03-02'), /has no bullets/);
|
||||
});
|
||||
+5
-15
@@ -174,20 +174,8 @@ function step(label, fn) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const RELEASE_CHANGELOG_FILES = ['CHANGELOG.md', 'packages/vscode/CHANGELOG.md'];
|
||||
|
||||
// Same check the release workflow runs before it publishes, so a missing
|
||||
// section fails here in seconds instead of after the tag is pushed.
|
||||
function assertChangelogSection(version) {
|
||||
const changelogPath = path.join(repoRoot, 'CHANGELOG.md');
|
||||
if (!existsSync(changelogPath)) {
|
||||
throw new Error('CHANGELOG.md not found; add it before releasing.');
|
||||
}
|
||||
const sections = readFileSync(changelogPath, 'utf8').split(/^## /m);
|
||||
if (!sections.some((section) => section.startsWith(`[${version}]`))) {
|
||||
throw new Error(`CHANGELOG.md has no "## [${version}] - YYYY-MM-DD" section. Add it before releasing.`);
|
||||
}
|
||||
}
|
||||
// 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'];
|
||||
|
||||
function printReleaseNextSteps(version) {
|
||||
log.success(`Release v${version} prepared locally`);
|
||||
@@ -613,7 +601,9 @@ 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');
|
||||
step('Checking changelog', () => assertChangelogSection(version));
|
||||
// Turns changelog/unreleased.md into changelog/<version>.md dated today and
|
||||
// regenerates CHANGELOG.md; 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]));
|
||||
printReleaseNextSteps(version);
|
||||
|
||||
Reference in New Issue
Block a user