danger-changelog 0.9.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +17 -4
  3. data/README.md +143 -8
  4. data/doc/intridea.md +47 -5
  5. data/doc/keep_a_changelog.md +2 -0
  6. data/lib/changelog/added_lines.rb +19 -0
  7. data/lib/changelog/changelog_line/changelog_entry_line.rb +16 -11
  8. data/lib/changelog/changelog_line/changelog_header_line.rb +25 -9
  9. data/lib/changelog/changelog_line/changelog_line.rb +6 -4
  10. data/lib/changelog/changelog_line/changelog_line_parser.rb +5 -3
  11. data/lib/changelog/changelog_line/changelog_placeholder_line.rb +3 -3
  12. data/lib/changelog/config.rb +76 -44
  13. data/lib/changelog/gem_version.rb +1 -1
  14. data/lib/changelog/intent_check.rb +78 -0
  15. data/lib/changelog/markdown_references.rb +57 -0
  16. data/lib/changelog/parsers/base.rb +14 -2
  17. data/lib/changelog/parsers/intridea_format.rb +15 -4
  18. data/lib/changelog/parsers/keep_a_changelog.rb +4 -4
  19. data/lib/changelog/parsers.rb +3 -3
  20. data/lib/changelog/plugin.rb +138 -30
  21. data/lib/changelog/pr_metadata.rb +15 -2
  22. data/lib/changelog/unreleased_section.rb +34 -0
  23. metadata +7 -47
  24. data/.github/workflows/danger-comment.yml +0 -10
  25. data/.github/workflows/danger.yml +0 -9
  26. data/.github/workflows/lint.yml +0 -16
  27. data/.github/workflows/test.yml +0 -20
  28. data/.gitignore +0 -6
  29. data/.rspec +0 -2
  30. data/.rubocop.yml +0 -31
  31. data/.rubocop_todo.yml +0 -118
  32. data/CONTRIBUTING.md +0 -125
  33. data/Dangerfile +0 -4
  34. data/Gemfile +0 -21
  35. data/Guardfile +0 -19
  36. data/RELEASING.md +0 -67
  37. data/Rakefile +0 -23
  38. data/danger-changelog.gemspec +0 -21
  39. data/images/have_you_updated_changelog.png +0 -0
  40. data/images/is_changelog_format_correct.png +0 -0
  41. data/spec/changelog_spec.rb +0 -144
  42. data/spec/config_spec.rb +0 -104
  43. data/spec/intridea/changelog_entry_line_spec.rb +0 -174
  44. data/spec/intridea/changelog_file_spec.rb +0 -155
  45. data/spec/intridea/changelog_header_line_spec.rb +0 -93
  46. data/spec/intridea/changelog_line_parser_spec.rb +0 -62
  47. data/spec/intridea/changelog_placeholder_line_spec.rb +0 -32
  48. data/spec/intridea/changelog_spec.rb +0 -168
  49. data/spec/intridea/fixtures/bot_authors.md +0 -5
  50. data/spec/intridea/fixtures/customized.md +0 -3
  51. data/spec/intridea/fixtures/dates.md +0 -16
  52. data/spec/intridea/fixtures/extra_trailing_space.md +0 -3
  53. data/spec/intridea/fixtures/imbalanced.md +0 -7
  54. data/spec/intridea/fixtures/lines.md +0 -10
  55. data/spec/intridea/fixtures/minimal.md +0 -3
  56. data/spec/intridea/fixtures/missing_your_contribution_here.md +0 -2
  57. data/spec/intridea/fixtures/semver.md +0 -13
  58. data/spec/intridea/fixtures/validation_result.md +0 -6
  59. data/spec/keep_a_changelog/changelog_spec.rb +0 -87
  60. data/spec/keep_a_changelog/fixtures/complete.md +0 -161
  61. data/spec/keep_a_changelog/fixtures/invalid_line.md +0 -23
  62. data/spec/keep_a_changelog/fixtures/lines_with_links.md +0 -14
  63. data/spec/keep_a_changelog/fixtures/missing_a_version_header.md +0 -20
  64. data/spec/plugin_spec.rb +0 -11
  65. data/spec/pr_metadata_spec.rb +0 -126
  66. data/spec/spec_helper.rb +0 -47
  67. data/spec/support/shared/changelog.rb +0 -41
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 173f58cbaf43ff80a454e062c19f6674e0742c37fbd59c5bb4e85bc00c8d3899
4
- data.tar.gz: 21ac5d08f53444c17cf3231880fc4f45beaca225970ecd1862002909ebbcf8fe
3
+ metadata.gz: 171cb4b8d29586152bb30888b61a9c1b98c281c776afbc23088453cdfb674fa7
4
+ data.tar.gz: 7320a09f2ab68192b2b93108257ceb1ceb0156a7024551576ee715088f3c2054
5
5
  SHA512:
6
- metadata.gz: 4e98417f1d4a0d74eeea613d876eca5b39dd5e12337255c08ba3598cde50731fce8533eaddd4b3bde26a20e84baa4674eff409a8b0564f4f539a2f23d0a0534b
7
- data.tar.gz: ae509fd46287e51ab6a78ff32fbcfe69ca5c7841abc57f37e6f1a822ddb2d42a627687ca1ea30ff26c7bbbd06c2acb93f431aac3af8d932707c2030ebf73cb4d
6
+ metadata.gz: e06612d23f1a19e70ec66d131f45fa8da746fef7ba5c06e264cc3e5951124dbc1c779d0b56ccfbe0d0e977281ab26b70b232759af6380da97942fbcda130e6c9
7
+ data.tar.gz: d3785758f29b77f00aec33fc7a812f57f3630efea7fa520dcd74b687a3ea631c74a8bed28623ff91f878c8defb00c55255c8ede092e3a15d1850055443cf744d
data/CHANGELOG.md CHANGED
@@ -1,9 +1,22 @@
1
1
  ## Changelog
