Compare commits
41 Commits
lord7-sche
...
main
Author | SHA1 | Date | |
---|---|---|---|
e69858f55e | |||
bc66ea09a2 | |||
04e9e5f71a | |||
f6141b139e | |||
9b9c15ecc4 | |||
a841165c51 | |||
251dce5b56 | |||
f759989698 | |||
09d60859e1 | |||
8faa2ef1d8 | |||
3f258a6e23 | |||
1c7b6158f4 | |||
031f2cb825 | |||
5ba75a8e3d | |||
2e9d0d4b66 | |||
bef10869ce | |||
d826b4191d | |||
b10b599585 | |||
167f631d1f | |||
e7f70f3aec | |||
363c195477 | |||
0d34b92358 | |||
d9f4244e3e | |||
467251bab6 | |||
1b25881ede | |||
063ee5d02b | |||
7d8cccf7f8 | |||
edfaf81b14 | |||
c8fb899d0d | |||
6cbfd2ed2b | |||
4f84450979 | |||
0d1fca8dbb | |||
13f443b61c | |||
385c09896a | |||
36e2aade92 | |||
3f266fa068 | |||
a326be7211 | |||
8f8a16d025 | |||
0ed4ca9cab | |||
01e5381b77 | |||
7336efacbb |
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,2 +1,3 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
pnpm-lock.yaml
|
pnpm-lock.yaml
|
||||||
|
*.tgz
|
||||||
|
6
.npmignore
Normal file
6
.npmignore
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
_templates
|
||||||
|
Taskfile.yaml
|
||||||
|
*.tgz
|
||||||
|
prettier.config.*
|
||||||
|
changelog-next/
|
||||||
|
*.test.js
|
37
CHANGELOG.md
Normal file
37
CHANGELOG.md
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
# Changelog for [changelord][homepage]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 0.2.0 2023-05-18
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add `git-gather` command
|
||||||
|
* add 'next' to alias to 'upcoming'
|
||||||
|
* cutting a release also add a new NEXT release
|
||||||
|
* git-gather also filters on descs
|
||||||
|
* add the validate command
|
||||||
|
* support changelog-next directory
|
||||||
|
|
||||||
|
### Statistics
|
||||||
|
|
||||||
|
* code churn: 21 files changed, 563 insertions(+), 153 deletions(-)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 0.1.0 2023-05-16
|
||||||
|
|
||||||
|
* port the core of the Perl changelord to JavaScript.
|
||||||
|
|
||||||
|
### Statistics
|
||||||
|
|
||||||
|
* code churn: 23 files changed, 767 insertions(+)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[homepage]: https://git.babyl.ca/yanick/changelord.js
|
@ -1,12 +1,41 @@
|
|||||||
project:
|
project:
|
||||||
name: null
|
name: changelord
|
||||||
homepage: null
|
homepage: https://git.babyl.ca/yanick/changelord.js
|
||||||
with_stats: true
|
with_stats: true
|
||||||
ticket_url: null
|
next_directory: ./changelog-next
|
||||||
releases:
|
releases:
|
||||||
- version: NEXT
|
- version: NEXT
|
||||||
changes: []
|
changes: []
|
||||||
|
- version: 0.2.0
|
||||||
|
changes:
|
||||||
|
- type: feat
|
||||||
|
desc: add `git-gather` command
|
||||||
|
- type: feat
|
||||||
|
desc: add 'next' to alias to 'upcoming'
|
||||||
|
- type: feat
|
||||||
|
desc: cutting a release also add a new NEXT release
|
||||||
|
- type: feat
|
||||||
|
desc: git-gather also filters on descs
|
||||||
|
- type: feat
|
||||||
|
desc: add the validate command
|
||||||
|
- desc: support changelog-next directory
|
||||||
|
type: feat
|
||||||
|
- type: stats
|
||||||
|
desc: |
|
||||||
|
code churn: 21 files changed, 563 insertions(+), 153 deletions(-)
|
||||||
|
date: 2023-05-18
|
||||||
|
- version: 0.1.0
|
||||||
|
changes:
|
||||||
|
- port the core of the Perl changelord to JavaScript.
|
||||||
|
- type: stats
|
||||||
|
desc: |
|
||||||
|
code churn: 23 files changed, 767 insertions(+)
|
||||||
|
date: 2023-05-16
|
||||||
change_types:
|
change_types:
|
||||||
|
- title: ""
|
||||||
|
level: minor
|
||||||
|
keywords:
|
||||||
|
- ""
|
||||||
- title: Features
|
- title: Features
|
||||||
level: minor
|
level: minor
|
||||||
keywords:
|
keywords:
|
||||||
|
130
README.md
Normal file
130
README.md
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
# Changelord, registrar of deeds extraordinaire
|
||||||
|
|
||||||
|
Changelord is a changelog manager scratching my particular itches.
|
||||||
|
It's cli-based, and keep its data in a YAML file adhering
|
||||||
|
to a well-defined schema.
|
||||||
|
|
||||||
|
The first iteration of `changelord` was written [in Perl][original]. You can
|
||||||
|
read its [introductory article][blog] on my blog.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
### Global options
|
||||||
|
|
||||||
|
#### `--help`
|
||||||
|
|
||||||
|
Outputs the list of commands and options.
|
||||||
|
|
||||||
|
#### `--version`
|
||||||
|
|
||||||
|
Outputs the `changelord` version.
|
||||||
|
|
||||||
|
#### `--source`
|
||||||
|
|
||||||
|
Specifies which source yaml file to use. Defaults to the `CHANGELOG.yml` file
|
||||||
|
in the current directory.
|
||||||
|
|
||||||
|
### `changelord init`
|
||||||
|
|
||||||
|
Initializes the changelog source file. The YAML file is made of three
|
||||||
|
sections.
|
||||||
|
|
||||||
|
- `project` -- contains information and configuration about the project itself.
|
||||||
|
- `releases` -- the entries for the changelog proper.
|
||||||
|
- `change_types` -- defines all types of changes this project supports.
|
||||||
|
|
||||||
|
### `changelord add`
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
#### Options
|
||||||
|
|
||||||
|
- `--type` -- type of change.
|
||||||
|
- `--ticket` -- associated ticket.
|
||||||
|
|
||||||
|
### `changelord print`
|
||||||
|
|
||||||
|
Renders the changelog as markdown.
|
||||||
|
|
||||||
|
#### Options
|
||||||
|
|
||||||
|
- `--no-next` -- don't show the `NEXT` section.
|
||||||
|
|
||||||
|
### `changelord cut`
|
||||||
|
|
||||||
|
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
|
||||||
|
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
|
||||||
|
|
||||||
|
- `--dry` -- Resolves the next version but only outputs the resulting section
|
||||||
|
without changing the source file.
|
||||||
|
|
||||||
|
### `changelord schema`
|
||||||
|
|
||||||
|
Outputs the JSON schema defining the structure of the source file.
|
||||||
|
|
||||||
|
### `changelord upcoming`
|
||||||
|
|
||||||
|
Outputs the changes listed in the `NEXT` release.
|
||||||
|
|
||||||
|
### `changelord latest-version`
|
||||||
|
|
||||||
|
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
|
||||||
|
added to the changelog.
|
||||||
|
|
||||||
|
#### Lower bound of the git log
|
||||||
|
|
||||||
|
`git-gather` inspects the git log from the most recent of those
|
||||||
|
three points:
|
||||||
|
|
||||||
|
- The last change in the NEXT release having a `commit` property.
|
||||||
|
- The last tagged version.
|
||||||
|
- The beginning of time.
|
||||||
|
|
||||||
|
#### Change-like git message
|
||||||
|
|
||||||
|
Git messages are compared to the regular expression
|
||||||
|
configured at `project.commit_regex`. If none is found, it
|
||||||
|
defaults to
|
||||||
|
|
||||||
|
^(?<type>[^: ]+):\s*(?<desc>.*?)(\[(?<ticket>[^\]]+)\])?$
|
||||||
|
|
||||||
|
The regular expression must capture a `desc` field, and may
|
||||||
|
capture a `type` and `ticket` as well.
|
||||||
|
|
||||||
|
[blog]: https://techblog.babyl.ca/entry/changelord/
|
||||||
|
[original]: https://metacpan.org/dist/App-Changelord/view/bin/changelord
|
22
Taskfile.yaml
Normal file
22
Taskfile.yaml
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
# https://taskfile.dev
|
||||||
|
version: "3"
|
||||||
|
|
||||||
|
vars:
|
||||||
|
PARENT_BRANCH: main
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
test: vitest run src
|
||||||
|
test:dev: vitest src
|
||||||
|
|
||||||
|
integrate:
|
||||||
|
deps: [test]
|
||||||
|
preconditions:
|
||||||
|
- sh: git is-clean
|
||||||
|
msg: checkout not clean
|
||||||
|
- sh: git diff-ls {{.PARENT_BRANCH}} | grep test
|
||||||
|
msg: no tests were added
|
||||||
|
- sh: git diff-ls {{.PARENT_BRANCH}} | grep CHANGELOG.yml
|
||||||
|
msg: no changelog entry detected
|
||||||
|
cmds:
|
||||||
|
- git checkout {{.PARENT_BRANCH}}
|
||||||
|
- git weld -
|
17
_templates/command/new/command.ejs.t
Normal file
17
_templates/command/new/command.ejs.t
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
to: src/command/<%= name %>.js
|
||||||
|
---
|
||||||
|
|
||||||
|
const handler = async (config) => {
|
||||||
|
// DO SOMETHING
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
command: '<%= name %>',
|
||||||
|
desc : 'TODO',
|
||||||
|
builder: (yargs) => {
|
||||||
|
yargs
|
||||||
|
},
|
||||||
|
handler,
|
||||||
|
}
|
||||||
|
|
5
_templates/generator/help/index.ejs.t
Normal file
5
_templates/generator/help/index.ejs.t
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
message: |
|
||||||
|
hygen {bold generator new} --name [NAME] --action [ACTION]
|
||||||
|
hygen {bold generator with-prompt} --name [NAME] --action [ACTION]
|
||||||
|
---
|
18
_templates/generator/new/hello.ejs.t
Normal file
18
_templates/generator/new/hello.ejs.t
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
---
|
||||||
|
to: _templates/<%= name %>/<%= action || 'new' %>/hello.ejs.t
|
||||||
|
---
|
||||||
|
---
|
||||||
|
to: app/hello.js
|
||||||
|
---
|
||||||
|
const hello = ```
|
||||||
|
Hello!
|
||||||
|
This is your first hygen template.
|
||||||
|
|
||||||
|
Learn what it can do here:
|
||||||
|
|
||||||
|
https://github.com/jondot/hygen
|
||||||
|
```
|
||||||
|
|
||||||
|
console.log(hello)
|
||||||
|
|
||||||
|
|
18
_templates/generator/with-prompt/hello.ejs.t
Normal file
18
_templates/generator/with-prompt/hello.ejs.t
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
---
|
||||||
|
to: _templates/<%= name %>/<%= action || 'new' %>/hello.ejs.t
|
||||||
|
---
|
||||||
|
---
|
||||||
|
to: app/hello.js
|
||||||
|
---
|
||||||
|
const hello = ```
|
||||||
|
Hello!
|
||||||
|
This is your first prompt based hygen template.
|
||||||
|
|
||||||
|
Learn what it can do here:
|
||||||
|
|
||||||
|
https://github.com/jondot/hygen
|
||||||
|
```
|
||||||
|
|
||||||
|
console.log(hello)
|
||||||
|
|
||||||
|
|
14
_templates/generator/with-prompt/prompt.ejs.t
Normal file
14
_templates/generator/with-prompt/prompt.ejs.t
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
to: _templates/<%= name %>/<%= action || 'new' %>/prompt.js
|
||||||
|
---
|
||||||
|
|
||||||
|
// see types of prompts:
|
||||||
|
// https://github.com/enquirer/enquirer/tree/master/examples
|
||||||
|
//
|
||||||
|
module.exports = [
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
name: 'message',
|
||||||
|
message: "What's your message?"
|
||||||
|
}
|
||||||
|
]
|
4
_templates/init/repo/new-repo.ejs.t
Normal file
4
_templates/init/repo/new-repo.ejs.t
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
setup: <%= name %>
|
||||||
|
force: true # this is because mostly, people init into existing folders is safe
|
||||||
|
---
|
26
package.json
26
package.json
@ -1,12 +1,20 @@
|
|||||||
{
|
{
|
||||||
"name": "changelord",
|
"name": "changelord",
|
||||||
"version": "0.0.1",
|
"version": "0.2.0",
|
||||||
"description": "cli-based changelog manager",
|
"description": "cli-based changelog manager",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.js",
|
"main": "src/changelord.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
"changelord": "./src/changelord.js"
|
"changelord": "./src/changelord.js"
|
||||||
},
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://git.babyl.ca/yanick/changelord.js.git"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://git.babyl.ca/yanick/changelord.js/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://git.babyl.ca/yanick/changelord.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
},
|
},
|
||||||
@ -16,12 +24,24 @@
|
|||||||
"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",
|
||||||
|
"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",
|
||||||
|
"nanoid": "^4.0.2",
|
||||||
|
"remeda": "^1.14.0",
|
||||||
|
"semver": "^7.5.0",
|
||||||
|
"simple-git": "^3.18.0",
|
||||||
"yaml": "^2.2.2",
|
"yaml": "^2.2.2",
|
||||||
"yargs": "^17.7.2"
|
"yargs": "^17.7.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"prettier": "^2.8.8"
|
"prettier": "^2.8.8",
|
||||||
|
"typescript": "^5.0.4",
|
||||||
|
"vitest": "^0.31.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
9
prettier.config.mjs
Normal file
9
prettier.config.mjs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export default {
|
||||||
|
endOfLine: "lf",
|
||||||
|
semi: true,
|
||||||
|
singleQuote: false,
|
||||||
|
tabWidth: 2,
|
||||||
|
trailingComma: "es5",
|
||||||
|
bracketSpacing: true,
|
||||||
|
proseWrap: "always",
|
||||||
|
};
|
118
src/changelog-schema.js
Normal file
118
src/changelog-schema.js
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
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.",
|
||||||
|
},
|
||||||
|
next_directory: {
|
||||||
|
type: "string",
|
||||||
|
description:
|
||||||
|
"directory where the changes for the NEXT release are stashed",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
@ -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' ] }
|
|
@ -1,26 +1,151 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
import { hideBin } from 'yargs/helpers';
|
import { hideBin } from "yargs/helpers";
|
||||||
import yargs from 'yargs';
|
import yargs from "yargs";
|
||||||
import { join } from 'path';
|
import { join } from "path";
|
||||||
|
import yaml from "yaml";
|
||||||
|
import fs from "fs-extra";
|
||||||
|
import consola from "consola";
|
||||||
|
import u from "@yanick/updeep-remeda";
|
||||||
|
import { once } from "remeda";
|
||||||
|
import simpleGit from "simple-git";
|
||||||
|
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";
|
||||||
import schema from './command/schema.js';
|
import schema from "./command/schema.js";
|
||||||
import consola from 'consola';
|
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";
|
||||||
|
import { globby } from "globby";
|
||||||
|
import { flatMap } from "remeda";
|
||||||
|
|
||||||
consola.raw = (...args) => console.log(...args);
|
consola.raw = (...args) => console.log(...args);
|
||||||
|
|
||||||
yargs(hideBin(process.argv))
|
yargs(hideBin(process.argv))
|
||||||
.config({
|
.middleware((config) => {
|
||||||
consola
|
config.git = once(simpleGit);
|
||||||
})
|
config.consola = consola;
|
||||||
.default('source', join( process.cwd(), 'CHANGELOG.yml' ))
|
config.changelog = once(() =>
|
||||||
.describe('source', 'changelog source')
|
fs
|
||||||
.command({
|
.readFile(config.source, "utf-8")
|
||||||
...print,
|
.then(yaml.parse)
|
||||||
command: '$0',
|
.then(async (doc) => {
|
||||||
})
|
if (!doc.project.next_directory) return doc;
|
||||||
.command(init)
|
|
||||||
.command(schema)
|
const changes = await globby([
|
||||||
.command(print).help().parse();
|
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) => {
|
||||||
|
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.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) => {
|
||||||
|
if (!changelog) changelog = await config.changelog();
|
||||||
|
return fs.writeFile(config.source, yaml.stringify(changelog));
|
||||||
|
};
|
||||||
|
config.next_release = next_release.bind(config);
|
||||||
|
config.latest_version = latest_version.bind(config);
|
||||||
|
return config;
|
||||||
|
})
|
||||||
|
.middleware((argv, yargs) => {
|
||||||
|
argv.yargs = yargs;
|
||||||
|
|
||||||
|
argv.add_to_next = async (entry) => {
|
||||||
|
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" }));
|
||||||
|
if (!next) {
|
||||||
|
changelog.releases.unshift(base_next_version);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(entry).length === 1) {
|
||||||
|
entry = entry.desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
next.changes.push(entry);
|
||||||
|
|
||||||
|
return argv.save_changelog();
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.default("source", join(process.cwd(), "CHANGELOG.yml"))
|
||||||
|
.describe("source", "changelog source")
|
||||||
|
.command(init)
|
||||||
|
.command(add)
|
||||||
|
.command(print)
|
||||||
|
.command(cut)
|
||||||
|
.command(schema)
|
||||||
|
.command(upcoming)
|
||||||
|
.command({
|
||||||
|
...upcoming,
|
||||||
|
command: "next",
|
||||||
|
desc: 'alias for "upcoming"',
|
||||||
|
})
|
||||||
|
.command(latest)
|
||||||
|
.command(git_gather)
|
||||||
|
.command(validate)
|
||||||
|
.strictCommands()
|
||||||
|
.help()
|
||||||
|
.parse();
|
||||||
|
34
src/command/add.js
Normal file
34
src/command/add.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import fs from "fs-extra";
|
||||||
|
import yaml from "yaml";
|
||||||
|
import u from "@yanick/updeep-remeda";
|
||||||
|
|
||||||
|
const handler = async (config) => {
|
||||||
|
if (!config.change)
|
||||||
|
throw new Error("can't add a change without a description");
|
||||||
|
|
||||||
|
const entry = {
|
||||||
|
desc: config.change.join(" "),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (config.ticket) entry.ticket = config.ticket;
|
||||||
|
if (config.type) entry.type = config.type;
|
||||||
|
|
||||||
|
config.consola.start(`adding '${entry.desc}' to the changelog`);
|
||||||
|
|
||||||
|
await config.add_to_next(entry);
|
||||||
|
|
||||||
|
config.consola.success("done!");
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
command: "add [change...]",
|
||||||
|
desc: "add a change to the NEXT release",
|
||||||
|
builder: (yargs) => {
|
||||||
|
yargs
|
||||||
|
.string("ticket")
|
||||||
|
.describe("ticket", "ticket associated with the change")
|
||||||
|
.string("type")
|
||||||
|
.describe("type", "type of change");
|
||||||
|
},
|
||||||
|
handler,
|
||||||
|
};
|
83
src/command/cut.js
Normal file
83
src/command/cut.js
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import yaml from "yaml";
|
||||||
|
import fs from "fs-extra";
|
||||||
|
import u from "@yanick/updeep-remeda";
|
||||||
|
import semverInc from "semver/functions/inc.js";
|
||||||
|
import { simpleGit } from "simple-git";
|
||||||
|
|
||||||
|
import { base_next_version } from "../utils.js";
|
||||||
|
|
||||||
|
const code_churn = async (previous_version) => {
|
||||||
|
previous_version = previous_version
|
||||||
|
? "v" + previous_version.version
|
||||||
|
: "4b825dc642cb6eb9a060e54bf8d69288fbee4904"; // empty tree sha1
|
||||||
|
|
||||||
|
return simpleGit().diff(["--shortstat", previous_version, "HEAD"]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handler = async (config) => {
|
||||||
|
config.consola.start("cutting the next version...");
|
||||||
|
|
||||||
|
const changelog = await config.changelog();
|
||||||
|
|
||||||
|
const next = changelog.releases.find(u.matches({ version: "NEXT" }));
|
||||||
|
|
||||||
|
if (!next) throw new Error("no changes since last version, aborting");
|
||||||
|
|
||||||
|
const previous_version = changelog.releases.find(
|
||||||
|
({ version }) => version !== "NEXT"
|
||||||
|
);
|
||||||
|
|
||||||
|
next.date = new Date().toISOString().replace(/T.*/, "");
|
||||||
|
|
||||||
|
const type_to_level = (type = "") =>
|
||||||
|
changelog.change_types.find((t) => t.keywords.includes(type)).level;
|
||||||
|
|
||||||
|
const bump_level = (types) =>
|
||||||
|
types.includes("major")
|
||||||
|
? "major"
|
||||||
|
: types.includes("minor")
|
||||||
|
? "minor"
|
||||||
|
: "patch";
|
||||||
|
|
||||||
|
const bumper = bump_level(
|
||||||
|
next.changes.map(({ type }) => type_to_level(type))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (changelog.project?.with_stats) {
|
||||||
|
next.changes.push({
|
||||||
|
type: "stats",
|
||||||
|
desc: "code churn:" + (await code_churn(previous_version)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
next.version = semverInc(
|
||||||
|
previous_version ? previous_version.version : "0.0.0",
|
||||||
|
bumper
|
||||||
|
);
|
||||||
|
|
||||||
|
// add a new NEXT
|
||||||
|
changelog.releases.unshift(base_next_version);
|
||||||
|
|
||||||
|
if (config.dry) {
|
||||||
|
config.consola.info("running in dry mode, not saving\n", next);
|
||||||
|
} else {
|
||||||
|
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!`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
command: "cut",
|
||||||
|
desc: "cut the next version",
|
||||||
|
builder: (yargs) => {
|
||||||
|
yargs.boolean("dry");
|
||||||
|
},
|
||||||
|
handler,
|
||||||
|
};
|
23
src/command/cut.test.js
Normal file
23
src/command/cut.test.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { test, expect, vi } from "vitest";
|
||||||
|
import cut from "./cut.js";
|
||||||
|
|
||||||
|
test("add a new NEXT", async () => {
|
||||||
|
const changelog = {
|
||||||
|
releases: [{ version: "NEXT", changes: [] }, { version: "1.0.0" }],
|
||||||
|
};
|
||||||
|
|
||||||
|
const noop = () => {};
|
||||||
|
const config = {
|
||||||
|
consola: {
|
||||||
|
start: noop,
|
||||||
|
success: noop,
|
||||||
|
},
|
||||||
|
changelog: () => changelog,
|
||||||
|
save_changelog: noop,
|
||||||
|
};
|
||||||
|
|
||||||
|
await cut.handler(config);
|
||||||
|
|
||||||
|
expect(changelog.releases[0].version).toBe("NEXT");
|
||||||
|
expect(changelog.releases).toHaveLength(3);
|
||||||
|
});
|
55
src/command/git-gather.js
Normal file
55
src/command/git-gather.js
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import simpleGit from "simple-git";
|
||||||
|
import { prop, compact, pickBy } from "remeda";
|
||||||
|
|
||||||
|
const handler = async (config) => {
|
||||||
|
const next = await config.next_release();
|
||||||
|
const seen_sha1s = compact(next?.changes.map(prop("commit")));
|
||||||
|
const seen_descs = next?.changes.map((change) =>
|
||||||
|
typeof change === "string" ? change : change.desc
|
||||||
|
);
|
||||||
|
|
||||||
|
const git = config.git();
|
||||||
|
|
||||||
|
const { version } = await config.latest_version();
|
||||||
|
|
||||||
|
let { all: commits } = await git.log({
|
||||||
|
from: "v" + version,
|
||||||
|
});
|
||||||
|
|
||||||
|
config.consola.start(`gathering changes since v${version}`);
|
||||||
|
|
||||||
|
commits = commits.filter(({ hash }) => !seen_sha1s.includes(hash));
|
||||||
|
|
||||||
|
const regex = new RegExp(
|
||||||
|
(await config.changelog().project?.commit_regex) ??
|
||||||
|
/^(?<type>[^: ]+):\s*(?<desc>.*?)(\[(?<ticket>[^\]]+)\])?$/
|
||||||
|
);
|
||||||
|
|
||||||
|
const changes = commits
|
||||||
|
.map((commit) => [commit, commit.message.match(regex)])
|
||||||
|
.filter((x) => x[1])
|
||||||
|
.filter((x) => !seen_descs.includes(x[1].groups.desc))
|
||||||
|
.map(([commit, res]) =>
|
||||||
|
pickBy({ ...res.groups, commit: commit.hash }, (x) => x)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (changes.length === 0) {
|
||||||
|
config.consola.success("no changes detected");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const change of changes) {
|
||||||
|
config.consola.info(`${change.type}: ${change.desc}`);
|
||||||
|
config.add_to_next(change);
|
||||||
|
}
|
||||||
|
config.consola.success("done");
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
command: "git-gather",
|
||||||
|
desc: "gather change entries from git commits",
|
||||||
|
builder: (yargs) => {
|
||||||
|
yargs;
|
||||||
|
},
|
||||||
|
handler,
|
||||||
|
};
|
28
src/command/git-gather.test.js
Normal file
28
src/command/git-gather.test.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { test, expect, vi } from "vitest";
|
||||||
|
import gather from "./git-gather.js";
|
||||||
|
|
||||||
|
test("no changes detected", async () => {
|
||||||
|
const changelog = {
|
||||||
|
releases: [{ version: "NEXT", changes: [] }, { version: "1.0.0" }],
|
||||||
|
};
|
||||||
|
|
||||||
|
const noop = () => {};
|
||||||
|
const config = {
|
||||||
|
consola: {
|
||||||
|
start: noop,
|
||||||
|
success: vi.fn(),
|
||||||
|
},
|
||||||
|
changelog: () => changelog,
|
||||||
|
next_release: () => ({
|
||||||
|
changes: [],
|
||||||
|
}),
|
||||||
|
latest_version: () => ({ version: "1.2.3" }),
|
||||||
|
git: () => ({
|
||||||
|
log: () => ({ all: [] }),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
await gather.handler(config);
|
||||||
|
|
||||||
|
expect(config.consola.success).toHaveBeenCalledWith("no changes detected");
|
||||||
|
});
|
@ -1,50 +1,51 @@
|
|||||||
import fs from 'fs-extra';
|
import fs from "fs-extra";
|
||||||
import { consola } from 'consola';
|
import { consola } from "consola";
|
||||||
import { stringify } from 'yaml';
|
import { stringify } from "yaml";
|
||||||
|
|
||||||
const change_types = [
|
const change_types = [
|
||||||
{ title: 'Features' , level: 'minor', keywords: [ 'feat' ] } ,
|
{ title: "", level: "minor", keywords: [""] },
|
||||||
{ title : 'Bug fixes' , level : 'patch', keywords : [ 'fix' ] },
|
{ title: "Features", level: "minor", keywords: ["feat"] },
|
||||||
{ title : 'Package maintenance' , level : 'patch', keywords : [ 'chore', 'maint', 'refactor' ] },
|
{ title: "Bug fixes", level: "patch", keywords: ["fix"] },
|
||||||
{ title : 'Statistics' , level : 'patch', keywords : [ 'stats' ] },
|
{
|
||||||
];
|
title: "Package maintenance",
|
||||||
|
level: "patch",
|
||||||
|
keywords: ["chore", "maint", "refactor"],
|
||||||
|
},
|
||||||
|
{ title: "Statistics", level: "patch", keywords: ["stats"] },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const base_changelog = {
|
||||||
const base_changelog = {
|
project: {
|
||||||
project: {
|
name: null,
|
||||||
name: null,
|
homepage: null,
|
||||||
homepage: null,
|
with_stats: true,
|
||||||
with_stats: true,
|
},
|
||||||
ticket_url: null,
|
releases: [{ version: "NEXT", changes: [] }],
|
||||||
},
|
change_types,
|
||||||
releases: [
|
|
||||||
{ version: 'NEXT', changes: [] }
|
|
||||||
],
|
|
||||||
change_types,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handler = async (config) => {
|
const handler = async (config) => {
|
||||||
if( await fs.pathExists(config.source) ) {
|
if (await fs.pathExists(config.source)) {
|
||||||
consola.error(`${config.source} already exist, aborting.`);
|
consola.error(`${config.source} already exist, aborting.`);
|
||||||
process.exit();
|
process.exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
consola.start(`creating ${config.source}...`);
|
consola.start(`creating ${config.source}...`);
|
||||||
|
|
||||||
await fs.writeFile( config.source, stringify(base_changelog) );
|
await fs.writeFile(config.source, stringify(base_changelog));
|
||||||
|
|
||||||
consola.success('done!');
|
|
||||||
|
|
||||||
|
consola.success("done!");
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
command: 'init',
|
command: "init",
|
||||||
desc : 'initialize new changelog source file',
|
desc: "initialize new changelog source file",
|
||||||
builder: (yargs) => {
|
builder: (yargs) => {
|
||||||
yargs.boolean('json')
|
yargs
|
||||||
.boolean('next')
|
.boolean("json")
|
||||||
.default('json',false)
|
.boolean("next")
|
||||||
.default('next',true);
|
.default("json", false)
|
||||||
},
|
.default("next", true);
|
||||||
handler,
|
},
|
||||||
}
|
handler,
|
||||||
|
};
|
||||||
|
29
src/command/latest-version.js
Normal file
29
src/command/latest-version.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import yaml from "yaml";
|
||||||
|
import fs from "fs-extra";
|
||||||
|
|
||||||
|
export async function latest_version() {
|
||||||
|
const changelog = await this.changelog();
|
||||||
|
|
||||||
|
return changelog.releases.find(({ version }) => version !== "NEXT");
|
||||||
|
}
|
||||||
|
|
||||||
|
const handler = async (config) => {
|
||||||
|
const latest_version = config.latest_version()?.version ?? "0.0.0";
|
||||||
|
|
||||||
|
if (config.json) {
|
||||||
|
config.consola.raw(latest_version);
|
||||||
|
} else {
|
||||||
|
config.consola.raw(latest_version.version);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
command: "latest-version",
|
||||||
|
desc: "output the latest version present in the changelog",
|
||||||
|
builder: (yargs) => {
|
||||||
|
yargs
|
||||||
|
.boolean("json")
|
||||||
|
.describe("json", "output latest version entry as json");
|
||||||
|
},
|
||||||
|
handler,
|
||||||
|
};
|
@ -1,16 +1,135 @@
|
|||||||
|
import * as R from "remeda";
|
||||||
|
import fs from "fs-extra";
|
||||||
|
import yaml from "yaml";
|
||||||
|
import mkd from "markdown-utils";
|
||||||
|
|
||||||
const handler = (...args) => {
|
const render_header = (source) => {
|
||||||
console.log('hi!',args);
|
let header = "# Changelog";
|
||||||
|
|
||||||
|
let name = source.project?.name;
|
||||||
|
|
||||||
|
const links = {};
|
||||||
|
|
||||||
|
if (source?.project?.homepage) {
|
||||||
|
links.homepage = source?.project?.homepage;
|
||||||
|
name = `[${name}][homepage]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name) header += ` for ${name}`;
|
||||||
|
|
||||||
|
return { header, links };
|
||||||
|
};
|
||||||
|
|
||||||
|
function render_links(links) {
|
||||||
|
return Object.entries(links)
|
||||||
|
.map(([name, url]) => ` [${name}]: ${url}`)
|
||||||
|
.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
const render_change = (config, source) => (change) => {
|
||||||
|
let body = change.desc;
|
||||||
|
let links = {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
body,
|
||||||
|
links,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const render_release = (config, source) => (release) => {
|
||||||
|
let links = {};
|
||||||
|
|
||||||
|
if (typeof release === "string")
|
||||||
|
return {
|
||||||
|
body: release,
|
||||||
|
links,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (release.version === "NEXT" && !config.next)
|
||||||
|
return {
|
||||||
|
body: "",
|
||||||
|
links,
|
||||||
|
};
|
||||||
|
|
||||||
|
let body = "## " + release.version;
|
||||||
|
|
||||||
|
if (release.date) {
|
||||||
|
body += " " + release.date;
|
||||||
|
}
|
||||||
|
|
||||||
|
body += "\n\n";
|
||||||
|
|
||||||
|
const type_map = Object.fromEntries(
|
||||||
|
source.change_types.flatMap((entry) =>
|
||||||
|
entry.keywords.map((k) => [k, entry])
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const changes = release.changes.map((change) =>
|
||||||
|
typeof change === "string" ? { desc: change, type: "" } : change
|
||||||
|
);
|
||||||
|
|
||||||
|
const unknown = R.uniq(
|
||||||
|
changes
|
||||||
|
.map(R.prop("type"))
|
||||||
|
.map((t) => t ?? "")
|
||||||
|
.filter((type) => !type_map[type])
|
||||||
|
);
|
||||||
|
|
||||||
|
if (unknown.length) {
|
||||||
|
throw new Error(
|
||||||
|
"unknown change types encountered: " +
|
||||||
|
unknown.map((t) => `'${t}'`).join(", ")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const change_type of source.change_types) {
|
||||||
|
const type_changes = changes.filter(({ type }) =>
|
||||||
|
change_type.keywords.includes(type)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (type_changes.length === 0) continue;
|
||||||
|
|
||||||
|
if (change_type.title) body += "\n" + mkd.h3(change_type.title) + "\n\n";
|
||||||
|
|
||||||
|
for (const c of type_changes) {
|
||||||
|
const res = render_change(config, source)(c);
|
||||||
|
body += mkd.li(res.body, 1) + "\n";
|
||||||
|
links = { ...links, ...res.links };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { body, links };
|
||||||
|
};
|
||||||
|
|
||||||
|
const handler = async (config) => {
|
||||||
|
const source = await config.changelog();
|
||||||
|
|
||||||
|
let output = "";
|
||||||
|
|
||||||
|
let { header, links } = render_header(source);
|
||||||
|
|
||||||
|
output += header + "\n\n";
|
||||||
|
|
||||||
|
for (const res of source.releases.map(render_release(config, source))) {
|
||||||
|
output += res.body + "\n\n";
|
||||||
|
links = { ...links, ...res.links };
|
||||||
|
}
|
||||||
|
|
||||||
|
output += "\n\n\n\n" + render_links(links);
|
||||||
|
|
||||||
|
config.consola.raw(output);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
command: 'print',
|
command: "print",
|
||||||
desc : 'render the changelog',
|
desc: "render the changelog",
|
||||||
builder: (yargs) => {
|
builder: (yargs) => {
|
||||||
yargs.boolean('json')
|
yargs
|
||||||
.boolean('next')
|
//.boolean('json')
|
||||||
.default('json',false)
|
.boolean("next")
|
||||||
.default('next',true);
|
// .default('json',false)
|
||||||
},
|
.default("next", true);
|
||||||
handler,
|
},
|
||||||
}
|
handler,
|
||||||
|
};
|
||||||
|
22
src/command/print.test.js
Normal file
22
src/command/print.test.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { test, expect, vi } from 'vitest';
|
||||||
|
import { render_release } from './print.js';
|
||||||
|
import { base_changelog } from './init.js';
|
||||||
|
|
||||||
|
test( 'render_release a string', () => {
|
||||||
|
const {body} = render_release({})("as is");
|
||||||
|
expect(body).toEqual('as is');
|
||||||
|
});
|
||||||
|
|
||||||
|
test( 'render_release', () => {
|
||||||
|
const consola = { error: vi.fn() };
|
||||||
|
const res= render_release({consola},base_changelog)({
|
||||||
|
version: '1.2.3',
|
||||||
|
date: 'today',
|
||||||
|
changes: [
|
||||||
|
"this",
|
||||||
|
"that",
|
||||||
|
]
|
||||||
|
});
|
||||||
|
expect(res.body).toContain('# 1.2.3 today');
|
||||||
|
expect(res.body).toContain('* this');
|
||||||
|
});
|
@ -1,12 +1,9 @@
|
|||||||
import fs from "fs-extra";
|
import fs from "fs-extra";
|
||||||
|
import yaml from "yaml";
|
||||||
|
import schemaV1 from "../changelog-schema.js";
|
||||||
|
|
||||||
const handler = async ({consola}) => {
|
const handler = async ({ consola }) => {
|
||||||
consola.raw(
|
consola.raw(yaml.stringify(schemaV1));
|
||||||
await fs.readFile(
|
|
||||||
new URL("../changelog-schema.yml", import.meta.url),
|
|
||||||
"utf-8"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
31
src/command/upcoming.js
Normal file
31
src/command/upcoming.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import u from "@yanick/updeep-remeda";
|
||||||
|
import fs from "fs-extra";
|
||||||
|
import yaml from "yaml";
|
||||||
|
import { render_release } from "./print.js";
|
||||||
|
|
||||||
|
export async function next_release() {
|
||||||
|
const source = await this.changelog();
|
||||||
|
return (
|
||||||
|
source.releases.find(u.matches({ version: "NEXT" })) ?? {
|
||||||
|
version: "NEXT",
|
||||||
|
changes: [],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handler = async (config) => {
|
||||||
|
const source = await config.changelog();
|
||||||
|
|
||||||
|
const { body } = render_release(
|
||||||
|
{ ...config, next: true },
|
||||||
|
source
|
||||||
|
)(await config.next_release());
|
||||||
|
|
||||||
|
config.consola.raw("\n" + body);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
command: "upcoming",
|
||||||
|
desc: "output the changes in NEXT",
|
||||||
|
handler,
|
||||||
|
};
|
29
src/command/upcoming.test.js
Normal file
29
src/command/upcoming.test.js
Normal 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();
|
||||||
|
});
|
16
src/command/validate.js
Normal file
16
src/command/validate.js
Normal file
@ -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,
|
||||||
|
};
|
16
src/command/validate.test.js
Normal file
16
src/command/validate.test.js
Normal file
@ -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();
|
||||||
|
});
|
6
src/utils.js
Normal file
6
src/utils.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export const base_next_version = {
|
||||||
|
version: "NEXT",
|
||||||
|
changes: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.freeze(base_next_version);
|
Loading…
Reference in New Issue
Block a user