Compare commits

...

9 Commits

10 changed files with 164 additions and 45 deletions

View File

@ -2,21 +2,20 @@ project:
name: changelord name: changelord
homepage: https://git.babyl.ca/yanick/changelord.js homepage: https://git.babyl.ca/yanick/changelord.js
with_stats: true with_stats: true
next_directory: ./changelog-next
releases: releases:
- version: NEXT - version: NEXT
changes: changes:
- add `git-gather` command
- type: feat - type: feat
desc: " add 'next' to alias to 'upcoming'" desc: add `git-gather` command
commit: 363c195477231a6b3b770e74ebfe316296ec5af2 - type: feat
desc: add 'next' to alias to 'upcoming'
- type: feat - type: feat
desc: cutting a release also add a new NEXT release desc: cutting a release also add a new NEXT release
commit: 167f631d1fe4eadba3ed5fdadbe378b8255d4ad2
- type: feat - type: feat
desc: git-gather also filters on descs desc: git-gather also filters on descs
- type: feat - type: feat
desc: add the validate command desc: add the validate command
commit: 5ba75a8e3d42f633e38c3584898ef0085c48fb04
- version: 0.1.0 - version: 0.1.0
changes: changes:
- port the core of the Perl changelord to JavaScript. - port the core of the Perl changelord to JavaScript.

View File

@ -11,6 +11,14 @@ read its [introductory article][blog] on my blog.
pnpm install changelord pnpm install changelord
## `changelog-next` directory
If you want to mininize merge conflicts in `CHANGELOG.yml`,
you can set the option `project.next_directory` to a directory (typically
`./changelog-next`) that will hold yaml files containing the
changes for the NEXT release. Each of those files is expected to
have a list of changes.
## CLI commands ## CLI commands
### Global options ### Global options
@ -41,6 +49,9 @@ sections.
Adds an entry to the `NEXT` release. Adds an entry to the `NEXT` release.
If `project.next_directory` is defined, the entry will be added to that
directory instead of directly into `CHANGELOG.yml`.
$ changelord add --type=maint added a changelog to the project. $ changelord add --type=maint added a changelog to the project.
#### Options #### Options
@ -62,6 +73,10 @@ Cuts the next release. That is, resolves the `NEXT` version number based on the
latest version and the changes in the `NEXT` section, and sets its date as latest version and the changes in the `NEXT` section, and sets its date as
today. Modifies the source file with the result. today. Modifies the source file with the result.
If the `project.next_directory` option is present,
all the changes in that directory are
merged to `CHANGELOG.yml` and the files themselves are deleted.
#### Options #### Options
- `--dry` -- Resolves the next version but only outputs the resulting section - `--dry` -- Resolves the next version but only outputs the resulting section

View File

@ -0,0 +1,2 @@
- desc: support changelog-next directory
type: feat

View File

@ -24,11 +24,15 @@
"author": "Yanick Champoux <yanick@babyl.ca> (http://techblog.babyl.ca/)", "author": "Yanick Champoux <yanick@babyl.ca> (http://techblog.babyl.ca/)",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@sindresorhus/slugify": "^2.2.1",
"@yanick/updeep-remeda": "^2.2.0", "@yanick/updeep-remeda": "^2.2.0",
"ajv": "^8.12.0", "ajv": "^8.12.0",
"consola": "^3.1.0", "consola": "^3.1.0",
"filenamify": "^6.0.0",
"fs-extra": "^11.1.1", "fs-extra": "^11.1.1",
"globby": "^13.1.4",
"markdown-utils": "^1.0.0", "markdown-utils": "^1.0.0",
"nanoid": "^4.0.2",
"remeda": "^1.14.0", "remeda": "^1.14.0",
"semver": "^7.5.0", "semver": "^7.5.0",
"simple-git": "^3.18.0", "simple-git": "^3.18.0",

View File

@ -50,6 +50,11 @@ export default {
with_stats: { with_stats: {
description: "if true, add git statistics when bumping the version.", description: "if true, add git statistics when bumping the version.",
}, },
next_directory: {
type: "string",
description:
"directory where the changes for the NEXT release are stashed",
},
}, },
type: "object", type: "object",
}, },

View File

@ -10,6 +10,8 @@ import u from "@yanick/updeep-remeda";
import { once } from "remeda"; import { once } from "remeda";
import simpleGit from "simple-git"; import simpleGit from "simple-git";
import Ajv from "ajv"; import Ajv from "ajv";
import { nanoid } from "nanoid";
import slugify from "@sindresorhus/slugify";
import print from "./command/print.js"; import print from "./command/print.js";
import init from "./command/init.js"; import init from "./command/init.js";
@ -21,6 +23,8 @@ import latest, { latest_version } from "./command/latest-version.js";
import validate from "./command/validate.js"; import validate from "./command/validate.js";
import git_gather from "./command/git-gather.js"; import git_gather from "./command/git-gather.js";
import schemaV1 from "./changelog-schema.js"; import schemaV1 from "./changelog-schema.js";
import { globby } from "globby";
import { flatMap } from "remeda";
consola.raw = (...args) => console.log(...args); consola.raw = (...args) => console.log(...args);
@ -32,6 +36,27 @@ yargs(hideBin(process.argv))
fs fs
.readFile(config.source, "utf-8") .readFile(config.source, "utf-8")
.then(yaml.parse) .then(yaml.parse)
.then(async (doc) => {
if (!doc.project.next_directory) return doc;
const changes = await globby([
doc.project.next_directory + "/*.yml",
doc.project.next_directory + "/*.yaml",
])
.then((files) =>
Promise.all(
files.map((f) => fs.readFile(f, "utf-8").then(yaml.parse))
)
)
.then((r) => r.flat());
if (changes.length)
doc.releases
.find((r) => r.version === "NEXT")
.changes.push(...changes);
return doc;
})
.then((doc) => { .then((doc) => {
const ajv = new Ajv(); const ajv = new Ajv();
const validate = ajv.compile(schemaV1); const validate = ajv.compile(schemaV1);
@ -42,6 +67,13 @@ yargs(hideBin(process.argv))
throw "changelog is invalid"; throw "changelog is invalid";
}) })
); );
config.delete_next_dir_entries = async () => {
const changelog = await config.changelog();
return globby([
changelog.project.next_directory + "/*.yml",
changelog.project.next_directory + "/*.yaml",
]).then((files) => Promise.all(files.map((f) => fs.remove(f))));
};
config.save_changelog = async (changelog) => { config.save_changelog = async (changelog) => {
if (!changelog) changelog = await config.changelog(); if (!changelog) changelog = await config.changelog();
return fs.writeFile(config.source, yaml.stringify(changelog)); return fs.writeFile(config.source, yaml.stringify(changelog));
@ -54,12 +86,39 @@ yargs(hideBin(process.argv))
argv.yargs = yargs; argv.yargs = yargs;
argv.add_to_next = async (entry) => { argv.add_to_next = async (entry) => {
const changelog = yaml.parse(await fs.readFile(argv.source, "utf-8")); const dir = await argv.changelog().then((c) => c.project.next_directory);
if (dir) {
await fs.ensureDir(dir);
const filename = join(
dir,
[
new Date().toISOString(),
entry.ticket,
entry.feat,
slugify(entry.desc, {
separator: "_",
}).slice(0, 10),
]
.filter((x) => x)
.join("-") + ".yml"
);
argv.consola.info(`writing change to ${filename}`);
if (fs.existsSync(filename)) {
argv.consola.error(`file ${filename} already exist`);
yargs.exit(1);
}
if (Object.keys(entry).length === 1) {
entry = entry.desc;
}
return fs.writeFile(filename, yaml.stringify([entry]));
}
const changelog = await argv.changelog();
let next = changelog.releases.find(u.matches({ version: "NEXT" })); let next = changelog.releases.find(u.matches({ version: "NEXT" }));
if (!next) { if (!next) {
next = { version: "NEXT", changes: [] }; changelog.releases.unshift(base_next_version);
changelog.releases.unshift(next);
} }
if (Object.keys(entry).length === 1) { if (Object.keys(entry).length === 1) {
@ -68,7 +127,7 @@ yargs(hideBin(process.argv))
next.changes.push(entry); next.changes.push(entry);
return fs.writeFile(argv.source, yaml.stringify(changelog)); return argv.save_changelog();
}; };
}) })
.default("source", join(process.cwd(), "CHANGELOG.yml")) .default("source", join(process.cwd(), "CHANGELOG.yml"))

