Skip to content

About

Changelog fragments (changelog.d) for Ruby gems: rake tasks and a GitHub Action PR check

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

2 Commits

Folders and files

Repository files navigation

changelogd

Changelog fragments for Ruby projects with a hand-curated CHANGES.txt, as used by net-ssh, net-scp and net-sftp.

Contributors don't edit CHANGES.txt. Each pull request adds a fragment file to changelog.d/; the project's version bump (rake vbump:*) writes all fragments into a new === <version> section at the top of CHANGES.txt and deletes them. No merge conflicts on CHANGES.txt, and every PR states whether it changes anything users can see.

changelogd is a small gem (Ruby >= 2.6, stdlib only, no dependencies) with rake tasks, a Ruby API and a CLI, plus a GitHub Action that checks pull requests. It isn't on rubygems.org; projects use it through a Bundler git source.

Fragments

A fragment is named <number>.<type>.txt, where <number> is the PR number (open the PR first, then add the fragment), and holds a single UTF-8 paragraph: the entry as it should read in CHANGES.txt, without the bullet or the BREAKING:/Security: prefix. Lines are joined with a space, so it can be wrapped.

Type Written as Use for
breaking-security * BREAKING: Security: <text> an incompatible change made for security
breaking * BREAKING: <text> an incompatible change
security * Security: <text> a security fix or hardening
change * <text> any other user-visible fix or feature
none nothing PRs with no user-visible change (tests, CI, docs, refactors)
rake "changelog:new[1058,change,Session#exec! now returns the exit status]"
# changelog.d/1058.change.txt -> * Session#exec! now returns the exit status [#1058]

echo 'Removed the `:paranoid` option; use `:verify_host_key`' > changelog.d/1050.breaking.txt
echo 'Reject packet lengths above 256 KB, as OpenSSH does [GHSA-pfqh-p9vh-wx4p]' > changelog.d/1060.security.txt
touch changelog.d/1061.none.txt

References: if the text doesn't end with a reference list, [#<number>] is appended. A reference list is a bracketed, comma-separated list of #<number>, GHSA-xxxx-xxxx-xxxx or CVE-YYYY-NNNN items at the very end, such as [#985, #1056], [GHSA-pfqh-p9vh-wx4p] or [CVE-2024-12345, #1060]. Write the list yourself to reference issues or advisories; include the PR number in it if you want it shown. Anything else in brackets ([npezza93]) isn't a reference list, so the PR number is still appended.

Several entries from one PR take a numeric suffix: 1050.breaking.txt, 1050.breaking.2.txt, 1050.change.txt. rake changelog:new picks the next free one.

A fragment is rejected if its name doesn't match <number>.<type>[.<n>].txt with a known type, or if a non-none fragment is empty, has more than one paragraph, isn't valid UTF-8, still holds the TODO placeholder that changelog:new writes without a text, or starts with a bullet or with BREAKING:/Security: (the type adds those). The content of none fragments is ignored.

