diff --git a/.changes/PRODENG-3931.md b/.changes/PRODENG-3931.md new file mode 100644 index 00000000..b9f4a1b1 --- /dev/null +++ b/.changes/PRODENG-3931.md @@ -0,0 +1 @@ +`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 new file mode 100644 index 00000000..cfd778b6 --- /dev/null +++ b/.changes/README.md @@ -0,0 +1,70 @@ +# 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 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 +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 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.** + +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..a2faea7a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -67,16 +67,55 @@ 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" + # 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 + # 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: