glab-release-notes 0.1.0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vladimir Voronov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,277 @@
1
+ Metadata-Version: 2.4
2
+ Name: glab-release-notes
3
+ Version: 0.1.0
4
+ Summary: GitLab release notes from merged MRs: validate labels and quote, append entries, and promote them under a version in CI.
5
+ Keywords: gitlab,release-notes,changelog,ci,merge-request
6
+ Author: Vladimir Voronov
7
+ Author-email: Vladimir Voronov <vvoronov@mwnts.ru>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Topic :: Software Development
18
+ Classifier: Topic :: Software Development :: Version Control
19
+ Classifier: Topic :: Utilities
20
+ Requires-Python: >=3.12
21
+ Project-URL: Homepage, https://github.com/redb0/glab-release-notes
22
+ Project-URL: Repository, https://github.com/redb0/glab-release-notes
23
+ Project-URL: Issues, https://github.com/redb0/glab-release-notes/issues
24
+ Project-URL: Changelog, https://github.com/redb0/glab-release-notes/releases
25
+ Description-Content-Type: text/markdown
26
+
27
+ # Glab Release Notes
28
+
29
+ Automatically add the changes from each merged merge request to human-readable release notes in a file.
30
+
31
+ This is a GitLab-oriented take on [latest-changes](https://github.com/tiangolo/latest-changes): a small, dependency-free CLI that runs in your GitLab CI and keeps a `release-notes.md` up to date as merge requests get merged.
32
+
33
+ Unlike tools that scrape `git log` or conventional-commit messages, it reads the data **from the merge request itself**:
34
+
35
+ * a **label** decides which section the entry lands in,
36
+ * a **one-line quote** (`> …`) in the MR description is the entry text,
37
+ * a **ticket key** in the source-branch name (e.g. `FMT-1110`) becomes an optional Jira link.
38
+
39
+ So the person writing the MR writes the one sentence that matters, and CI takes care of the formatting, links, author, and ordering.
40
+
41
+ ## How it works
42
+
43
+ A release notes file is plain Markdown with an `## Latest Changes` block at the top and versioned blocks below it:
44
+
45
+ ```Markdown
46
+ # Release notes
47
+
48
+ ## Latest Changes
49
+
50
+ ### Features
51
+
52
+ ### Fixes
53
+
54
+ ## 0.8.1
55
+
56
+ ### Fixes {#fix_0.8.1}
57
+
58
+ * 🐛 Fixed a crash on empty input ([#35](https://gitlab.example/your/project/-/merge_requests/35)) by [@alice](https://gitlab.example/alice)
59
+ ```
60
+
61
+ After a merge request is merged, the `append` command finds the `## Latest Changes` block and adds a new list item to the section chosen by the MR's label:
62
+
63
+ > ## Latest Changes
64
+ >
65
+ > ### Features
66
+ >
67
+ > * ✨ Add support for detailed gRPC errors ([#123](https://gitlab.example/your/project/-/merge_requests/123), [FMT-1110](https://jira.example/browse/FMT-1110)) by [@bob](https://gitlab.example/bob)
68
+
69
+ The entry is built from:
70
+
71
+ * the **label** → section and emoji (see [Using labels](#using-labels)),
72
+ * the **last non-empty `> …` quote** in the MR description → the entry text,
73
+ * the **ticket key** parsed from the source branch name → a Jira link (only when a `--jira-base` is given),
74
+ * the MR **author** and **URL** → trailing `by [@author](…)`.
75
+
76
+ The entry is prepended to the top of the section so the newest change reads first. Because the notes are just a file in your repo, you can later tweak, reorder, or extend any entry by hand.
77
+
78
+ ## Using labels
79
+
80
+ A merge request is routed to a section by its labels. The built-in default mapping is:
81
+
82
+ | Label | Section | Emoji |
83
+ | ----------- | --------------- | ----- |
84
+ | `feature` | `Features` | ✨ |
85
+ | `bug` | `Fixes` | 🐛 |
86
+ | `docs` | `Documentation` | 📝 |
87
+ | `ci` | `CI` | 👷 |
88
+
89
+ Labels that are never recorded into the notes:
90
+
91
+ * `release`, `refactor`, `test`, `chore`, `style`
92
+
93
+ Rules:
94
+
95
+ * A merge request must carry **either** a section label **or** a skip label.
96
+ * A **skip label wins** if both a section label and a skip label are present.
97
+ * With several section labels, the first one in the configured order wins.
98
+ * A section label **requires** a `> …` quote in the description; a skip label does not.
99
+
100
+ `Features` and `Fixes` always exist (even when empty). The other sections appear only once they have at least one entry.
101
+
102
+ ## Getting started
103
+
104
+ 1. **Add the notes file** (if you don't have one), e.g. `docs/release-notes.md`:
105
+
106
+ ```Markdown
107
+ # Release notes
108
+
109
+ ## Latest Changes
110
+
111
+ ### Features
112
+
113
+ ### Fixes
114
+ ```
115
+
116
+ 2. **Add a merge-request template** that asks authors to set a label and write a one-line quote. A minimal `Default.md`:
117
+
118
+ ```Markdown
119
+ /label ~feature
120
+
121
+ ## Release notes
122
+
123
+ One line in a quote. No emojis and no links — CI adds those after the merge.
124
+
125
+ >
126
+ ```
127
+
128
+ 3. **Add the CI jobs** (see [CI wiring](#ci-wiring)).
129
+
130
+ That's enough for `validate` and `append` to work. `promote` is what your release job calls.
131
+
132
+ ## Commands
133
+
134
+ ```
135
+ glab-release-notes [--config PATH] [--file PATH] <command>
136
+ ```
137
+
138
+ * `validate` — a gate for the MR pipeline. Fails the pipeline when the MR has neither a section label nor a skip label, or has a section label but no `> …` quote.
139
+ * `append` — after a merge, appends the entry to the `## Latest Changes` block and pushes a `chore(notes): add entry for MR !<iid>` commit. It retries on a push race and is idempotent: an already-recorded MR is skipped.
140
+ * `promote` — moves the non-empty sections of `## Latest Changes` under a new `## <version>` heading with anchors, then empties the block back to the always-kept sections. `--version` is required; `--no-push` rewrites the file only so your release process can own the commit.
141
+
142
+ ## CI wiring
143
+
144
+ The tool talks to the GitLab API using CI variables, so it is bound to whichever project's pipeline runs it:
145
+
146
+ * `CI_API_V4_URL`, `CI_PROJECT_ID`, `GITLAB_API_TOKEN` — API endpoint and auth;
147
+ * `CI_MERGE_REQUEST_IID` — used by `validate`;
148
+ * `CI_COMMIT_SHA` — used by `append` to find the MR of the current commit.
149
+
150
+ A typical `.gitlab-ci.yml`:
151
+
152
+ ```yaml
153
+ validate-release-notes:
154
+ stage: linter
155
+ script:
156
+ - uv run glab-release-notes validate
157
+ rules:
158
+ - if: $CI_PIPELINE_SOURCE == "merge_request_event"
159
+
160
+ append-release-notes:
161
+ stage: linter
162
+ variables:
163
+ GIT_DEPTH: 0
164
+ before_script:
165
+ - !reference [.base_git_pre_settings, script] # git credentials for the push
166
+ script:
167
+ - uv run glab-release-notes append --jira-base https://jira.example/browse
168
+ rules:
169
+ - if: $CI_COMMIT_TAG || $CI_PIPELINE_SOURCE == "merge_request_event"
170
+ when: never
171
+ - if: '$CI_COMMIT_MESSAGE =~ /^chore\(notes\)/'
172
+ when: never
173
+ - if: $CI_COMMIT_BRANCH == "main" && $CI_PIPELINE_SOURCE == "push"
174
+ ```
175
+
176
+ The `chore(notes)` rule prevents the job from re-running on its own commits, and `GIT_DEPTH: 0` gives `append` a full history. The `before_script` must configure git with credentials able to push to the target branch (see [Pushing to a protected branch](#pushing-to-a-protected-branch)).
177
+
178
+ ### Promoting on release
179
+
180
+ Call `promote` in your release job, right before (and, with `--no-push`, folded into) the release commit:
181
+
182
+ ```yaml
183
+ - uv run glab-release-notes promote --version "${version}" --no-push
184
+ ```
185
+
186
+ ## Existing MRs — running manually
187
+
188
+ `append` resolves the MR from `CI_COMMIT_SHA` by default. To record a specific merge request by hand — for example one that was merged before you added this — run `append` on the target branch with the `MR_IID` variable set:
189
+
190
+ ```
191
+ MR_IID=123 uv run glab-release-notes append --jira-base https://jira.example/browse
192
+ ```
193
+
194
+ This is also the escape hatch for any MR the automatic run skipped.
195
+
196
+ ## Configuration
197
+
198
+ The wording of the release notes — the file, the `Latest Changes` header, the sections, the labels, and the skip labels — is configurable, in the same spirit as [latest-changes](https://github.com/tiangolo/latest-changes). Put a `release-notes.toml` in the repo root and it is picked up automatically; pass `--config PATH` to point at a different file. Without a config file, the built-in defaults from [Using labels](#using-labels) apply.
199
+
200
+ Precedence: CLI flags (`--file`) > config file > built-in defaults.
201
+
202
+ ```toml
203
+ # release-notes.toml
204
+ file = "docs/release-notes.md" # the notes file (CLI --file wins)
205
+ unreleased_header = "## Latest Changes"
206
+ section_prefix = "### "
207
+ skip_labels = ["release", "refactor", "test", "chore", "style"]
208
+ always_keep = ["Features", "Fixes"]
209
+
210
+ [[section]]
211
+ label = "feature"
212
+ header = "Features"
213
+ emoji = "✨"
214
+ anchor_prefix = "feat"
215
+
216
+ [[section]]
217
+ label = "bug"
218
+ header = "Fixes"
219
+ emoji = "🐛"
220
+ anchor_prefix = "fix"
221
+
222
+ [[section]]
223
+ label = "docs"
224
+ header = "Documentation"
225
+ emoji = "📝"
226
+ anchor_prefix = "docs"
227
+
228
+ [[section]]
229
+ label = "ci"
230
+ header = "CI"
231
+ emoji = "👷"
232
+ anchor_prefix = "ci"
233
+ ```
234
+
235
+ Keys:
236
+
237
+ * `file` — the notes file to update. Defaults to `docs/release-notes.md`.
238
+ * `unreleased_header` — the header of the accumulating block. Defaults to `## Latest Changes`.
239
+ * `section_prefix` — the prefix of the section headers inside the block. Defaults to `### `.
240
+ * `skip_labels` — labels that are never recorded. A skip label also wins over a section label.
241
+ * `always_keep` — section headers that are always rendered, even when empty. Defaults to `["Features", "Fixes"]`; use `[]` to render a section only once it has an entry (the latest-changes behavior).
242
+ * `[[section]]` — one table per section:
243
+ * `label` (required) — the GitLab label that routes an MR to this section.
244
+ * `header` (required) — the section header text.
245
+ * `emoji` (optional) — the emoji prefixed to entries in this section. Omit it for a plain entry.
246
+ * `anchor_prefix` (optional) — the anchor prefix used when promoting, e.g. `{#feat_1.0.0}`. Defaults to the label.
247
+
248
+ The order of `[[section]]` tables matters in both directions: it is the display order of the sections, and the first matching label wins when an MR carries several section labels.
249
+
250
+ The entry line format itself is fixed: `* <emoji> <text> (links) by <author>`. It is not a template.
251
+
252
+ ## Pushing to a protected branch
253
+
254
+ `append` and `promote` (without `--no-push`) `git commit` and `git push` the updated notes. If your target branch is protected against direct pushes, the CI job must be running with credentials that can push to it.
255
+
256
+ In GitLab that usually means one of:
257
+
258
+ * the job runs as a user/role allowed to push to the branch, or
259
+ * you pass a token to the git setup (the `!reference [.base_git_pre_settings, script]` pattern above) that can push.
260
+
261
+ The commits are attributed to the CI identity, not to any individual author.
262
+
263
+ ## Development
264
+
265
+ ```
266
+ uv sync
267
+ uv run ruff check .
268
+ uv run ruff format --check .
269
+ uv run mypy glab_release_notes tests
270
+ uv run pytest
271
+ ```
272
+
273
+ Releases are managed with [commitizen](https://github.com/commitizen/commitizen): `uv run cz bump` updates the version and `CHANGELOG.md` and tags the commit. A GitHub Release on that tag publishes the package to PyPI via trusted publishing (`.github/workflows/publish.yaml`).
274
+
275
+ ## License
276
+
277
+ This project is licensed under the terms of the MIT license.
@@ -0,0 +1,251 @@
1
+ # Glab Release Notes
2
+
3
+ Automatically add the changes from each merged merge request to human-readable release notes in a file.
4
+
5
+ This is a GitLab-oriented take on [latest-changes](https://github.com/tiangolo/latest-changes): a small, dependency-free CLI that runs in your GitLab CI and keeps a `release-notes.md` up to date as merge requests get merged.
6
+
7
+ Unlike tools that scrape `git log` or conventional-commit messages, it reads the data **from the merge request itself**:
8
+
9
+ * a **label** decides which section the entry lands in,
10
+ * a **one-line quote** (`> …`) in the MR description is the entry text,
11
+ * a **ticket key** in the source-branch name (e.g. `FMT-1110`) becomes an optional Jira link.
12
+
13
+ So the person writing the MR writes the one sentence that matters, and CI takes care of the formatting, links, author, and ordering.
14
+
15
+ ## How it works
16
+
17
+ A release notes file is plain Markdown with an `## Latest Changes` block at the top and versioned blocks below it:
18
+
19
+ ```Markdown
20
+ # Release notes
21
+
22
+ ## Latest Changes
23
+
24
+ ### Features
25
+
26
+ ### Fixes
27
+
28
+ ## 0.8.1
29
+
30
+ ### Fixes {#fix_0.8.1}
31
+
32
+ * 🐛 Fixed a crash on empty input ([#35](https://gitlab.example/your/project/-/merge_requests/35)) by [@alice](https://gitlab.example/alice)
33
+ ```
34
+
35
+ After a merge request is merged, the `append` command finds the `## Latest Changes` block and adds a new list item to the section chosen by the MR's label:
36
+
37
+ > ## Latest Changes
38
+ >
39
+ > ### Features
40
+ >
41
+ > * ✨ Add support for detailed gRPC errors ([#123](https://gitlab.example/your/project/-/merge_requests/123), [FMT-1110](https://jira.example/browse/FMT-1110)) by [@bob](https://gitlab.example/bob)
42
+
43
+ The entry is built from:
44
+
45
+ * the **label** → section and emoji (see [Using labels](#using-labels)),
46
+ * the **last non-empty `> …` quote** in the MR description → the entry text,
47
+ * the **ticket key** parsed from the source branch name → a Jira link (only when a `--jira-base` is given),
48
+ * the MR **author** and **URL** → trailing `by [@author](…)`.
49
+
50
+ The entry is prepended to the top of the section so the newest change reads first. Because the notes are just a file in your repo, you can later tweak, reorder, or extend any entry by hand.
51
+
52
+ ## Using labels
53
+
54
+ A merge request is routed to a section by its labels. The built-in default mapping is:
55
+
56
+ | Label | Section | Emoji |
57
+ | ----------- | --------------- | ----- |
58
+ | `feature` | `Features` | ✨ |
59
+ | `bug` | `Fixes` | 🐛 |
60
+ | `docs` | `Documentation` | 📝 |
61
+ | `ci` | `CI` | 👷 |
62
+
63
+ Labels that are never recorded into the notes:
64
+
65
+ * `release`, `refactor`, `test`, `chore`, `style`
66
+
67
+ Rules:
68
+
69
+ * A merge request must carry **either** a section label **or** a skip label.
70
+ * A **skip label wins** if both a section label and a skip label are present.
71
+ * With several section labels, the first one in the configured order wins.
72
+ * A section label **requires** a `> …` quote in the description; a skip label does not.
73
+
74
+ `Features` and `Fixes` always exist (even when empty). The other sections appear only once they have at least one entry.
75
+
76
+ ## Getting started
77
+
78
+ 1. **Add the notes file** (if you don't have one), e.g. `docs/release-notes.md`:
79
+
80
+ ```Markdown
81
+ # Release notes
82
+
83
+ ## Latest Changes
84
+
85
+ ### Features
86
+
87
+ ### Fixes
88
+ ```
89
+
90
+ 2. **Add a merge-request template** that asks authors to set a label and write a one-line quote. A minimal `Default.md`:
91
+
92
+ ```Markdown
93
+ /label ~feature
94
+
95
+ ## Release notes
96
+
97
+ One line in a quote. No emojis and no links — CI adds those after the merge.
98
+
99
+ >
100
+ ```
101
+
102
+ 3. **Add the CI jobs** (see [CI wiring](#ci-wiring)).
103
+
104
+ That's enough for `validate` and `append` to work. `promote` is what your release job calls.
105
+
106
+ ## Commands
107
+
108
+ ```
109
+ glab-release-notes [--config PATH] [--file PATH] <command>
110
+ ```
111
+
112
+ * `validate` — a gate for the MR pipeline. Fails the pipeline when the MR has neither a section label nor a skip label, or has a section label but no `> …` quote.
113
+ * `append` — after a merge, appends the entry to the `## Latest Changes` block and pushes a `chore(notes): add entry for MR !<iid>` commit. It retries on a push race and is idempotent: an already-recorded MR is skipped.
114
+ * `promote` — moves the non-empty sections of `## Latest Changes` under a new `## <version>` heading with anchors, then empties the block back to the always-kept sections. `--version` is required; `--no-push` rewrites the file only so your release process can own the commit.
115
+
116
+ ## CI wiring
117
+
118
+ The tool talks to the GitLab API using CI variables, so it is bound to whichever project's pipeline runs it:
119
+
120
+ * `CI_API_V4_URL`, `CI_PROJECT_ID`, `GITLAB_API_TOKEN` — API endpoint and auth;
121
+ * `CI_MERGE_REQUEST_IID` — used by `validate`;
122
+ * `CI_COMMIT_SHA` — used by `append` to find the MR of the current commit.
123
+
124
+ A typical `.gitlab-ci.yml`:
125
+
126
+ ```yaml
127
+ validate-release-notes:
128
+ stage: linter
129
+ script:
130
+ - uv run glab-release-notes validate
131
+ rules:
132
+ - if: $CI_PIPELINE_SOURCE == "merge_request_event"
133
+
134
+ append-release-notes:
135
+ stage: linter
136
+ variables:
137
+ GIT_DEPTH: 0
138
+ before_script:
139
+ - !reference [.base_git_pre_settings, script] # git credentials for the push
140
+ script:
141
+ - uv run glab-release-notes append --jira-base https://jira.example/browse
142
+ rules:
143
+ - if: $CI_COMMIT_TAG || $CI_PIPELINE_SOURCE == "merge_request_event"
144
+ when: never
145
+ - if: '$CI_COMMIT_MESSAGE =~ /^chore\(notes\)/'
146
+ when: never
147
+ - if: $CI_COMMIT_BRANCH == "main" && $CI_PIPELINE_SOURCE == "push"
148
+ ```
149
+
150
+ The `chore(notes)` rule prevents the job from re-running on its own commits, and `GIT_DEPTH: 0` gives `append` a full history. The `before_script` must configure git with credentials able to push to the target branch (see [Pushing to a protected branch](#pushing-to-a-protected-branch)).
151
+
152
+ ### Promoting on release
153
+
154
+ Call `promote` in your release job, right before (and, with `--no-push`, folded into) the release commit:
155
+
156
+ ```yaml
157
+ - uv run glab-release-notes promote --version "${version}" --no-push
158
+ ```
159
+
160
+ ## Existing MRs — running manually
161
+
162
+ `append` resolves the MR from `CI_COMMIT_SHA` by default. To record a specific merge request by hand — for example one that was merged before you added this — run `append` on the target branch with the `MR_IID` variable set:
163
+
164
+ ```
165
+ MR_IID=123 uv run glab-release-notes append --jira-base https://jira.example/browse
166
+ ```
167
+
168
+ This is also the escape hatch for any MR the automatic run skipped.
169
+
170
+ ## Configuration
171
+
172
+ The wording of the release notes — the file, the `Latest Changes` header, the sections, the labels, and the skip labels — is configurable, in the same spirit as [latest-changes](https://github.com/tiangolo/latest-changes). Put a `release-notes.toml` in the repo root and it is picked up automatically; pass `--config PATH` to point at a different file. Without a config file, the built-in defaults from [Using labels](#using-labels) apply.
173
+
174
+ Precedence: CLI flags (`--file`) > config file > built-in defaults.
175
+
176
+ ```toml
177
+ # release-notes.toml
178
+ file = "docs/release-notes.md" # the notes file (CLI --file wins)
179
+ unreleased_header = "## Latest Changes"
180
+ section_prefix = "### "
181
+ skip_labels = ["release", "refactor", "test", "chore", "style"]
182
+ always_keep = ["Features", "Fixes"]
183
+
184
+ [[section]]
185
+ label = "feature"
186
+ header = "Features"
187
+ emoji = "✨"
188
+ anchor_prefix = "feat"
189
+
190
+ [[section]]
191
+ label = "bug"
192
+ header = "Fixes"
193
+ emoji = "🐛"
194
+ anchor_prefix = "fix"
195
+
196
+ [[section]]
197
+ label = "docs"
198
+ header = "Documentation"
199
+ emoji = "📝"
200
+ anchor_prefix = "docs"
201
+
202
+ [[section]]
203
+ label = "ci"
204
+ header = "CI"
205
+ emoji = "👷"
206
+ anchor_prefix = "ci"
207
+ ```
208
+
209
+ Keys:
210
+
211
+ * `file` — the notes file to update. Defaults to `docs/release-notes.md`.
212
+ * `unreleased_header` — the header of the accumulating block. Defaults to `## Latest Changes`.
213
+ * `section_prefix` — the prefix of the section headers inside the block. Defaults to `### `.
214
+ * `skip_labels` — labels that are never recorded. A skip label also wins over a section label.
215
+ * `always_keep` — section headers that are always rendered, even when empty. Defaults to `["Features", "Fixes"]`; use `[]` to render a section only once it has an entry (the latest-changes behavior).
216
+ * `[[section]]` — one table per section:
217
+ * `label` (required) — the GitLab label that routes an MR to this section.
218
+ * `header` (required) — the section header text.
219
+ * `emoji` (optional) — the emoji prefixed to entries in this section. Omit it for a plain entry.
220
+ * `anchor_prefix` (optional) — the anchor prefix used when promoting, e.g. `{#feat_1.0.0}`. Defaults to the label.
221
+
222
+ The order of `[[section]]` tables matters in both directions: it is the display order of the sections, and the first matching label wins when an MR carries several section labels.
223
+
224
+ The entry line format itself is fixed: `* <emoji> <text> (links) by <author>`. It is not a template.
225
+
226
+ ## Pushing to a protected branch
227
+
228
+ `append` and `promote` (without `--no-push`) `git commit` and `git push` the updated notes. If your target branch is protected against direct pushes, the CI job must be running with credentials that can push to it.
229
+
230
+ In GitLab that usually means one of:
231
+
232
+ * the job runs as a user/role allowed to push to the branch, or
233
+ * you pass a token to the git setup (the `!reference [.base_git_pre_settings, script]` pattern above) that can push.
234
+
235
+ The commits are attributed to the CI identity, not to any individual author.
236
+
237
+ ## Development
238
+
239
+ ```
240
+ uv sync
241
+ uv run ruff check .
242
+ uv run ruff format --check .
243
+ uv run mypy glab_release_notes tests
244
+ uv run pytest
245
+ ```
246
+
247
+ Releases are managed with [commitizen](https://github.com/commitizen/commitizen): `uv run cz bump` updates the version and `CHANGELOG.md` and tags the commit. A GitHub Release on that tag publishes the package to PyPI via trusted publishing (`.github/workflows/publish.yaml`).
248
+
249
+ ## License
250
+
251
+ This project is licensed under the terms of the MIT license.
@@ -0,0 +1,32 @@
1
+ """Release notes for GitLab projects, generated from merged merge requests."""
2
+
3
+ from glab_release_notes.config import NotesConfig, default_config, load_config
4
+ from glab_release_notes.edit import append_note, promote_notes
5
+ from glab_release_notes.models import LabelDecision, MergeRequest, ReleaseNotesError
6
+ from glab_release_notes.parse import (
7
+ already_recorded,
8
+ classify,
9
+ parse_quote,
10
+ parse_ticket,
11
+ validate_record,
12
+ )
13
+ from glab_release_notes.payload import merge_request_from_payload
14
+ from glab_release_notes.render import format_entry
15
+
16
+ __all__ = (
17
+ "LabelDecision",
18
+ "MergeRequest",
19
+ "NotesConfig",
20
+ "ReleaseNotesError",
21
+ "already_recorded",
22
+ "append_note",
23
+ "classify",
24
+ "default_config",
25
+ "format_entry",
26
+ "load_config",
27
+ "merge_request_from_payload",
28
+ "parse_quote",
29
+ "parse_ticket",
30
+ "promote_notes",
31
+ "validate_record",
32
+ )
@@ -0,0 +1,6 @@
1
+ """Entry point: ``python -m glab_release_notes``."""
2
+
3
+ from glab_release_notes.cli import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -0,0 +1,40 @@
1
+ """Fetch merge requests from the GitLab API."""
2
+
3
+ from urllib.parse import quote
4
+
5
+ from glab_release_notes.http import Payload, gitlab_json, payload_list
6
+ from glab_release_notes.models import MergeRequest, ReleaseNotesError
7
+ from glab_release_notes.payload import merge_request_from_payload
8
+
9
+
10
+ def fetch_mr(iid: int) -> MergeRequest:
11
+ """Load a merge request by iid."""
12
+ raw = gitlab_json(f"merge_requests/{iid}")
13
+ if not isinstance(raw, dict):
14
+ msg = f"Invalid GitLab response for MR !{iid}"
15
+ raise ReleaseNotesError(msg)
16
+ return merge_request_from_payload(raw)
17
+
18
+
19
+ def _first_or_none(payloads: list[Payload]) -> Payload | None:
20
+ if payloads:
21
+ return payloads[0]
22
+ return None
23
+
24
+
25
+ def _pick_payload(payloads: list[Payload]) -> Payload | None:
26
+ merged = [payload for payload in payloads if payload.get("state") == "merged"]
27
+ chosen = _first_or_none(merged)
28
+ if chosen is not None:
29
+ return chosen
30
+ return _first_or_none(payloads)
31
+
32
+
33
+ def fetch_mr_for_commit(sha: str) -> MergeRequest | None:
34
+ """Find the merged merge request for a commit SHA."""
35
+ encoded = quote(sha, safe="")
36
+ path = f"repository/commits/{encoded}/merge_requests"
37
+ chosen = _pick_payload(payload_list(gitlab_json(path)))
38
+ if chosen is None:
39
+ return None
40
+ return merge_request_from_payload(chosen)