assemble-news
Collate news.d/ fragment files into NEWS.md under section headings (by default ## Breaking changes, ## New features, ## Bug fixes, ## Minor improvements) and delete consumed fragment files.
Pairs with check-news.yml, allowing pull requests to add fragment files under news.d/ instead of directly editing NEWS.md, eliminating merge conflicts between concurrent PRs.
Inputs
| Input | Type | Default | Description |
|---|---|---|---|
fragments-dir |
string | 'news.d' |
Directory containing fragment files (<slug>.<category>.md). |
news-file |
string | 'NEWS.md' |
Target NEWS.md file path. |
headings |
string | '' |
Category-to-heading map, as newline-separated category = Heading pairs. Empty keeps the built-in map. |
bullet-style |
string | '' |
List-marker character (-, *, or +) every inserted fragment bullet is normalized to. Empty auto-detects from news-file’s own first bullet, falling back to - when it has none; see Bullet-marker normalization for what counts as that first bullet. |
Example
# Copy to .github/workflows/assemble-news.yml in your repo.
name: Assemble NEWS.md
on:
workflow_dispatch:
jobs:
assemble:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Assemble news fragments
uses: Morrison-Lab/gha/.github/actions/assemble-news@v2
with:
fragments-dir: news.d
news-file: NEWS.md
- name: Commit assembled NEWS.md
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add NEWS.md news.d/
git diff --staged --quiet || git commit -m "chore(release): assemble news fragments into NEWS.md"
git pushCustom headings
The built-in map is breaking to Breaking changes; added and feature to New features; fixed and bug to Bug fixes; and changed, minor, deprecated, removed, and security to Minor improvements.
A repo whose NEWS.md uses a different taxonomy supplies its own:
- name: Assemble news fragments
uses: Morrison-Lab/gha/.github/actions/assemble-news@v2
with:
fragments-dir: news.d
headings: |
breaking = Breaking changes
added = New features
fixed = Bug fixes
infrastructure = Infrastructure
docs = DocumentationWhen set, headings replaces the built-in map rather than extending it, defining both the complete set of recognized categories and the order the headings are written in. Several categories may share a heading, which then takes the position of its first-listed category.
A # comments out a line only when it starts the line, so a heading may contain one (csharp = C# interop). A category may not contain a dot – it is a single segment of <slug>.<category>.md, and a dotted one would collate the same fragment twice whenever its suffix is also configured.
A fragment whose category is outside the active map fails the step, naming the offending files, rather than being skipped – the check runs before anything is consumed, so a mixed batch fails without deleting the valid fragments beside it.
Repeat assemblies
Fragments go into the first top-level section of news-file. In an R package’s NEWS.md that is the development-version block, but the script does not inspect the heading text: whatever section comes first is the target. When that block already holds a section with the same heading from an earlier assembly, the new bullets are spliced in under it, ahead of the bullets it already has and as one list with them, rather than under a second copy of the heading, which markdownlint’s MD024 would reject as a duplicate sibling. Only headings the block does not have yet are added, as one fresh block in the map’s display order directly under the top-level heading, above the sections the block already had. A heading that appears only in an older release block further down is not a target. The splice adds a single blank line between the new bullets and a following heading, paragraph, or fenced code block, and never a second one; blank lines the file already carried past that point are left as they were. Matching is exact after trimming trailing spaces and tabs, so a heading spelled differently (case, a doubled space, a CRLF line ending, whose \r is not trimmed) does not merge into the existing section and gets a fresh one instead, as every run did before. A line inside a fenced code block that starts with # and a space is not read as a heading, and a fenced block right after a splice point keeps its separating blank line.
Bullet-marker normalization
markdownlint’s MD004 rule defaults to style: consistent, which takes its required marker (-, *, or +) from the assembled file’s first bullet. Fragments are spliced in at the top of news-file, so a fragment authored with a different marker than the file’s dominant one would flip that requirement for every other bullet already in the file.
To prevent that, every inserted bullet’s top-of-line marker is normalized to one target, in this order: the bullet-style input when set; otherwise news-file’s own first existing bullet marker; otherwise -, when news-file has no bullet yet to take a style from (so fragments authored with different markers still collate under one consistent style even on a fresh file). Only the marker character is rewritten – the rest of each line, including nested-list indentation, is left untouched.
Detection considers only lines matching a marker followed by whitespace, and among those it skips a spaced thematic break rather than reading it as the first bullet – so a NEWS.md opening with - - - above a *-styled list normalizes to *. A run of fewer than three markers is a list item rather than a break, so an empty list item still sets the style. + is never treated as a break character, so + + + is a list at any length.
Two details of that test are the detector’s own rather than CommonMark’s, and both are deliberate:
- It tests only
-and*. The spec’s third break character,_, is never reached, because a_line is not a marker-followed-by-whitespace candidate in the first place. - It ignores leading indentation, where the spec caps a break at three spaces. A more deeply indented separator is indented-code or nested-list content, which should not set the whole file’s style either.
One consequence is worth knowing before hand-editing a NEWS.md: a marker alone on its line, with no trailing whitespace, is not a detection candidate at all, so it sets no style and the fall-back - applies. An empty list item therefore has to carry its trailing space to count (#746).
See assemble-news.yml in examples/ for the caller stub.