Class: Brute::Changelog
- Inherits:
-
Object
- Object
- Brute::Changelog
- Extended by:
- GemKit::Deprecate
- Defined in:
- lib/brute/changelog.rb
Overview
A parser and linter for CHANGELOG.md, which follows Keep a Changelog.
The changelog is the one release artefact nothing else can regenerate, so
it is the one most easily forgotten. Making it machine-checkable turns
"did anyone write the changelog?" into a gate: bin/lint-changelog
validates the format, and bin/release-gem refuses to push a version that
has no section of its own.
changelog = Brute::Changelog.load
changelog.problems # => [] when the format is clean
changelog.release_problems("4.1.0")
The shape it expects:
# Changelog
## [Unreleased]
## [4.1.0] - 2026-08-20
### Added
- Something that happened.
Defined Under Namespace
Classes: Release
Constant Summary collapse
- SECTIONS =
The six change types Keep a Changelog defines. Anything else under a version is a typo or an invention, and both are worth catching.
%w[Added Changed Deprecated Removed Fixed Security].freeze
- UNRELEASED =
"Unreleased"- HEADING =
/\A##\s+\[([^\]]+)\](?:\s+-\s+(.*))?\s*\z/- SUBHEADING =
/\A###\s+(.*?)\s*\z/- BULLET =
/\A[-*]\s+\S/- DATE =
/\A\d{4}-\d{2}-\d{2}\z/
Instance Attribute Summary collapse
-
#path ⇒ Object
readonly
Returns the value of attribute path.
-
#releases ⇒ Object
readonly
Returns the value of attribute releases.
Class Method Summary collapse
-
.load(path = File.expand_path("../../CHANGELOG.md", __dir__)) ⇒ Object
Load the changelog sitting beside the gem (../../CHANGELOG.md).
Instance Method Summary collapse
- #find(version) ⇒ Object
-
#initialize(text, path: "CHANGELOG.md") ⇒ Changelog
constructor
A new instance of Changelog.
- #missing? ⇒ Boolean
-
#problems ⇒ Object
Everything wrong with the file's format, as a list of human-readable problems.
-
#release_problems(version) ⇒ Object
Everything standing between this changelog and releasing
version. - #released ⇒ Object
- #unreleased ⇒ Object
Constructor Details
#initialize(text, path: "CHANGELOG.md") ⇒ Changelog
Returns a new instance of Changelog.
65 66 67 68 69 |
# File 'lib/brute/changelog.rb', line 65 def initialize(text, path: "CHANGELOG.md") @path = path @text = text @releases = text ? parse(text) : [] end |
Instance Attribute Details
#path ⇒ Object (readonly)
Returns the value of attribute path.
62 63 64 |
# File 'lib/brute/changelog.rb', line 62 def path @path end |
#releases ⇒ Object (readonly)
Returns the value of attribute releases.
62 63 64 |
# File 'lib/brute/changelog.rb', line 62 def releases @releases end |
Class Method Details
.load(path = File.expand_path("../../CHANGELOG.md", __dir__)) ⇒ Object
Load the changelog sitting beside the gem (../../CHANGELOG.md).
58 59 60 |
# File 'lib/brute/changelog.rb', line 58 def self.load(path = File.("../../CHANGELOG.md", __dir__)) new(File.exist?(path) ? File.read(path) : nil, path: path) end |
Instance Method Details
#find(version) ⇒ Object
77 78 79 80 |
# File 'lib/brute/changelog.rb', line 77 def find(version) target = Gem::Version.new(version.to_s) released.find { |release| Gem::Version.new(release.version) == target rescue false } end |
#missing? ⇒ Boolean
71 |
# File 'lib/brute/changelog.rb', line 71 def missing? = @text.nil? |
#problems ⇒ Object
Everything wrong with the file's format, as a list of human-readable problems. Empty means it lints clean.
84 85 86 87 88 |
# File 'lib/brute/changelog.rb', line 84 def problems return ["#{path} does not exist"] if missing? [*header_problems, *heading_problems, *ordering_problems, *content_problems] end |
#release_problems(version) ⇒ Object
Everything standing between this changelog and releasing version.
Format problems count: a file nobody can parse is not documentation.
92 93 94 95 96 97 98 99 100 101 102 103 104 105 |
# File 'lib/brute/changelog.rb', line 92 def release_problems(version) return problems unless problems.empty? release = find(version) return ["#{path} has no section for #{version} — run bin/update-changelog"] if release.nil? return ["#{path} section for #{version} is empty"] if release.empty? newest = released.first if newest && newest.version != release.version return ["#{path} lists #{newest.version} above #{version}; the release being cut must come first"] end [] end |
#released ⇒ Object
75 |
# File 'lib/brute/changelog.rb', line 75 def released = releases.reject(&:unreleased?) |
#unreleased ⇒ Object
73 |
# File 'lib/brute/changelog.rb', line 73 def unreleased = releases.find(&:unreleased?) |