diff --git a/CHANGELOG.yml b/CHANGELOG.yml index e6ad922..57761b8 100644 --- a/CHANGELOG.yml +++ b/CHANGELOG.yml @@ -2,7 +2,6 @@ project: name: changelord homepage: https://git.babyl.ca/yanick/changelord.js with_stats: true - ticket_url: null releases: - version: NEXT changes: diff --git a/README.md b/README.md index 049859e..5d6037a 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,10 @@ Outputs the latest non-NEXT release. $ changelord latest-version 3.2.0 +### `changelord validate` + +Validates the changelog source against its json schema. + ### `changelord git-gather` Gathers change entries from git commits. If any are found, they are diff --git a/package.json b/package.json index 0bf0df3..7f21b17 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "license": "ISC", "dependencies": { "@yanick/updeep-remeda": "^2.2.0", + "ajv": "^8.12.0", "consola": "^3.1.0", "fs-extra": "^11.1.1", "markdown-utils": "^1.0.0", diff --git a/src/changelog-schema.js b/src/changelog-schema.js new file mode 100644 index 0000000..041b4e9 --- /dev/null +++ b/src/changelog-schema.js @@ -0,0 +1,113 @@ +export default { + type: "object", + additionalProperties: false, + properties: { + change_types: { + items: { + additionalProperties: false, + properties: { + keywords: { + items: { + type: "string", + }, + type: "array", + }, + level: { + enum: ["major", "minor", "patch"], + }, + title: { + type: "string", + }, + }, + type: "object", + }, + type: "array", + }, + project: { + additionalProperties: false, + properties: { + homepage: { + description: "url of the project's homepage", + examples: ["https://github.com/yanick/app-changelord"], + type: ["string", "null"], + }, + name: { + description: "name of the project", + examples: ["App::Changelord"], + type: ["null", "string"], + }, + ticket_url: { + description: + "perl code that takes a ticket string (e.g. 'GH123') via the `$_` variable and turns it into a link.", + examples: [ + "s!GH(\\d+)!https://github.com/yanick/App-Changelord/issue/$1/", + { + '/^\\d+$/ ? "https://.../$_"': "undef", + }, + ], + type: "string", + }, + with_stats: { + description: "if true, add git statistics when bumping the version.", + }, + }, + type: "object", + }, + releases: { + items: { + oneOf: [ + { + type: "string", + }, + { + additionalProperties: false, + properties: { + changes: { + items: { + $ref: "#/$defs/change", + }, + type: "array", + }, + date: { + type: ["null", "string"], + }, + version: { + type: ["null", "string"], + }, + }, + type: "object", + }, + ], + }, + type: "array", + }, + }, + $defs: { + change: { + oneOf: [ + { + type: "string", + }, + { + additionalProperties: false, + properties: { + commit: { + type: ["string", "null"], + }, + desc: { + type: "string", + }, + ticket: { + type: ["string", "null"], + }, + type: { + type: ["string", "null"], + }, + }, + required: ["desc"], + type: "object", + }, + ], + }, + }, +}; diff --git a/src/changelog-schema.yml b/src/changelog-schema.yml deleted file mode 100644 index 409587d..0000000 --- a/src/changelog-schema.yml +++ /dev/null @@ -1,59 +0,0 @@ -type: object -additionalProperties: false -properties: - project: - type: object - additionalProperties: false - properties: - homepage: - type: [ string, 'null' ] - description: url of the project's homepage - examples: - - https://github.com/yanick/app-changelord - name: - type: [ 'null', string ] - description: name of the project - examples: - - App::Changelord - ticket_url: - type: string - description: perl code that takes a ticket string (e.g. 'GH123') via the `$_` variable and turns it into a link. - examples: - - s!GH(\d+)!https://github.com/yanick/App-Changelord/issue/$1/ - - /^\d+$/ ? "https://.../$_" : undef - with_stats: - description: if true, add git statistics when bumping the version. - change_types: - type: array - items: - type: object - additionalProperties: false - properties: - keywords: - type: array - items: { type: string } - level: { enum: [ major, minor, patch ] } - title: { type: string } - releases: - type: array - items: - oneOf: - - type: string - - type: object - additionalProperties: false - properties: - version: { type: [ 'null', string ] } - date: { type: ['null',string] } - changes: { type: 'array', items: { $ref: '#/$defs/change' } } -$defs: - change: - oneOf: - - type: string - - type: object - required: [ desc ] - additionalProperties: false - properties: - desc: { type: string } - ticket: { type: [ string, 'null' ] } - type: { type: [ string, 'null' ] } - commit: { type: [ string, 'null' ] } diff --git a/src/changelord.js b/src/changelord.js index 395af53..985addf 100755 --- a/src/changelord.js +++ b/src/changelord.js @@ -9,6 +9,7 @@ import consola from "consola"; import u from "@yanick/updeep-remeda"; import { once } from "remeda"; import simpleGit from "simple-git"; +import Ajv from "ajv"; import print from "./command/print.js"; import init from "./command/init.js"; @@ -17,7 +18,9 @@ import add from "./command/add.js"; import cut from "./command/cut.js"; import upcoming, { next_release } from "./command/upcoming.js"; import latest, { latest_version } from "./command/latest-version.js"; +import validate from "./command/validate.js"; import git_gather from "./command/git-gather.js"; +import schemaV1 from "./changelog-schema.js"; consola.raw = (...args) => console.log(...args); @@ -26,7 +29,18 @@ yargs(hideBin(process.argv)) config.git = once(simpleGit); config.consola = consola; config.changelog = once(() => - fs.readFile(config.source, "utf-8").then(yaml.parse) + fs + .readFile(config.source, "utf-8") + .then(yaml.parse) + .then((doc) => { + const ajv = new Ajv(); + const validate = ajv.compile(schemaV1); + const valid = validate(doc); + if (valid) return doc; + + config.consola.error("invalid changelog: ", validate.errors); + throw "changelog is invalid"; + }) ); config.save_changelog = async (changelog) => { if (!changelog) changelog = await config.changelog(); @@ -36,7 +50,9 @@ yargs(hideBin(process.argv)) config.latest_version = latest_version.bind(config); return config; }) - .middleware((argv) => { + .middleware((argv, yargs) => { + argv.yargs = yargs; + argv.add_to_next = async (entry) => { const changelog = yaml.parse(await fs.readFile(argv.source, "utf-8")); @@ -70,6 +86,7 @@ yargs(hideBin(process.argv)) }) .command(latest) .command(git_gather) + .command(validate) .strictCommands() .help() .parse(); diff --git a/src/command/init.js b/src/command/init.js index c417937..92c0af4 100644 --- a/src/command/init.js +++ b/src/command/init.js @@ -19,7 +19,6 @@ export const base_changelog = { name: null, homepage: null, with_stats: true, - ticket_url: null, }, releases: [{ version: "NEXT", changes: [] }], change_types, diff --git a/src/command/print.js b/src/command/print.js index 69afd08..74be42b 100644 --- a/src/command/print.js +++ b/src/command/print.js @@ -103,7 +103,7 @@ export const render_release = (config, source) => (release) => { }; const handler = async (config) => { - const source = await fs.readFile(config.source, "utf-8").then(yaml.parse); + const source = await config.changelog(); let output = ""; diff --git a/src/command/schema.js b/src/command/schema.js index e64225a..956c207 100644 --- a/src/command/schema.js +++ b/src/command/schema.js @@ -1,12 +1,9 @@ import fs from "fs-extra"; +import yaml from "yaml"; +import schemaV1 from "../changelog-schema.js"; -const handler = async ({consola}) => { - consola.raw( - await fs.readFile( - new URL("../changelog-schema.yml", import.meta.url), - "utf-8" - ) - ); +const handler = async ({ consola }) => { + consola.raw(yaml.stringify(schemaV1)); }; export default { diff --git a/src/command/validate.js b/src/command/validate.js new file mode 100644 index 0000000..dfdf04e --- /dev/null +++ b/src/command/validate.js @@ -0,0 +1,16 @@ +const handler = async (config) => { + config.consola.start("validating changelog..."); + + try { + await config.changelog(); + config.consola.success("valid!"); + } catch (e) { + config.yargs.exit(1, "changelog is invalid :-("); + } +}; + +export default { + command: "validate", + desc: "validate the changelog against its json schema", + handler, +}; diff --git a/src/command/validate.test.js b/src/command/validate.test.js new file mode 100644 index 0000000..9b926c5 --- /dev/null +++ b/src/command/validate.test.js @@ -0,0 +1,16 @@ +import { test, expect, vi } from "vitest"; +import validate from "./validate.js"; + +test("basic", async () => { + const success = vi.fn(); + const noop = () => true; + await validate.handler({ + changelog: () => ({}), + consola: { + start: noop, + success, + }, + }); + + expect(success).toHaveBeenCalled(); +});