The changelog.d/ directory is what opts a project in. Keep a changelog.d/README.md in it (it's ignored, as are dotfiles) so the directory survives releases. Without the directory, releasing does nothing, so the same release tooling also works on old maintenance branches.

Release sections

Releasing for a heading such as 8.0.3 or 8.0.3 rc1 writes === <heading> at the top of CHANGES.txt (after any text before the first === heading), followed by a blank line, the entries, and a blank line before the previous section. It then git rms all fragments, including none ones, so the deletions are staged with the version bump; untracked fragments are deleted from disk. Committing is left to the caller.

  • Entries are grouped BREAKING: Security:, BREAKING:, Security:, then the rest.
  • Within a group, fragments are ordered by PR number, newest first, then by suffix.
  • Hand-written entries from an === Unreleased section at the top of CHANGES.txt, if there is one, follow the fragments of their group in their written order; that section is replaced by the release section. An entry starts with * and may span several lines; its group comes from its BREAKING: Security:/BREAKING:/Security: prefix.
  • A final release (X.Y.Z without a pre-release suffix) with nothing to report still gets a section saying No changes since <previous version>., so every published version has a heading. A pre-release with nothing to report leaves CHANGES.txt alone.
  • The bullet comes from the first entry already in CHANGES.txt ( * in net-ssh, * in net-scp and net-sftp), * if there is none.
  • Releasing a heading that CHANGES.txt already has fails, unless no fragments or Unreleased entries are pending; then it only removes the none fragments. That lets a hand-written section for the version stand.

Setting up a project

Gemfile:

gem "changelogd", github: "net-ssh/changelogd", tag: "v1", group: :development

github: needs Bundler 2. Bundler 1 (the default on Ruby 2.6) expands it to a git:// URL that GitHub no longer serves, so a project that still uses Bundler 1 anywhere needs git: "https://github.com/net-ssh/changelogd.git" instead. v1 is moved to each compatible release; bundle update changelogd picks up the latest.

Rakefile, with a change_version that rewrites version.rb for rake vbump:*:

begin
  require "changelogd/rake"
rescue LoadError
  # A git gem isn't on the load path under plain `rake`, as the release workflow runs it; bundler/setup could clash with the running rake.
  require "bundler"
  $LOAD_PATH.unshift(*Bundler.definition.specs["changelogd"].flat_map(&:full_require_paths))
  require "changelogd/rake"
end

Changelogd::RakeTasks.new(version_file: "lib/net/ssh/version.rb")

def change_version
  Rake::Task["changelog:check"].invoke # fail before touching version.rb
  # ... write the new MAJOR/MINOR/TINY/PRE to lib/net/ssh/version.rb ...
  Rake::Task["changelog:release"].invoke
end

Under bundle exec the first require works; under plain rake after bundle install (as the net-ssh-release bump workflows run it) the rescue finds the gem through Bundler without bundler/setup, which would fail when the running rake isn't the locked one. changelog:release reads the version file when it runs, so it sees the bumped version. The version file must define MAJOR, MINOR and TINY as integers and PRE as nil or a string, one per line; PRE = "rc1" gives the heading 8.0.3 rc1.

Then add changelog.d/README.md, the pull request check, and a short contributor note: a "Changelog" section in CONTRIBUTING.md and a comment in .github/pull_request_template.md. Contributors need the file name, the types, an example and the skip-changelog label; link here for the rest.

To use a local changelogd checkout, set BUNDLE_LOCAL__CHANGELOGD=../changelogd and BUNDLE_DISABLE_LOCAL_BRANCH_CHECK=true (Bundler's local overrides otherwise require a branch:), or the equivalent bundle config set --local local.changelogd ... settings.

Pull request check

# .github/workflows/changelog.yml
name: Changelog

on:
  pull_request:
    types: [opened, synchronize, reopened, labeled, unlabeled]

permissions:
  contents: read

jobs:
  changelog:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          # HEAD is the PR merged into the base branch; HEAD^1 is the base it was merged into.
          fetch-depth: 2
      - uses: net-ssh/changelogd@v1

The action runs the CLI with the runner's Ruby, no Bundler needed. It validates every fragment and the === Unreleased section, and fails the PR if it

  • edits CHANGES.txt, or
  • doesn't add or modify a changelog.d/<PR>.<type>[.<n>].txt fragment, unless it only touches changelog.d/.

A PR with the skip-changelog label, or opened by dependabot[bot], needs no fragment; fragments it does add are still validated. labeled/unlabeled in the trigger make the check re-run when a maintainer adds or removes the label.

Input Default Description
pr event's PR number PR whose fragment is required
base-ref HEAD^1 commit the PR is diffed against
skip-label skip-changelog label that waives the fragment requirement
skip-users dependabot[bot] space-separated PR authors that need no fragment
working-directory . project root
changes CHANGES.txt changelog file, relative to the root
dir changelog.d fragment directory, relative to the root
ruby-version runner's Ruby install this Ruby with ruby/setup-ruby first

Rake tasks

require "changelogd/rake" and Changelogd::RakeTasks.new(version_file: ..., root: Dir.pwd, changes: "CHANGES.txt", dir: "changelog.d", bullet: nil) define:

Task Does
rake "changelog:new[1058,change,Text]" creates a fragment; commas and spaces in the text are kept
rake changelog:check validates the fragments and the Unreleased section
rake changelog:preview prints the section the next release would write ([8.0.3] sets the heading)
rake changelog:release writes the release section for the version file's version (or [<heading>])

Errors abort the task with exit status 1.

Ruby API

require "changelogd"

changelog = Changelogd.new(root, changes: "CHANGES.txt", dir: "changelog.d", bullet: nil)
changelog.check                          # => fragments; raises Changelogd::Error listing every problem
changelog.section("8.0.3")               # => section text, or nil when there is nothing to release
changelog.release("8.0.3")               # => writes it, removes the fragments; section or nil
changelog.new_fragment(1058, "change", "Text") # => path
Changelogd.heading_from_version_file("lib/net/ssh/version.rb") # => "8.0.3 rc1"

CLI

bundle exec changelogd, or ruby exe/changelogd from a checkout:

changelogd [options] check
changelogd [options] require-fragment --pr N [--base-ref HEAD^1]
changelogd [options] section [HEADING | --version-file F]   # preview, default heading: Unreleased
changelogd [options] release HEADING | --version-file F
changelogd [options] new N TYPE [TEXT...]

  --root DIR         project root (default: .)
  --changes FILE     changelog, relative to the root (default: CHANGES.txt)
  --dir DIR          fragment directory, relative to the root (default: changelog.d)
  --bullet STR       entry prefix (default: as in CHANGES.txt)

Exit status: 0 on success, 1 on invalid fragments or a failed check, 2 on a usage error. On GitHub Actions errors are printed as ::error:: annotations.

Development

rake test, on Ruby 2.6 and newer. Releases bump Changelogd::VERSION and are tagged vX.Y.Z; the v1 tag is moved to the latest v1.x.y (git tag -f v1 && git push -f origin v1), so both net-ssh/changelogd@v1 and tag: "v1" Gemfiles get compatible fixes. Breaking changes go to v2.

License

MIT, see LICENSE.

About

Changelog fragments (changelog.d) for Ruby gems: rake tasks and a GitHub Action PR check

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages