From 0f9a77148a92b9c7335e7873cbfc028eb0587fe2 Mon Sep 17 00:00:00 2001 From: James Nesbitt Date: Thu, 5 Nov 2026 14:41:39 +0200 Subject: [PATCH 1/2] release: assemble release notes from in-repo fragments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There is no published Launchpad release-notes page: https://docs.mirantis.com/mke/3.9/launchpad/lp-release-notes.html returns 404. The GitHub release body is the announcement surface, and nothing caused it to be written. Adds `.changes/`, one markdown fragment per change an operator needs to know about before upgrading, named after the ticket. The release job checks out the tree, concatenates the fragments in filename order under a "Release notes" heading, and passes the result to action-gh-release as `body_path`. GitHub's generated commit and contributor notes are appended below, so nothing is lost. Fragments are emitted verbatim, separated by a blank line, and own their own markdown: a note may run to several paragraphs, and continuation lines under a list marker would need indenting to stay in the list. When no fragment is present the body says so: _No release notes were recorded for this release._ rather than carrying an empty section. An empty section reads as "nothing notable changed", which is indistinguishable from "nobody wrote a fragment" — the first is a fact about the release, the second is a fact about the process, and they must not look the same. Failing the release outright was rejected: a release with genuinely no notable changes is legitimate and should not be blocked, and a gate would be satisfied by adding empty fragments to clear it. `.changes/` rather than `docs/release-notes/` because the fragments are build inputs consumed by the release workflow, not pages for a reader browsing `docs/`. `.changes/README.md` documents when to add a fragment, the format, that the reason for a change belongs in the note, and that fragments are removed when the next release is cut. The release job gains an `actions/checkout` step, SHA-pinned like the others, because it previously downloaded artifacts only and had no access to the repository. The build job already had its own checkout. First fragment records the PRODENG-3931 `metadata.name` validation change, which rejects configurations that previously applied. Which ticket owns this work is not yet settled, so the subject carries no ticket prefix. Written by AI: claude-opus-5 Signed-off-by: James Nesbitt --- .changes/PRODENG-3931.md | 1 + .changes/README.md | 64 +++++++++++++++++++++++++++++++++++ .github/workflows/release.yml | 35 +++++++++++++++++++ 3 files changed, 100 insertions(+) create mode 100644 .changes/PRODENG-3931.md create mode 100644 .changes/README.md diff --git a/.changes/PRODENG-3931.md b/.changes/PRODENG-3931.md new file mode 100644 index 00000000..1164ae5e --- /dev/null +++ b/.changes/PRODENG-3931.md @@ -0,0 +1 @@ +`metadata.name` now rejects names containing `/`, `\`, `.`, `..` or leading/trailing whitespace. Configs using such a name stop applying on upgrade. Rejection was chosen over silent rewriting because a rewritten name silently relocates Launchpad's internal storage paths. diff --git a/.changes/README.md b/.changes/README.md new file mode 100644 index 00000000..e9d2b082 --- /dev/null +++ b/.changes/README.md @@ -0,0 +1,64 @@ +# Release note fragments + +One file per change that an operator needs to know about before upgrading. +`.github/workflows/release.yml` assembles every fragment in this directory into +the GitHub release body when a tag is pushed. + +## When to add one + +Add a fragment when the change alters behaviour an existing configuration or +workflow depends on: a validation rule that rejects input that previously +applied, a default that changes, a flag or field that is removed or renamed, or +anything that makes a documented example stop working. + +Do not add one for an internal refactor, a test change, or a fix that makes the +product match its existing documentation. + +## Format + +Name the file after the ticket, `PRODENG-NNNN.md`. Fragments are emitted into +the release body **verbatim**, in filename order, separated by a blank line. +The fragment owns its own markdown: no list marker or heading is added around +it. + +A short note can be a single paragraph. A note that needs more — what breaks, +what to do about it — may run to several paragraphs; start it with a bold +one-line summary so the release body is scannable. Avoid headings (`#`), which +would compete with the `## Release notes` heading and with GitHub's generated +sections below it. + +State what changed, what it breaks, and **why it was done that way**. The reason +is the part that stops an operator trying to work around the change. + +```markdown +`metadata.name` now rejects names containing `/`, `\`, `.`, `..` or +leading/trailing whitespace. Configs using such a name stop applying on +upgrade. Rejection was chosen over silent rewriting because a rewritten name +silently relocates Launchpad's internal storage paths. +``` + +```markdown +**Breaking: one-line summary of what changed.** + +What now fails, with the exact error text an operator will see. + +Why this shape was chosen over the alternative. +``` + +## After a release + +Fragments are not deleted automatically. Remove the ones covered by a release +when cutting the next one, so the notes describe that release rather than +accumulating. + +## If there are none + +The release body says so explicitly: + +``` +_No release notes were recorded for this release._ +``` + +That line exists so that a release nobody wrote a fragment for is +distinguishable from a release with genuinely nothing to report. An empty +section would read as the latter while meaning the former. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0d06c3c4..f5b437a5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -67,16 +67,51 @@ jobs: permissions: contents: write steps: + - name: Checkout code + # The release notes are assembled from .changes/ in the repository, so + # this job needs the tree as well as the built artifacts. + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + persist-credentials: false + - name: Download binaries uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: name: launchpad-release-binaries path: dist/ + - name: Assemble release notes + run: | + set -euo pipefail + notes="$RUNNER_TEMP/release-notes.md" + printf '## Release notes\n\n' > "$notes" + count=0 + for fragment in .changes/PRODENG-*.md; do + [ -e "$fragment" ] || continue + # Emitted verbatim rather than prefixed with a list marker: a note + # may run to several paragraphs, and continuation lines under a + # bullet would need indenting to stay in the list. The fragment + # owns its own markdown. + cat "$fragment" >> "$notes" + printf '\n' >> "$notes" + count=$((count + 1)) + done + if [ "$count" -eq 0 ]; then + # Stated rather than left blank. An empty section reads as "nothing + # notable changed"; this release may instead be one where nobody + # wrote a fragment. The two must not look the same. + printf '_No release notes were recorded for this release._\n' >> "$notes" + fi + echo "Assembled $count fragment(s):" + cat "$notes" + - name: Create GitHub Release uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3 with: files: dist/* + # The assembled fragments become the body; GitHub's generated notes + # (commits and contributors) are appended below them. + body_path: ${{ runner.temp }}/release-notes.md generate_release_notes: true draft: true env: From 7eb3f0bc441a085c4ca032df29adb72f3ff5c823 Mon Sep 17 00:00:00 2001 From: James Nesbitt Date: Thu, 5 Nov 2026 15:45:31 +0200 Subject: [PATCH 2/2] PRODENG-3931 separate fragments reliably; correct the name rule Two review findings on the pull request, both confirmed by measurement before changing anything. A fragment that does not end with a newline ran into the next one. With `First change.` (no trailing newline) followed by `Second change.`, the assembled body contained First change. Second change. which Markdown renders as one paragraph. The step now emits two newlines after each fragment, so the separating blank line does not depend on how the file happens to end. A fragment that does end with a newline gains a blank line, which Markdown collapses. `.changes/README.md` records that a trailing newline is not required. The PRODENG-3931 note stated the validation rule too broadly. It said names "containing" `/`, `\`, `.` or `..` are rejected. The validator in PR #687 rejects a name that contains `/` or `\` anywhere, but only a name that is exactly `.` or `..`: if name == "." || name == ".." { return false } return !strings.ContainsAny(name, `/\`) && ... So `prod.eu` remains valid. The previous wording told operators that a working configuration was about to stop applying. The fragment and the worked example in `.changes/README.md` now distinguish the two cases, and the README states why the distinction matters. Written by AI: claude-opus-5 Signed-off-by: James Nesbitt --- .changes/PRODENG-3931.md | 2 +- .changes/README.md | 16 +++++++++++----- .github/workflows/release.yml | 6 +++++- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/.changes/PRODENG-3931.md b/.changes/PRODENG-3931.md index 1164ae5e..b9f4a1b1 100644 --- a/.changes/PRODENG-3931.md +++ b/.changes/PRODENG-3931.md @@ -1 +1 @@ -`metadata.name` now rejects names containing `/`, `\`, `.`, `..` or leading/trailing whitespace. Configs using such a name stop applying on upgrade. Rejection was chosen over silent rewriting because a rewritten name silently relocates Launchpad's internal storage paths. +`metadata.name` now rejects names that contain `/` or `\`, names that are exactly `.` or `..`, and names with leading or trailing whitespace. Dotted names such as `prod.eu` are unaffected. Configs using a rejected name stop applying on upgrade. Rejection was chosen over silent rewriting because a rewritten name silently relocates Launchpad's internal storage paths. diff --git a/.changes/README.md b/.changes/README.md index e9d2b082..cfd778b6 100644 --- a/.changes/README.md +++ b/.changes/README.md @@ -19,7 +19,8 @@ product match its existing documentation. Name the file after the ticket, `PRODENG-NNNN.md`. Fragments are emitted into the release body **verbatim**, in filename order, separated by a blank line. The fragment owns its own markdown: no list marker or heading is added around -it. +it. A trailing newline is not required: the workflow emits the separating +blank line whether or not the file ends with one. A short note can be a single paragraph. A note that needs more — what breaks, what to do about it — may run to several paragraphs; start it with a bold @@ -31,12 +32,17 @@ State what changed, what it breaks, and **why it was done that way**. The reason is the part that stops an operator trying to work around the change. ```markdown -`metadata.name` now rejects names containing `/`, `\`, `.`, `..` or -leading/trailing whitespace. Configs using such a name stop applying on -upgrade. Rejection was chosen over silent rewriting because a rewritten name -silently relocates Launchpad's internal storage paths. +`metadata.name` now rejects names that contain `/` or `\`, names that are +exactly `.` or `..`, and names with leading or trailing whitespace. Dotted +names such as `prod.eu` are unaffected. Configs using a rejected name stop +applying on upgrade. Rejection was chosen over silent rewriting because a +rewritten name silently relocates Launchpad's internal storage paths. ``` +State the rule precisely. "Names containing `.`" and "names that are exactly +`.`" are different claims, and the broader one tells operators their working +configuration is about to break when it is not. + ```markdown **Breaking: one-line summary of what changed.** diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f5b437a5..a2faea7a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -93,7 +93,11 @@ jobs: # bullet would need indenting to stay in the list. The fragment # owns its own markdown. cat "$fragment" >> "$notes" - printf '\n' >> "$notes" + # Two newlines, not one: a fragment that does not end with a + # newline would otherwise run into the next one and Markdown would + # render the pair as a single paragraph. A fragment ending with a + # newline gains a blank line, which Markdown collapses. + printf '\n\n' >> "$notes" count=$((count + 1)) done if [ "$count" -eq 0 ]; then