View File

@ -3,32 +3,32 @@ import yaml from "yaml";
import u from "@yanick/updeep-remeda"; import u from "@yanick/updeep-remeda";
const handler = async (config) => { const handler = async (config) => {
if (!config.change) if (!config.change)
throw new Error("can't add a change without a description"); throw new Error("can't add a change without a description");
const entry = { const entry = {
desc: config.change.join(" "), desc: config.change.join(" "),
}; };
if (config.ticket) entry.ticket = config.ticket; if (config.ticket) entry.ticket = config.ticket;
if (config.type) entry.type = config.type; if (config.type) entry.type = config.type;
config.consola.start(`adding '${entry.desc}' to the changelog`); config.consola.start(`adding '${entry.desc}' to the changelog`);
config.add_to_next(entry); await config.add_to_next(entry);
config.consola.success("done!"); config.consola.success("done!");
}; };
export default { export default {
command: "add [change...]", command: "add [change...]",
desc: "add a change to the NEXT release", desc: "add a change to the NEXT release",
builder: (yargs) => { builder: (yargs) => {
yargs yargs
.string("ticket") .string("ticket")
.describe("ticket", "ticket associated with the change") .describe("ticket", "ticket associated with the change")
.string("type") .string("type")
.describe("type", "type of change"); .describe("type", "type of change");
}, },
handler, handler,
}; };

View File

@ -62,6 +62,12 @@ const handler = async (config) => {
config.consola.info("running in dry mode, not saving\n", next); config.consola.info("running in dry mode, not saving\n", next);
} else { } else {
await config.save_changelog(changelog); await config.save_changelog(changelog);
if (changelog.project?.next_directory) {
config.consola.info(
`removing files in ${changelog.project.next_directory}`
);
await config.delete_next_dir_entries();
}
} }
config.consola.success(`version ${next.version} is cut!`); config.consola.success(`version ${next.version} is cut!`);

View File

@ -4,28 +4,28 @@ import yaml from "yaml";
import { render_release } from "./print.js"; import { render_release } from "./print.js";
export async function next_release() { export async function next_release() {
const source = await fs.readFile(this.source, "utf-8").then(yaml.parse); const source = await this.changelog();
return ( return (
source.releases.find(u.matches({ version: "NEXT" })) ?? { source.releases.find(u.matches({ version: "NEXT" })) ?? {
version: "NEXT", version: "NEXT",
changes: [], changes: [],
} }
); );
} }
const handler = async (config) => { const handler = async (config) => {
const source = await fs.readFile(config.source, "utf-8").then(yaml.parse); const source = await config.changelog();
const res = render_release( const { body } = render_release(
{ ...config, next: true }, { ...config, next: true },
source source
)(source.releases.find(u.matches({ version: "NEXT" }))); )(await config.next_release());
config.consola.raw("\n" + res.body); config.consola.raw("\n" + body);
}; };
export default { export default {
command: "upcoming", command: "upcoming",
desc: "output the changes in NEXT", desc: "output the changes in NEXT",
handler, handler,
}; };

View File

@ -0,0 +1,29 @@
import { test, expect, vi } from "vitest";
import upcoming from "./upcoming.js";
test("basic", async () => {
const changelog = {
releases: [{ version: "NEXT", changes: [] }, { version: "1.0.0" }],
change_types: [],
};
const noop = () => {};
const config = {
consola: {
start: noop,
raw: vi.fn(),
},
changelog: () => changelog,
next_release: () => ({
changes: [],
}),
latest_version: () => ({ version: "1.2.3" }),
git: () => ({
log: () => ({ all: [] }),
}),
};
await upcoming.handler(config);
expect(config.consola.raw).toHaveBeenCalled();
});