2
2
 
3
- ### 0.9.1 (Next)
4
-
5
- * Your contribution here.
6
-
3
+ ### 0.10.1 (2026-10-23)
4
+
5
+ * [#85](https://github.com/dblock/danger-changelog/pull/85): Restore validation of entries with multiple leading issue or pull request links separated by commas or colons - [@dblock](https://github.com/dblock).
6
+ ### 0.10.0 (2026-10-21)
7
+
8
+ * [#84](https://github.com/dblock/danger-changelog/pull/84): Add global and per-check unreleased label lists for exact, case-sensitive header validation while preserving permissive defaults - [@dblock](https://github.com/dblock).
9
+ * [#82](https://github.com/dblock/danger-changelog/pull/82): Add opt-in intent-word checks with a configurable dictionary, consistent forms within each verb family, and full-file or added-only validation - [@dblock](https://github.com/dblock).
10
+ * [#83](https://github.com/dblock/danger-changelog/pull/83): Add global and per-check only_added scope for format diagnostics while keeping full-file validation as the default and file-wide requirements unconditional - [@dblock](https://github.com/dblock).
11
+ * [#81](https://github.com/dblock/danger-changelog/pull/81): Require added changelog entries to belong to an unreleased section by default, with global and per-check heading overrides and opt-out - [@dblock](https://github.com/dblock).
12
+ * [#80](https://github.com/dblock/danger-changelog/pull/80): Keep invalid-line reports fenced correctly when changelog text contains backticks and verify consecutive Keep a Changelog error rendering - [@dblock](https://github.com/dblock).
13
+ * [#79](https://github.com/dblock/danger-changelog/pull/79): Add global and per-check strict_semver settings to allow opt-in numeric versions with two or more components in both changelog formats - [@dblock](https://github.com/dblock).
14
+ * [#78](https://github.com/dblock/danger-changelog/pull/78): Support GitLab.com merge request and author links and generate reminders from GitLab metadata without requiring GitHub - [@dblock](https://github.com/dblock).
15
+ * [#77](https://github.com/dblock/danger-changelog/pull/77): Support Markdown reference-style PR and author links in Intridea changelogs, reporting undefined references and preserving original entries in diagnostics - [@dblock](https://github.com/dblock).
16
+ * [#76](https://github.com/dblock/danger-changelog/pull/76): Allow uncased scripts and emoji at the start of Intridea descriptions while retaining capitalization rules for cased letters - [@dblock](https://github.com/dblock).
17
+ * [#75](https://github.com/dblock/danger-changelog/pull/75): Support all global settings as isolated per-check keyword overrides, including placeholder validation and parser selection - [@dblock](https://github.com/dblock).
18
+ * [#74](https://github.com/dblock/danger-changelog/pull/74): Explain missing colons after pull request links, separate suggested entries with a heading, and document conditional changelog reminders - [@dblock](https://github.com/dblock).
19
+ * [#73](https://github.com/dblock/danger-changelog/pull/73): Require Ruby 3.1, test through Ruby 4.0 with compatible ActiveSupport versions, modernize pinned tokenless workflows, and restrict packaged files - [@dblock](https://github.com/dblock).
7
20
  ### 0.9.0 (2026/10/14)
8
21
 
9
22
  * [#72](https://github.com/dblock/danger-changelog/pull/72): Accept bot author links with a `[bot]` suffix without changing their attribution - [@dblock](https://github.com/dblock).
data/README.md CHANGED
@@ -1,7 +1,6 @@
1
1
  # Danger-Changelog
2
2
 
3
- A plugin for [danger.systems](http://danger.systems) that obsessive-compulsively lints your project’s `CHANGELOG.md`.
4
- It can make sure, for example, that changes are attributed properly, have a valid version number, a date in the ISO8601 format, balanced parenthesis and brackets, and that they’re always terminated with a period.
3
+ A plugin for [danger.systems](https://danger.systems) that obsessive-compulsively lints your project’s `CHANGELOG.md`. It can make sure, for example, that changes are attributed properly, have a valid version number, a date in the ISO8601 format, balanced parenthesis and brackets, and that they’re always terminated with a period.
5
4
 
6
5
  [![Gem Version](https://badge.fury.io/rb/danger-changelog.svg)](https://badge.fury.io/rb/danger-changelog)
7
6
  [![Build Status](https://github.com/dblock/danger-changelog/actions/workflows/test.yml/badge.svg?branch=master&event=push)](https://github.com/dblock/danger-changelog/actions/workflows/test.yml)
@@ -16,6 +15,12 @@ It can make sure, for example, that changes are attributed properly, have a vali
16
15
  - [changelog.format](#changelogformat)
17
16
  - [changelog.placeholder_line](#changelogplaceholder_line)
18
17
  - [changelog.ignore_files](#changelogignore_files)
18
+ - [changelog.strict_semver](#changelogstrict_semver)
19
+ - [changelog.unreleased_labels](#changelogunreleased_labels)
20
+ - [changelog.unreleased_heading](#changelogunreleased_heading)
21
+ - [changelog.only_added](#changelogonly_added)
22
+ - [changelog.check_intent and changelog.intent_words](#changelogcheck_intent-and-changelogintent_words)
23
+ - [Conditional reminders](#conditional-reminders)
19
24
  - [Checks](#checks)
20
25
  - [changelog.check!](#changelogcheck)
21
26
  - [changelog.have_you_updated_changelog?](#changeloghave_you_updated_changelog)
@@ -27,12 +32,16 @@ It can make sure, for example, that changes are attributed properly, have a vali
27
32
 
28
33
  By design, `danger-changelog` is quite strict with what it allows as a valid changelog file, using the [Intridea style](doc/intridea.md), [used by this library itself](CHANGELOG.md). It also supports the [Keep a Changelog](doc/keep_a_changelog.md) format.
29
34
 
35
+ Intridea PR and author links can be inline links or [Markdown references](doc/intridea.md#changelog-entries), with definitions elsewhere in the file. Undefined references are reported without changing the original entry shown in the report.
36
+
37
+ Entries can begin with multiple inline issue or pull request links separated by `, ` or `: `, followed by `: ` and the description.
38
+
30
39
  ## Installation
31
40
 
32
- Add `danger-changelog` to your Gemfile.
41
+ Requires Ruby 3.1 or newer. Add `danger-changelog` to your Gemfile.
33
42
 
34
43
  ```
35
- gem 'danger-changelog', '~> 0.6.0'
44
+ gem 'danger-changelog'
36
45
  ```
37
46
 
38
47
  Call `changelog.check!` from your `Dangerfile`. Make a pull request and see this plugin in action.
@@ -43,7 +52,16 @@ Methods and attributes from this plugin are available in your `Dangerfile` under
43
52
 
44
53
  ## Configuration
45
54
 
46
- The following options and checks are supported.
55
+ Every option can be set globally on `changelog` or passed as a keyword to a check. Per-check keywords override global defaults only for that call; parsing and reports use an immutable snapshot. Unknown options and invalid values raise `ArgumentError`.
56
+
57
+ ```ruby
58
+ changelog.ignore_files = ['README.md', /\.txt$/]
59
+ changelog.check!
60
+ changelog.check!(filename: 'packages/api/CHANGELOG.md')
61
+ changelog.check!(filename: 'CHANGES.md', format: :keep_a_changelog)
62
+ ```
63
+
64
+ Global strings and arrays are copied and frozen; assign a new value rather than modifying a setting in place. The deprecated `Danger::Changelog.configure` block still sets global defaults.
47
65
 
48
66
  ### changelog.filename
49
67
 
@@ -51,6 +69,7 @@ Set the CHANGELOG file name, defaults to `CHANGELOG.md`.
51
69
 
52
70
  ```ruby
53
71
  changelog.filename = 'CHANGES.md'
72
+ changelog.check!(filename: 'CHANGES.md')
54
73
  ```
55
74
 
56
75
  ### changelog.format
@@ -59,9 +78,10 @@ Set the format of the CHANGELOG file.
59
78
 
60
79
  ```ruby
61
80
  changelog.format = :keep_a_changelog
81
+ changelog.check!(format: :keep_a_changelog)
62
82
  ```
63
83
 
64
- Available formats are [Intridea](doc/intridea.md) (default) and [Keep a Changelog](doc/keep_a_changelog.md).
84
+ Available formats are [Intridea](doc/intridea.md) (default) and [Keep a Changelog](doc/keep_a_changelog.md). Symbol and string format names are accepted.
65
85
 
66
86
  ### changelog.placeholder_line
67
87
 
@@ -69,8 +89,11 @@ Customize the `* Your contribution here.` line. Set the value to `nil` to stop c
69
89
 
70
90
  ```ruby
71
91
  changelog.placeholder_line = "* Your change here.\n"
92
+ changelog.check!(placeholder_line: 'Your change here.')
72
93
  ```
73
94
 
95
+ The leading `* ` and trailing newline are added when omitted. This option applies to the Intridea parser; Keep a Changelog does not require a placeholder.
96
+
74
97
  ### changelog.ignore_files
75
98
 
76
99
  Ignore additions and changes with a certain name or expression, default is to ignore `README.md` changes.
@@ -79,26 +102,138 @@ For example, ignore `UPGRADING.md` and all `.txt` files.
79
102
 
80
103
  ```ruby
81
104
  changelog.ignore_files = ['README.md', 'UPGRADING.md', /\.txt$/]
105
+ changelog.check!(ignore_files: ['README.md', 'UPGRADING.md', /\.txt$/])
106
+ ```
107
+
108
+ ### changelog.strict_semver
109
+
110
+ Require three-component Semantic Versions by default. Set `strict_semver: false` to accept numeric versions with two or more components, such as `2.0` or `1.2.3.4`, with optional prerelease/build suffixes. Date, delimiter, and other formatting checks remain active. This applies to both supported formats.
111
+
112
+ ```ruby
113
+ changelog.strict_semver = false
114
+ changelog.check!(filename: 'CHANGES.md', strict_semver: false)
115
+ ```
116
+
117
+ ### changelog.unreleased_labels
118
+
119
+ By default (`nil`), header validation accepts existing label spellings such as `Next`, `next`, `Unreleased`, and project-specific words. Supply a list to replace that permissive behavior with exact, case-sensitive matching in both formats:
120
+
121
+ ```ruby
122
+ changelog.unreleased_labels = ['Next']
123
+ changelog.check! # Accepts 0.4.0 (Next), rejects 0.4.0 (next).
124
+ changelog.is_changelog_format_correct?(unreleased_labels: ['Next', 'Unreleased'])
125
+ changelog.check!(unreleased_labels: nil) # Restore permissive validation for this call.
82
126
  ```
83
127
 
128
+ The list restricts labels following a version (with or without delimiters) and standalone labels enclosed in parentheses, brackets, or braces, such as `[Unreleased]`. Bare versions, release dates, and ordinary unwrapped titles such as `Changelog` and `Changed:` remain unaffected. Labels are literal strings, not regular expressions; custom labels may contain spaces and punctuation. Each must be nonempty, have no leading/trailing whitespace, and contain no newlines or enclosing delimiter characters. An empty list rejects all labels while still allowing dated and bare versions.
129
+
130
+ This option validates header spelling, not entry placement. Configure `unreleased_heading` separately if you adopt a custom heading convention; setting the label list does not change its pattern.
131
+
132
+ ### changelog.unreleased_heading
133
+
134
+ By default, added list entries must belong to an unreleased section. The default heading pattern, `/(?:\(Next\)|\[Unreleased\])/i`, recognizes Intridea's `(Next)` and Keep a Changelog's `[Unreleased]`, case-insensitively. This global or per-check setting applies to both formats and is enforced by `check!` after its existing checks pass:
135
+
136
+ ```ruby
137
+ changelog.unreleased_heading = /\(Next\)/
138
+ changelog.check!
139
+ changelog.check!(format: :keep_a_changelog, unreleased_heading: /\[Unreleased\]/i)
140
+ changelog.check!(unreleased_heading: nil) # Allow intentional historical corrections.
141
+ ```
142
+
143
+ Only added `*` or `-` list lines in Danger's changelog diff are checked. Nested category headings stay inside the release; the next heading at the same or shallower level ends it. Unchanged historical entries are ignored. Historical corrections appear as additions too; use `unreleased_heading: nil` for those PRs and release-preparation changes. Customize the pattern for your project's heading convention.
144
+
145
+ Call `changelog.entries_in_unreleased_section?(unreleased_heading: /\(Next\)/)` to run this check separately. Missing diff data produces an explicit error rather than silently skipping the enabled check.
146
+
147
+ ### changelog.only_added
148
+
149
+ Format validation and enabled intent validation check the entire file by default (`only_added: false`). Set `only_added: true` globally or per check to report line-level format problems and compare intent words only on added lines in Danger's diff. Edited historical entries count as additions. Headers, punctuation, capitalization, attribution, and reference diagnostics follow this scope; reference definitions elsewhere in the file remain available for resolution.
150
+
151
+ ```ruby
152
+ changelog.only_added = true
153
+ changelog.check!
154
+ changelog.is_changelog_format_correct?(only_added: false)
155
+ ```
156
+
157
+ Missing files, required placeholders, and Keep a Changelog preamble requirements always apply to the whole file. Missing diff data is an explicit failure when added-only checking is requested for a changed changelog. An unchanged file has no added-line diagnostics but still receives global checks.
158
+
159
+ Release-section placement always checks additions only, regardless of `only_added`; checking released history against an unreleased section would incorrectly reject existing releases. Use `unreleased_heading: nil` to disable placement validation.
160
+
161
+ ### changelog.check_intent and changelog.intent_words
162
+
163
+ Opt into a finite dictionary of intent words and consistent wording within each verb family with `check_intent: true`. This rule is disabled by default and works with both formats. Once enabled, it checks the entire file by default, skipping the configured placeholder and fenced code examples. Set `only_added: true` to compare only added entries, excluding unchanged history. Missing diff data is an error only when added-only checking is requested for a changed changelog.
164
+
165
+ ```ruby
166
+ changelog.check_intent = true
167
+ changelog.check!
168
+ changelog.check!(check_intent: true, intent_words: %w[Add Fix Remove])
169
+ changelog.intent_correct?(check_intent: true)
170
+ ```
171
+
172
+ The default verb families are **Add, Allow, Change, Clarify, Deprecate, Disable, Document, Enable, Fix, Improve, Optimize, Refactor, Remove, Rename, Replace, Revert, Simplify, Support, Update, Upgrade**, including their past, third-person present, and gerund forms (for example, `Add`, `Added`, `Adds`, `Adding`; `Fix`, `Fixed`, `Fixes`, `Fixing`). Any form is acceptable on its own, but mixing forms within a family fails: `Add` with `Added` fails, while repeated `Added` entries pass. Each family is independent, so `Added` with `Fix` passes. Diagnostics ask later entries to match the first form used among the checked entries and include its line number. Unknown words show the allowed dictionary.
173
+
174
+ `intent_words` replaces the entire dictionary and must be a nonempty array of single words. Custom words are accepted exactly as listed, without automatic inflection; known forms still belong to their built-in verb family for consistency checks. Matching is case-sensitive, supports Unicode letters, and checks the first word after an optional PR reference or inline link. Setting a dictionary alone does not enable the rule. Projects with non-English prose, emoji-first entries, or inline-code-first descriptions should leave it disabled or adopt a compatible custom word list. Existing format validation remains independent; a custom dictionary does not relax capitalization rules.
175
+
176
+ ### Conditional reminders
177
+
178
+ By default, only `README.md` is ignored. To skip reminders when all added or modified files are documentation at the repository root, use an anchored pattern. Subdirectory files still require a changelog update.
179
+
180
+ ```ruby
181
+ changelog.ignore_files = [/\A[^\/]+\.md\z/]
182
+ changelog.check!
183
+ ```
184
+
185
+ For code-only reminders, use Danger's changed files directly. Keep format validation unconditional, so an edited changelog is checked even when no code changes:
186
+
187
+ ```ruby
188
+ changed_files = git.modified_files + git.added_files + git.deleted_files
189
+ changelog.have_you_updated_changelog? if changed_files.any? { |file| file.start_with?('lib/') }
190
+ changelog.is_changelog_format_correct?
191
+ ```
192
+
193
+ Other policies can use the same separate checks, such as reminding only for changes larger than 50 lines unless the pull request is marked trivial:
194
+
195
+ ```ruby
196
+ declared_trivial = github.pr_labels.include?('trivial')
197
+ changelog.have_you_updated_changelog? if git.lines_of_code > 50 && !declared_trivial
198
+ changelog.is_changelog_format_correct?
199
+ ```
200
+
201
+ The label example uses the GitHub plugin; adapt the condition to your provider. Do not also call `check!` in these conditional examples, because it would run the reminder unconditionally.
202
+
84
203
  ## Checks
85
204
 
86
- Invoke check methods.
205
+ All checks accept the same configuration keywords: `filename`, `format`, `placeholder_line`, `ignore_files`, `strict_semver`, `unreleased_labels`, `unreleased_heading`, `only_added`, `check_intent`, and `intent_words`. Each uses only the relevant settings.
87
206
 
88
207
  ### changelog.check!
89
208
 
90
- Run all checks with defaults, including `have_you_updated_changelog?` and `is_changelog_format_correct?`.
209
+ Run all checks with defaults, including `have_you_updated_changelog?`, `is_changelog_format_correct?`, and `entries_in_unreleased_section?`. When enabled, intent validation runs after those checks pass.
210
+
211
+ ```ruby
212
+ changelog.check!(filename: 'CHANGES.md', placeholder_line: nil)
213
+ ```
214
+
215
+ As before, format validation runs only if the update reminder passes. The deprecated `check` also accepts keyword overrides and its legacy positional format, without modifying global defaults.
91
216
 
92
217
  #### changelog.have_you_updated_changelog?
93
218
 
94
219
  Checks whether you have updated CHANGELOG.md.
95
220
 
221
+ ```ruby
222
+ changelog.have_you_updated_changelog?(filename: 'CHANGES.md', ignore_files: [/\.md$/])
223
+ ```
224
+
96
225
  ![](images/have_you_updated_changelog.png)
97
226
 
98
227
  #### changelog.is_changelog_format_correct?
99
228
 
100
229
  Checks whether the CHANGELOG format is correct.
101
230
 
231
+ ```ruby
232
+ changelog.is_changelog_format_correct?(filename: 'CHANGES.md', format: :keep_a_changelog)
233
+ ```
234
+
235
+ The change predicates `changelog_changes?` and `file_changes?` accept the same keywords, for example `changelog.changelog_changes?(filename: 'CHANGES.md')`.
236
+
102
237
  ![](images/is_changelog_format_correct.png)
103
238
 
104
239
  ## Contributing
data/doc/intridea.md CHANGED
@@ -92,8 +92,19 @@ The Intridea style changelog was pioneered on the Hashie project. It has many el
92
92
  # Not valid (2019/12/31)
93
93
  ```
94
94
 
95
+ ### Unreleased labels
96
+
97
+ Version headers can use labels such as `## 0.4.0 (Next)` or `## 0.4.0 (next)` by default. To enforce one spelling, set `changelog.unreleased_labels = ['Next']` or pass `unreleased_labels: ['Next']` to a check. This replaces permissive label matching with an exact, case-sensitive list and also applies to standalone delimited labels such as `## [Next]`. Dates, bare versions, and ordinary unwrapped titles are unaffected. The default `nil` preserves existing behavior; `[]` allows no labels. See [configuration](../README.md#changelogunreleased_labels).
98
+
95
99
  ### Changelog entries
96
100
 
101
+ Multiple inline issue or pull request links can precede a description, separated by `, ` or `: `. The final link must be followed by `: ` and the description.
102
+
103
+ ```markdown
104
+ * [#1](https://github.com/owner/repo/pull/1), [#2](https://github.com/owner/repo/issues/2): Fix issue - [@contributor](https://github.com/contributor).
105
+ * [#1](https://github.com/owner/repo/pull/1): [#2](https://github.com/owner/repo/issues/2): Fix issue - [@contributor](https://github.com/contributor).
106
+ ```
107
+
97
108
  ✅ Under each title, you can have changelog entries in the format: `* {optional link to GitHub pull request that must be followed by a colon} {a single sentence of text description} - {author github link}.`:
98
109
 
99
110
  ```markdown
@@ -105,12 +116,35 @@ The Intridea style changelog was pioneered on the Hashie project. It has many el
105
116
 
106
117
  Bot author names may end in a literal `[bot]` suffix, such as `github-actions[bot]` or `dependabot[bot]`. Keep the suffix in the attribution; links to GitHub app pages and bot profiles are supported.
107
118
 
108
- ❌ Entry descriptions need to start with an uppercase A-Z letter
119
+ ✅ Entry descriptions can start with uppercase letters, uncased scripts (such as Japanese, Chinese, or Arabic), emoji, or inline code
120
+
121
+ ```markdown
122
+ * 日本語の変更 - [@ivoanjo](https://github.com/ivoanjo).
123
+ * 修复问题 - [@ivoanjo](https://github.com/ivoanjo).
124
+ * 🚀 Add a feature - [@ivoanjo](https://github.com/ivoanjo).
125
+ ```
126
+
127
+ ✅ Pull request and author links may use Markdown references: full (`[@dblock][author]`), collapsed (`[@dblock][]`), or shortcut (`[@dblock]`)
128
+
129
+ ```markdown
130
+ * [#1]: Fix an issue - [@dblock].
131
+ * [#2][second-pr]: Add a feature - [@dblock][author].
132
+
133
+ [#1]: https://github.com/dblock/danger-changelog/pull/1
134
+ [second-pr]: https://github.com/dblock/danger-changelog/pull/2
135
+ [@dblock]: https://github.com/dblock
136
+ [author]: https://github.com/dblock
137
+ ```
138
+
139
+ Definitions may appear before or after entries. Labels are case-insensitive and consecutive whitespace is collapsed; the first definition wins when a label is repeated. Angle-bracket destinations and optional titles are accepted, including an indented destination or title on the following line. Inline links and reference links can be mixed. Bot links can use a named reference such as `[@dependabot[bot]][bot]`.
140
+
141
+ Undefined references are reported explicitly. Resolved destinations must satisfy the same GitHub URL rules as inline links. Reports show the original entry, not its expanded form. Reference expansion applies to PR and author links, not arbitrary references or inline-code examples inside the description.
142
+
143
+ ❌ When a description starts with a cased letter, that letter must be uppercase
109
144
 
110
145
  ```markdown
111
146
  * not valid - [@ivoanjo](https://github.com/ivoanjo).
112
- * いいえ - [@ivoanjo](https://github.com/ivoanjo).
113
- * ❌ - [@ivoanjo](https://github.com/ivoanjo).
147
+ * écriture en minuscule - [@ivoanjo](https://github.com/ivoanjo).
114
148
  * [#1](https://github.com/dblock/danger-changelog/pull/1): not valid - [@dblock](https://github.com/dblock).
115
149
  ```
116
150
 
@@ -120,10 +154,18 @@ Bot author names may end in a literal `[bot]` suffix, such as `github-actions[bo
120
154
  * [#1](https://github.com/dblock/danger-changelog/pull/1) Not valid - [@dblock](https://github.com/dblock).
121
155
  ```
122
156
 
123
- ❌ Only links to GitHub are allowed, and only using `https`
157
+ ✅ GitHub pull requests and GitLab.com merge requests and author profiles are supported, including GitLab subgroups
158
+
159
+ ```markdown
160
+ * [#1](https://gitlab.com/group/subgroup/project/-/merge_requests/1): Fix an issue - [@contributor](https://gitlab.com/contributor).
161
+ ```
162
+
163
+ GitLab reminders use merge-request metadata for their suggested entries. Self-managed GitLab hosts are not currently accepted by the Intridea URL rules; Keep a Changelog accepts arbitrary link destinations.
164
+
165
+ ❌ Links must use supported hosts and `https`
124
166
 
125
167
  ```markdown
126
- * No GitLab - [@ivoanjo](https://gitlab.com/ivoanjo).
168
+ * Unsupported host - [@ivoanjo](https://example.com/ivoanjo).
127
169
  * No insecure http - [@ivoanjo](http://github.com/ivoanjo).
128
170
  * [#1](https://you-get-the-point.com): Entry with a PR link - [@dblock](https://github.com/dblock).
129
171
  ```
@@ -2,6 +2,8 @@
2
2
 
3
3
  The Keep a Changelog style format checks that your changelog is formatted like the specification outlined at [KeepAChangelog.com](https://keepachangelog.com).
4
4
 
5
+ Header labels are permissive by default. Set `changelog.unreleased_labels = ['Unreleased']` or pass `unreleased_labels: ['Unreleased']` to a check to require that exact spelling in `## [Unreleased]`; lowercase `[unreleased]` then fails. The list also restricts labels following version numbers but does not restrict dates, bare versions, the `Changelog` title, or category headings. The default `nil` preserves existing behavior; `[]` allows no labels. This does not change the separate entry-placement pattern, `unreleased_heading`. See [configuration](../README.md#changelogunreleased_labels).
6
+
5
7
  As an example, here is a changelog formatted in the Keep a Changelog style, as borrowed from the Keep a Changelog project:
6
8
 
7
9
  ```markdown
@@ -0,0 +1,19 @@
1
+ module Danger
2
+ module Changelog
3
+ module AddedLines
4
+ def self.from_patch(patch)
5
+ number = nil
6
+ added = []
7
+ patch.each_line do |line|
8
+ if (hunk = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/))
9
+ number = hunk[1].to_i
10
+ elsif number && line.start_with?('+', ' ')
11
+ added << number if line.start_with?('+')
12
+ number += 1
13
+ end
14
+ end
15
+ added
16
+ end
17
+ end
18
+ end
19
+ end
@@ -4,8 +4,12 @@ module Danger
4
4
  module Changelog
5
5
  # A CHANGELOG.md line represents the change entry.
6
6
  class ChangelogEntryLine < ChangelogLine
7
- AUTHOR_NAME = /[\w-]+(?:\[bot\])?/.freeze
8
- AUTHOR_LINK = %r{\[@#{AUTHOR_NAME}\]\(https://github\.com/.*#{AUTHOR_NAME}\)}.freeze
7
+ AUTHOR_NAME = /[\w-]+(?:\[bot\])?/
8
+ AUTHOR_LINK = %r{\[@#{AUTHOR_NAME}\]\(https://(?:github\.com/.*#{AUTHOR_NAME}|gitlab\.com/#{AUTHOR_NAME})\)}
9
+ REQUEST_URL = %r{(?:github\.com/[^\s)]+?\d+|gitlab\.com/[^\s)]+/-/merge_requests/\d+)}
10
+ PR_LINK = %r{\[\#\d+\]\(https?://#{REQUEST_URL}/?\)}
11
+ REQUEST_LINKS = %r{\[\#\d+\]\(https://#{REQUEST_URL}\)(?:[,:] \[\#\d+\]\(https://#{REQUEST_URL}\))*}
12
+ DESCRIPTION_START = /[`\p{Lu}\p{Lt}\p{Lo}\p{Lm}\p{So}]/
9
13
 
10
14
  def valid?
11
15
  return validation_result.valid? if validation_result
@@ -15,18 +19,19 @@ module Danger
15
19
  validation_result.error! 'too many parenthesis' unless balanced?(line)
16
20
  return false if validation_result.invalid?
17
21
 
18
- return true if line =~ /^\*\s[`[:upper:]].*[^.,] - #{AUTHOR_LINK}.$/
19
- return true if line =~ %r{^\*\s\[\#\d+\]\(https://github\.com/.*\d+\): [`[:upper:]].*[^.,] - #{AUTHOR_LINK}.$}
22
+ return true if line =~ /^\*\s#{DESCRIPTION_START}(?:.*[^.,])? - #{AUTHOR_LINK}.$/
23
+ return true if line =~ /^\*\s#{REQUEST_LINKS}: #{DESCRIPTION_START}(?:.*[^.,])? - #{AUTHOR_LINK}.$/
20
24
 
21
25
  validation_result.error! 'does not start with a star' unless ChangelogEntryLine.starts_with_star?(line)
22
26
  validation_result.error! 'does not include a pull request link' unless ChangelogEntryLine.with_pr_link?(line)
27
+ validation_result.error! 'is missing a colon after the pull request link' if ChangelogEntryLine.with_pr_link?(line) && !/#{PR_LINK}:/.match?(line)
23
28
  validation_result.error! 'does not have a description' unless ChangelogEntryLine.with_changelog_description?(line)
24
29
  validation_result.error! 'does not include an author link' unless ChangelogEntryLine.with_author_link?(line)
25
30
  validation_result.error! 'has an extra trailing space' if ChangelogEntryLine.ends_with_space?(line)
26
31
  validation_result.error! 'is missing a period at the end of the line' unless ChangelogEntryLine.ends_with_period?(line)
27
32
  validation_result.error! 'has an extra period or comma at the end of the description' if
28
- line =~ /^\*\s[`[:upper:]].*[.,] - #{AUTHOR_LINK}.$/ ||
29
- line =~ %r{^\*\s\[\#\d+\]\(https://github\.com/.*\d+\): [`[:upper:]].*[.,] - #{AUTHOR_LINK}.$}
33
+ line =~ /^\*\s#{DESCRIPTION_START}.*[.,] - #{AUTHOR_LINK}.$/ ||
34
+ line =~ /^\*\s#{REQUEST_LINKS}: #{DESCRIPTION_START}.*[.,] - #{AUTHOR_LINK}.$/
30
35
 
31
36
  false
32
37
  end
@@ -48,7 +53,7 @@ module Danger
48
53
  .sub(/[?.!,;]?$/, '')
49
54
  .capitalize
50
55
  pr_author = github.pr_author
51
- pr_author_url = "https://github.com/#{github.pr_author}"
56
+ pr_author_url = github.is_a?(PRMetadata) ? github.author_url : "https://github.com/#{github.pr_author}"
52
57
  "* [##{pr_number}](#{pr_url}): #{pr_title} - [@#{pr_author}](#{pr_author_url})."
53
58
  end
54
59
 
@@ -75,21 +80,21 @@ module Danger
75
80
 
76
81
  # checks whether line contains a MARKDOWN link to a PR
77
82
  def self.with_pr_link?(line)
78
- return true if line =~ %r{\[\#\d+\]\(https?://github\.com/.*\d+/?\)}
83
+ return true if PR_LINK.match?(line)
79
84
 
80
85
  false
81
86
  end
82
87
 
83
- # checks whether line contains a capitalized Text, treated as a description
88
+ # Checks for a description starting with uppercase or uncased text, code, or a symbol.
84
89
  def self.with_changelog_description?(line)
85
- return true if line =~ /[`[:upper:]].*/
90
+ return true if DESCRIPTION_START.match?(line)
86
91
 
87
92
  false
88
93
  end
89
94
 
90
95
  # checks whether line contains a MARKDOWN link to an author
91
96
  def self.with_author_link?(line)
92
- return true if line =~ %r{\[@#{AUTHOR_NAME}\]\(https?://github\.com/.*#{AUTHOR_NAME}/?\)}
97
+ return true if line =~ %r{\[@#{AUTHOR_NAME}\]\(https?://(?:github\.com/.*#{AUTHOR_NAME}|gitlab\.com/#{AUTHOR_NAME})/?\)}
93
98
 
94
99
  false
95
100
  end
@@ -2,21 +2,23 @@ module Danger
2
2
  module Changelog
3
3
  # A CHANGELOG.md line represents the version header.
4
4
  class ChangelogHeaderLine < ChangelogLine
5
- OPEN_PARENS = /[(\[{]?/.freeze
6
- CLOSE_PARENS = /[)\]}]?/.freeze
5
+ OPEN_PARENS = /[(\[{]?/
6
+ CLOSE_PARENS = /[)\]}]?/
7
7
 
8
- HASHES = /\#{1,4}/.freeze
9
- SEMVER = /(?<semver>(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)/.freeze
10
- ISO8601_DATETIME = %r{(?<datetime>([0-9]{4})[-/]?(1[0-2]|0?[1-9])[-/]+(3[01]|0?[1-9]|[12][0-9])(T[0-9]{2}:[0-9]{2}:[0-9]{2})?Z?)}.freeze
8
+ HASHES = /\#{1,4}/
9
+ SEMVER = /(?<semver>(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)/
10
+ RELAXED_VERSION = /(?:0|[1-9]\d*)(?:\.(?:0|[1-9]\d*))+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?/
11
+ ISO8601_DATETIME = %r{(?<datetime>([0-9]{4})[-/]?(1[0-2]|0?[1-9])[-/]+(3[01]|0?[1-9]|[12][0-9])(T[0-9]{2}:[0-9]{2}:[0-9]{2})?Z?)}
11
12
 
12
13
  def valid?
13
14
  stripped_line = line.strip
15
+ version = config.strict_semver ? SEMVER : RELAXED_VERSION
14
16
 
15
- m = stripped_line.match(/^#{HASHES}\s#{OPEN_PARENS}[\w\s:]*#{CLOSE_PARENS}$/) # title
16
- m ||= stripped_line.match(/^#{HASHES}\s#{OPEN_PARENS}#{SEMVER}#{CLOSE_PARENS}$/) # semver only
17
- m ||= stripped_line.match(/^#{HASHES}\s#{OPEN_PARENS}#{SEMVER}#{CLOSE_PARENS}[\s-]+#{OPEN_PARENS}(#{ISO8601_DATETIME}|\w*)#{CLOSE_PARENS}$/) # semver and ISO 8601 date & time or next version description
17
+ m = title_valid?(stripped_line) # title
18
+ m ||= stripped_line.match(/^#{HASHES}\s#{OPEN_PARENS}#{version}#{CLOSE_PARENS}$/) # version only
19
+ m ||= stripped_line.match(/^#{HASHES}\s#{OPEN_PARENS}#{version}#{CLOSE_PARENS}[\s-]+#{OPEN_PARENS}(#{ISO8601_DATETIME}|#{labels})#{CLOSE_PARENS}$/) # version and date or next version description
18
20
 
19
- !m.nil? && balanced?(stripped_line)
21
+ !!m && balanced?(stripped_line)
20
22
  end
21
23
 
22
24
  def self.validates_as_changelog_line?(line)
@@ -24,6 +26,20 @@ module Danger
24
26
 
25
27
  false
26
28
  end
29
+
30
+ private
31
+
32
+ def labels
33
+ config.unreleased_labels ? Regexp.union(config.unreleased_labels) : /\w*/
34
+ end
35
+
36
+ def title_valid?(title)
37
+ if config.unreleased_labels && (match = title.match(/^#{HASHES}\s[(\[{](.*)[)\]}]$/))
38
+ config.unreleased_labels.include?(match[1])
39
+ else
40
+ title.match?(/^#{HASHES}\s#{OPEN_PARENS}[\w\s:]*#{CLOSE_PARENS}$/)
41
+ end
42
+ end
27
43
  end
28
44
  end
29
45
  end
@@ -2,13 +2,15 @@ module Danger
2
2
  module Changelog
3
3
  # An abstract CHANGELOG.md line.
4
4
  class ChangelogLine
5
- NON_DELIMITERS = /[^(){}\[\]]*/.freeze
6
- PAIRED = /\(#{NON_DELIMITERS}\)|\{#{NON_DELIMITERS}\}|\[#{NON_DELIMITERS}\]/.freeze
7
- DELIMITER = /[(){}\[\]]/.freeze
5
+ NON_DELIMITERS = /[^(){}\[\]]*/
6
+ PAIRED = /\(#{NON_DELIMITERS}\)|\{#{NON_DELIMITERS}\}|\[#{NON_DELIMITERS}\]/
7
+ DELIMITER = /[(){}\[\]]/
8
8
 
9
9
  attr_accessor :line, :validation_result
10
+ attr_reader :config
10
11
 
11
- def initialize(line)
12
+ def initialize(line, config: Danger::Changelog.config.with)
13
+ @config = config
12
14
  self.line = line
13
15
  self.validation_result = nil
14
16
  end
@@ -7,18 +7,20 @@ module Danger
7
7
  # A parser of the CHANGELOG.md lines
8
8
  class ChangelogLineParser
9
9
  # Returns an instance of Changelog::ChangelogLine class based on the given line
10
- def self.parse(line)
10
+ def self.parse(line, config: Danger::Changelog.config.with)
11
+ return ChangelogPlaceholderLine.new(line, config: config) if ChangelogPlaceholderLine.validates_as_changelog_line?(line, config: config)
12
+
11
13
  changelog_line_class = available_changelog_lines.find do |changelog_line|
12
14
  changelog_line.validates_as_changelog_line?(line)
13
15
  end
14
16
  return nil unless changelog_line_class
15
17
 
16
- changelog_line_class.new(line)
18
+ changelog_line_class.new(line, config: config)
17
19
  end
18
20
 
19
21
  def self.available_changelog_lines
20
22
  # Order is important
21
- [ChangelogPlaceholderLine, ChangelogEntryLine, ChangelogHeaderLine]
23
+ [ChangelogEntryLine, ChangelogHeaderLine]
22
24
  end
23
25
 
24
26
  private_class_method :available_changelog_lines
@@ -3,11 +3,11 @@ module Danger
3
3
  # A CHANGELOG.md line represents the "Your contribution here".
4
4
  class ChangelogPlaceholderLine < ChangelogLine
5
5
  def valid?
6
- ChangelogPlaceholderLine.validates_as_changelog_line?(line)
6
+ ChangelogPlaceholderLine.validates_as_changelog_line?(line, config: config)
7
7
  end
8
8
 
9
- def self.validates_as_changelog_line?(line)
10
- return true if line == Danger::Changelog.config.placeholder_line
9
+ def self.validates_as_changelog_line?(line, config: Danger::Changelog.config)
10
+ return true if line == config.placeholder_line
11
11
 
12
12
  false
13
13
  end