Changelord, registrar of deeds extraordinaire

July 19th, 2022
json schemaperlchangelog

Changelord, registrar of deeds extraordinaire

This year’s Perl release brought us some nice candies, no least of all signatures being finally, FINALLY out of experimental status. In honor of it, and because I was feeling nostalgic, I dusted off my 1337 P3rl skillz and wrote a brand new cli application: Changelord, registrar of deeds extraordinaire.

(yeah, it’s a mouthful. I was originally going for the shorter and more to-the-point Changelord, annals master, but I could already hear the snort-giggles from the back of the room, so I reconsidered)

Changelord, registrar of what now?

A long time ago, I was writing a lot of Perl modules. Along the way, I also developed a love for semantic versioning, which allows me the greatest of all luxuries: not to think about things.

To help with the publishing of Perl modules to CPAN, I was (and still am) using dist-zilla and a gazillion plugins. Some of those plugins help the bumping of versions and the generation of the project’s CHANGELOG.

With my setting, I would have the CHANGELOG have a first section looking like this:

Revision history for Taskwarrior-Hooks

{{$NEXT}}

  [ API CHANGES ]

  [ BUG FIXES ]

  - blah blah blah

  [ DOCUMENTATION ]

  [ ENHANCEMENTS ]

  - some of blahs

  [ NEW FEATURES ]

  [ STATISTICS ]

When cutting a new release, the plugins would look into the non-empty subsections, figure out if the version bump should be a patch / minor / major, do it, replace the {{$NEXT}} title with it, remove the empty subsections AND put some stats of the number of files and lines changed since the last version.

That, already was already pretty awesome right there.

Of course, in JavaScript land, there are also tons of tools to help with semantic versioning and all that good stuff. The ones I was favoring lately glean the changes from commit messages following the pattern <type of change>: description. On paper it’s terrific, as the changelog message is literally part of the commits. In practice, it has its edges. Forgot to put such a commit message in the branch? Oopsy, time for a rebase. Oh, that was 3 merges ago? … Well, hope you like pain, ‘cause Git ordered some for you and the courier is about to ring the doorbell.

So, anyway, because of that I decided to try to take a step back to what was working so nicely. And spruce it up while we’re at it.

Like, let’s use YAML as the source of truth to formalize things (and make them programmatically searchable and stuff).

And let’s formalize the formalization and give that YAML a JSON Schema.

And— ah, but enough tell, let’s show you what I have so far.

Port an already-existing changelog

Let’s assume you already have a project, and it has a changelog looking like so:

# Changelog for Some Glorious App

## v1.0.0, 2022-04-01

    - Some stuff
    - Some other stuff

## v0.0.1, 2022-01-01

    - Initial release.

First thing is to initialize the changelog new source of truth:

$ changelord init
file 'CHANGELOG.yml' created, enjoy!

And indeed, if we look at CHANGELOG.yml, we have a brand-new stubbed changelog:

project:
  name: ~
  homepage: ~
  ticket_url: ~
  with_stats: true
releases:
  - version: NEXT
    date: ~
    changes: []
change_types:
  - keywords:
      - feat
    level: minor
    title: Features
  - keywords:
      - fix
    level: patch
    title: Bug fixes
  - keywords:
      - chore
      - maint
      - refactor
    level: patch
    title: Package maintenance
  - keywords:
      - stats
    level: patch
    title: Statistics

Right now the changelog has 3 sections. One for the project meta-data, one for the different releases (including the upcoming one). And a last one to describe all the types of changes that we’ll use.

Using our example, let’s fill the project metadata with some non-boring values:

project:
  name: Some Glorious App
  homepage: https://github.com/yanick/glorious-app
  ticket_url: s!#!https://github.com/yanick/glorious-app/issue/!
  with_stats: true

Already with that we can render a (pretty blank) changelog:

$ changelord 

# Changelog for [Some Glorious App][homepage]

    [homepage]: https://github.com/yanick/glorious-app



## NEXT

We can also verify that our yaml file is up-to-snuff with the schema:

$ changelord validate
woo, changelog is valid!

And we can also have a peek at said schema:

$ changelord schema 

$defs:
  change:
    additionalProperties: false
    properties:
      desc:
        type: string
      ticket:
        type:
          - string
          - null
      type:
        type:
          - string
          - null
    required:
      - desc
    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: string
          type: object
    type: array
type: object

Okay, we have already two releases. Do we have to convert them into a YAML structure off the bat? I mean, two releases are no big deal, but if you have an older project with hundreds of them…

Don’t worry, as it is, you don’t have to. You can just drop them verbatim right there like a cold clump of congealed spaghetti:

releases:
  - version: NEXT
    date: ~
    changes: []
  - |
      ## v1.0.0, 2022-04-01

          - Some stuff
          - Some other stuff

      ## v0.0.1, 2022-01-01

          - Initial release.

And with that:

$ changelord 
# Changelog for [Some Glorious App][homepage]

    [homepage]: https://github.com/yanick/glorious-app

## NEXT

## v1.0.0, 2022-04-01

    - Some stuff
    - Some other stuff

## v0.0.1, 2022-01-01

    - Initial release.

Of course, in due time you probably want to at least convert the most recent release to something more formal:

releases:
  - version: NEXT
    date: ~
    changes: []
  - version: v1.0.0
    date: 2022-04-01
    changes:
      - Some stuff
      - Some other stuff
  - |
      ## v0.0.1, 2022-01-01

          - Initial release.

Let’s add some changes

To add changes, you can edit CHANGELOG.yml yourself, or you can delegate the job to changelord:

$ changelord add --type chore some trivial thing

$ changelord add --type feat --ticket '#123' some awesome thing

The yaml file is updated as you would expect:

releases:
  - version: NEXT
    date: ~
    changes:
      - desc: some trivial thing
        type: chore
      - desc: some awesome thing
        ticket: '#123'
        type: feat

And the rendered version does its thing too:

$ changelord 

# Changelog for [Some Glorious App][homepage]

    [homepage]: https://github.com/yanick/glorious-app

## NEXT

### Features

  * some awesome thing [#123]

### Package maintenance

  * some trivial thing

  [#123]: https://github.com/yanick/glorious-app/issue/123

## v1.0.0, 2022-04-01

  * Some stuff
  * Some other stuff


## v0.0.1, 2022-01-01

    - Initial release.

Bump the version

And for the (for now) final step, the bumping of the version:

$ changelord bump

new version minted: v1.1.0

Which updates the version, date, and statistics in the yaml file:

releases:
  - version: v1.1.0
    date: 2022-06-20
    changes:
      - desc: some trivial thing
        type: chore
      - desc: some awesome thing
        ticket: '#123'
        type: feat
      - desc: 'code churn: 7 files changed, 179 insertions(+)'
        type: stats

Which are themselves reflected in the rendered changelog:

$ changelord 
# Changelog for [Some Glorious App][homepage]

    [homepage]: https://github.com/yanick/glorious-app

## v1.1.0, 2022-06-20

### Features

  * some awesome thing [#123]

### Package maintenance

  * some trivial thing

### Statistics

  * code churn: 7 files changed, 179 insertions(+)

  [#123]: https://github.com/yanick/glorious-app/issue/123

## dummy, 2022-04-01

  * Some stuff
  * Some other stuff


## v0.0.1, 2022-01-01

    - Initial release.

Feeling cute, might publish later

And that’s the extent of what I have so far. If I feel it, I might eventually document the whole thing and publish it to CPAN, until then it lives in its snug little repo.