From a63c01d62a55a34bbc5f6084cc687c3edd70e53a Mon Sep 17 00:00:00 2001 From: Geoffrey Vancoetsem <10533139+geeooff@users.noreply.github.com> Date: Fri, 18 Sep 2026 18:35:57 +0200 Subject: [PATCH 1/2] Write the release notes here, in a changelog the release copies The release page said "First public release" over a list of commits: a changelog for the people who wrote them, not for the person who clicked What changed. CHANGELOG.md now carries a section per version in Keep a Changelog's shape, each line something the user can see or do, written by the agent from the commits and the design pages and read by the maintainer as a diff -- the release commit is made on the maintainer's machine anyway, with the agent present, so the runner never has to summarise. The notes script copies the tag's section onto the release page, keeps the commits below it for the curious and the checksums last, and refuses a version without a section; the release build refuses it first. The rules of a section are in AGENTS.md. The sections for 0.1.0 and for what 0.2.0 will carry are written. Co-Authored-By: Claude Opus 5 --- AGENTS.md | 30 +++++++++-- CHANGELOG.md | 89 +++++++++++++++++++++++++++++++++ README.md | 1 + docs/design/14-release-notes.md | 71 ++++++++++++++------------ docs/design/README.md | 2 +- docs/reference.md | 12 +++-- scripts/build.ps1 | 19 ++++++- scripts/release-notes.ps1 | 42 ++++++++++++---- 8 files changed, 212 insertions(+), 54 deletions(-) create mode 100644 CHANGELOG.md diff --git a/AGENTS.md b/AGENTS.md index cb71233..6d2d7e1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -122,11 +122,33 @@ tree because the binaries carry the commit they were built from, and from the commit that carries the final documentation, never before it. **Publishing a release** is a tag, and the tag needs explicit approval like -any push: bump `version` in `Cargo.toml` in the release commit, merge it, +any push: bump `version` in `Cargo.toml` in the release commit, turn the +`[Unreleased]` section of `CHANGELOG.md` into that version's, merge it, tag that commit `vX.Y.Z`, push the tag. The release workflow runs the same -script on a runner and publishes the installer, the zip and their checksums. -Versions follow `docs/design/08-distribution.md`: the number moves only in -a release commit, and 1.0.0 waits for the criteria written there. +script on a runner and publishes the installer, the zip and their checksums, +with the changelog section as the notes; `build.ps1 release` and the +workflow both refuse a version the changelog does not carry. Versions +follow `docs/design/08-distribution.md`: the number moves only in a release +commit, and 1.0.0 waits for the criteria written there. + +**The changelog is written here, by the agent, and read by the maintainer +as a diff.** The workflow cannot summarise, and the release commit is made +on the maintainer's machine anyway, with the agent present. Rules for a +section, so that two agents write it the same way: + +- Every line is something the person running the program can see or do, + in a sentence, under *Added*, *Changed*, *Fixed* or *Removed*. Never the + code, the module or the commit; those are on the release page under + *For the curious*, which the workflow writes from `git log`. +- One line per thing that changed for them, however many commits it took; + no line for what changed only for the people working here — tests, + refactors, the design record, this file. +- Written from the commits since the previous tag and the design pages + they touched, in `[Unreleased]` as the work lands or at the latest in the + release commit. A link to a lot page is allowed when the reasoning is + worth a click; no line needs one to make sense. +- Once a version is published its section is history: corrected in place + only for an error of fact, never rewritten for taste. Three tests read this machine's registry — the Known Game List, the Game Bar registration, the real sensor — which a GitHub-hosted Windows Server runner diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..23da7aa --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,89 @@ +# Changelog + +What changed for the person running the program, one section per release, +newest first. Each line is something you can see or do; the commits behind +it are listed on the release page, under *For the curious*, and the +reasoning is in the [design record](docs/design/README.md). The shape is +[Keep a Changelog](https://keepachangelog.com/): *Added*, *Changed*, +*Fixed*, *Removed*. + +The release workflow takes the section for the tag it is given and refuses +to publish without one. `AGENTS.md` says how a section is written. + +## [Unreleased] + +### Added + +- **Check for updates**, in the icon's menu: asks GitHub whether a newer + release exists — the only time the program ever connects to anything, + and only when you click. The answer comes as a silent notification and + waits in the menu: *Up to date*, or **Download and install** beside + **What changed**, which opens the release page. +- **Download and install** fetches the new release, checks it against the + checksums the release publishes, and installs it — the installer for an + installed copy, the zip for one unpacked by hand. The icon disappears for + a second and comes back on the new version, which says so with a + notification at its first start. +- Updating in the middle of a game keeps your commands out of it: the old + watcher hands the game session to the new one, which picks it up where it + was. Nothing runs twice, and your gaming configuration is never switched + off and on again mid-game. +- `gamemode-executor update` does the same from a terminal; `update --check` + only asks. +- `gamemode-executor stop --handover` stops the watcher and leaves an open + game session to the next one, for anyone restarting it by hand during a + game. +- The log gains an `update` category: every request, every verdict and + every step of an install, with the reason when something fails. + +### Changed + +- The installer and the zip now carry the same four files: the two + executables, `LICENSE.txt` and `README.txt`. The zip no longer ships a + copy of the documentation; the readme links the pages and the recipes for + the exact build you have. +- Upgrading with the installer no longer closes a game session: the new + watcher resumes it. +- The recipes say to restart the watcher after copying a configuration — + `stop`, then `install-task` — which an installed copy needs to read the + new file. +- The release page says where the program was measured, Windows 11, and + what Windows 10 shares with it, rather than claiming both. + +## [0.1.0] - 2026-09-18 + +The first release. Runs the executables you configure when a game starts +and when it stops, on Windows' own signal that a game is running. + +### Added + +- Detection through the Game Bar presence writer Windows starts for a game + and stops when it is gone: no list of games to maintain, nothing polled + while you play. +- A configuration file, `config.toml`, naming the commands to run on each + edge, in series or in parallel, with placeholders for the game's name and + path; `validate` to check it, `trigger start|stop` to try the commands + without a game. +- The game's name in the log and in the placeholders, refined a little way + into the session by asking the GPU which process is really drawing. +- A notification area icon — grey without a game, green with one — with a + menu: edit the configuration, open the log, open the documentation for + the exact build running, quit. +- A windowless twin, `gamemode-executorw.exe`, started at logon by a task + `install-task` registers; the console executable is the one you type + commands into. +- Recovery after a logoff, shutdown or crash mid-game: the stop commands + run at the next start, so you are not left on a gaming configuration. +- A log for two readers: plain sentences at `info`, the technical detail as + fields at `debug`. +- A per-user installer, `.msi`, that needs no administrator rights, writes a + starter configuration if you have none, registers the logon task and + starts the watcher; a `.zip` for anyone who would rather not run an + installer. Uninstalling removes the executables and the task and leaves + your configuration; `purge` removes every trace on request. +- Recipes with ready-made configurations: fan profiles with FanControl, + including the scheduled tasks a program that needs administrator rights + requires, and a Windows power plan. + +[Unreleased]: https://github.com/Geeooff/GameModeExecutor/compare/v0.1.0...HEAD +[0.1.0]: https://github.com/Geeooff/GameModeExecutor/releases/tag/v0.1.0 diff --git a/README.md b/README.md index 3f21ba8..a4f93db 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ update, and shows nothing but a small icon in the notification area. | [Recipes](docs/recipes/) | Worked examples, one folder each with a ready-made `config.toml` — including fan profiles with FanControl, which needs one extra step because it requires administrator rights. | | [How it works](docs/how-it-works.md) | For the curious: how it knows a game is running, why there are two executables, why the wait after you quit. No programming needed. | | [Reference](docs/reference.md) | Commands, configuration fields, exit codes, the log contract, building. | +| [Changelog](CHANGELOG.md) | What each release changed for you, in plain sentences. | | [Design record](docs/design/) | Why it is built this way: the decisions, the measurements behind them, and what is still open. | | [AGENTS.md](AGENTS.md) | How to work in this repository — for coding agents, and for people. | diff --git a/docs/design/14-release-notes.md b/docs/design/14-release-notes.md index 0920f7b..9ebac70 100644 --- a/docs/design/14-release-notes.md +++ b/docs/design/14-release-notes.md @@ -1,10 +1,17 @@ # Lot 14 — Release notes people can read -**Status: proposed 2026-09-18**, on the maintainer's remark after the first -update ran through the menu: *What changed in 0.1.0* opened the release -page, and the page said *First public release* over a list of commits. -That is a changelog for the people who wrote the commits, not for the -person who clicked. +**Status: in progress since 2026-09-18, the same day it was proposed**, on +the maintainer's remark after the first update ran through the menu: +*What changed in 0.1.0* opened the release page, and the page said *First +public release* over a list of commits. That is a changelog for the people +who wrote the commits, not for the person who clicked. Taken before 0.2.0, +so that release reads well from the moment it is published. + +- [x] `CHANGELOG.md`, with the sections for 0.1.0 and what 0.2.0 will carry — 2026-09-18 +- [x] `release-notes.ps1` takes the section for the tag and refuses a version without one; `build.ps1 release` refuses it first, on the machine that can still write it — 2026-09-18 +- [x] The rules of a section, in `AGENTS.md` — 2026-09-18 +- [ ] `v0.1.0`'s notes rewritten once with its section, on approval +- [ ] The first release published this way: 0.2.0 **Goal.** Every release page reads, in a few lines, what changed for the person running the program — and the same lines are what the updater's @@ -42,41 +49,41 @@ way. - **Published releases are rewritten once**, by hand, with the new shape, when this lot lands — `v0.1.0` and whatever follows it before then. -## Who writes it — the maintainer's questions, 2026-09-18 +## Who writes it — the maintainer's questions, and the answer -The page above says *by hand, in the release commit*. The maintainer's -reading, the same day, is that this is a task for the coding agent, not -for a person: a person forgets, and an agent can establish everything a -changelog needs from the commits between two tags and the design pages -they touch, and turn it into something coherent that a lay user can read. -Two constraints shape how, and both are noted here to be settled when the -lot is taken: +The first draft of this page said *by hand, in the release commit*. The +maintainer's reading, the same day, was that this is a task for the coding +agent, not for a person: a person forgets, and an agent can establish +everything a changelog needs from the commits between two tags and the +design pages they touch, and turn it into something coherent that a lay +user can read. Two constraints were raised with it: - **The release workflow cannot call the agent.** A GitHub runner has Copilot, not Claude, and the agent's memory of this project is local to the maintainer's machine. So the notes cannot be written *by* the - release. What the workflow can do is publish a **placeholder** — the - install lines, the checksums, the commits — and mark the notes as - pending; the agent then rewrites them on the maintainer's request, from - the commits and the design record, and the maintainer publishes the - result. Whether that rewrite goes through `gh release edit` by the agent - on approval, or through a `CHANGELOG.md` the next release picks up, is - the choice to make. -- **The rules of the summary belong in `AGENTS.md`.** What a changelog - line is made of — what the user sees, in sentences, never the code; - which commits are one line and which are none; when a lot page is - linked and when nothing is; how the *For the curious* list relates to - the lines above it — has to be written down before an agent is asked to - follow it twice the same way. A framework there, the way the log - contract and the commit-message shape already are. + release; a placeholder the agent fills afterwards was one shape + considered. +- **The rules of the summary belong in `AGENTS.md`**, written down before + an agent is asked to follow them twice the same way. + +**Settled the same evening, by noticing where the release commit is made:** +on the maintainer's machine, with the agent present. Nothing needs the +runner to summarise. The agent writes the section in `CHANGELOG.md` — in +`[Unreleased]` as the work lands, at the latest in the release commit — +from the commits since the previous tag and the design pages they touch; +the maintainer reads it as a diff in the release pull request, the way any +change is read; the workflow copies it onto the release page and refuses a +version without one, so no release is ever published with nothing to say. +No placeholder, no edit after publication. The rules are in `AGENTS.md` +beside the release procedure: what a line is, what earns none, when to +link, and that a published section is history. -Left open on purpose until then. What the page decided above stands where -it does not depend on the author: the notes sit first, the commits stay -below, published releases are rewritten once. +The one exception is the release published before this lot: `v0.1.0`'s +notes are rewritten once, by `gh release edit` on the maintainer's +approval, with the section the changelog now carries for it. -## To settle when it is taken +## To settle later -- The two questions above. - Whether the updater should show the notes itself one day, through the API's `body`, rather than open the page. Not before the notes are worth showing. diff --git a/docs/design/README.md b/docs/design/README.md index 838fd80..fafa4bb 100644 --- a/docs/design/README.md +++ b/docs/design/README.md @@ -25,7 +25,7 @@ session rather than when the code compiles. Each has its own page. | 11 | [Documentation for the people who use it](11-user-documentation.md) | done | | 12 | [Editing the configuration without breaking it](12-editing-on-a-copy.md) | proposed | | 13 | [Updating](13-updating.md) | in progress | -| 14 | [Release notes people can read](14-release-notes.md) | proposed | +| 14 | [Release notes people can read](14-release-notes.md) | in progress | **Dependency order:** 1 → 2 → 4 → 5 → 6 → 7, with 3 independent and 7 needing both 3 and 6. Logging sits before the icon deliberately — the icon logs too, diff --git a/docs/reference.md b/docs/reference.md index 5f7b5f9..500e163 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -252,11 +252,13 @@ header**: a console program and a windowless one cannot be the same file, and getting that backwards is invisible until someone sees a black window at logon. A release proper is a tag. The version is bumped in `Cargo.toml` in the -release commit, that commit is tagged `vX.Y.Z`, and pushing the tag makes -the release workflow run this same script on a GitHub runner, then publish -the installer, the zip and their SHA-256 checksums as a GitHub release, with -notes listing the commits since the previous tag. Nothing is built or -uploaded by hand. +release commit, `CHANGELOG.md` gains that version's section, that commit +is tagged `vX.Y.Z`, and pushing the tag makes the release workflow run +this same script on a GitHub runner, then publish the installer, the zip +and their SHA-256 checksums as a GitHub release, with the changelog section +as the notes and the commits since the previous tag below it. A version +the changelog does not carry is refused, by `release` here and by the +workflow. Nothing is built or uploaded by hand. `release` refuses a dirty tree, checks the commit stamped into the binaries is the commit being built, checks the version block each executable carries, diff --git a/scripts/build.ps1 b/scripts/build.ps1 index b66c1dd..2f9676b 100644 --- a/scripts/build.ps1 +++ b/scripts/build.ps1 @@ -167,7 +167,7 @@ function Invoke-Tests { Step "Documentation links resolve" $broken = @() - $pages = @((Join-Path $root 'README.md'), (Join-Path $root 'AGENTS.md')) + + $pages = @((Join-Path $root 'README.md'), (Join-Path $root 'AGENTS.md'), (Join-Path $root 'CHANGELOG.md')) + (Get-ChildItem (Join-Path $root 'docs') -Recurse -Filter '*.md' | ForEach-Object { $_.FullName }) foreach ($page in $pages) { @@ -263,11 +263,28 @@ function Invoke-Build { } } +# The section of CHANGELOG.md for one version, heading excluded, or nothing. +# The workflow's notes script reads it the same way, so a version without +# its section fails here first, on the machine that can still write it. +function Get-ChangelogSection([string] $Version) { + $text = Get-Content (Join-Path $root 'CHANGELOG.md') -Raw + $pattern = "(?ms)^## \[$([regex]::Escape($Version))\][^\r\n]*\r?\n(.*?)(?=^## |\z)" + $match = [regex]::Match($text, $pattern) + if ($match.Success) { $match.Groups[1].Value.Trim() } else { $null } +} + function Invoke-Release { $version = Get-Version $stage = Join-Path $root "dist\GameModeExecutor-$version" $zip = Join-Path $root "dist\GameModeExecutor-$version.zip" + Step "The changelog carries $version" + $section = Get-ChangelogSection $version + if (-not $section) { + Fail "CHANGELOG.md has no section for $version -- a release commit carries one, see AGENTS.md" + } + Write-Host " $((($section -split "`n") | Where-Object { $_ -match '^- ' }).Count) lines" + Step "Staging $version" if (Test-Path $stage) { Remove-Item -Recurse -Force $stage } New-Item -ItemType Directory -Force -Path $stage | Out-Null diff --git a/scripts/release-notes.ps1 b/scripts/release-notes.ps1 index d58d064..ec5da29 100644 --- a/scripts/release-notes.ps1 +++ b/scripts/release-notes.ps1 @@ -1,7 +1,10 @@ -# Writes the release notes for a tag, from what the release build produced: -# the documentation link the binary carries, the checksums of the two -# artefacts, and the commits since the previous tag. Run by the release -# workflow after scripts\build.ps1 release; nothing in it needs GitHub. +# Writes the release notes for a tag: the section CHANGELOG.md carries for +# that version -- written before the release, for the person running the +# program -- then the documentation link the binary carries, the commits +# since the previous tag for the curious, and the checksums of the two +# artefacts. Run by the release workflow after scripts\build.ps1 release; +# nothing in it needs GitHub. A version without its section is refused: +# the workflow cannot summarise, and a release must have something to say. # # .\scripts\release-notes.ps1 -Tag v0.1.0 writes dist\notes.md and dist\SHA256SUMS.txt param( @@ -13,6 +16,19 @@ $dist = Join-Path $root 'dist' if ($Tag -notmatch '^v(\d+\.\d+\.\d+)$') { throw "tag `"$Tag`" is not vX.Y.Z" } $version = $Matches[1] + +# The notes proper, from CHANGELOG.md: the section for this version, +# heading excluded, up to the next one. Refused when absent, the way a tag +# that disagrees with Cargo.toml is refused. +$changelog = Get-Content (Join-Path $root 'CHANGELOG.md') -Raw +$pattern = "(?ms)^## \[$([regex]::Escape($version))\][^\r\n]*\r?\n(.*?)(?=^## |\z)" +$section = [regex]::Match($changelog, $pattern) +if (-not $section.Success) { throw "CHANGELOG.md has no section for $version; a release commit carries one" } +# The last section is followed by the link definitions Keep a Changelog +# keeps at the bottom; those are the file's, not the release's. +$notes = (($section.Groups[1].Value -replace '\r\n', "`n") -split "`n" | + Where-Object { $_ -notmatch '^\[[^\]]+\]: ' }) -join "`n" +$notes = $notes.Trim() $msi = Join-Path $dist "GameModeExecutor-$version.msi" $zip = Join-Path $dist "GameModeExecutor-$version.zip" foreach ($artefact in $msi, $zip) { @@ -34,11 +50,11 @@ $docs = ($stamp | Select-String -Pattern '^documentation:\s+(\S+)').Matches[0].G # into Select-Object -First, which stops the upstream command. $tags = @(& git -C $root tag --sort=-v:refname | Where-Object { $_ -ne $Tag -and $_ -match '^v\d+\.\d+\.\d+$' }) $previous = if ($tags.Count) { $tags[0] } else { $null } -$changes = if ($previous) { - $log = @(& git -C $root log --format='- %s' "$previous..$Tag") - "## Changes since $previous`n`n" + ($log -join "`n") +$commits = if ($previous) { + $log = @(& git -C $root log --format='- %s' "$previous..$Tag" --no-merges) + "## For the curious`n`nThe commits since $previous, newest first:`n`n" + ($log -join "`n") } else { - "## Changes`n`nFirst public release." + "## For the curious`n`nThe first release: every commit is in it." } $lines = @( @@ -51,13 +67,17 @@ $lines = @( '', "[Documentation for this exact build]($docs).", '', + '## What changed', + '', + $notes, + '', + $commits, + '', '## SHA-256', '', '```' ) + $sums + @( - '```', - '', - $changes + '```' ) Set-Content -Path (Join-Path $dist 'notes.md') -Value $lines -Encoding utf8 Get-Content (Join-Path $dist 'notes.md') From cf900b642df16beaf7334bdf70fd8444d04caa8e Mon Sep 17 00:00:00 2001 From: Geoffrey Vancoetsem <10533139+geeooff@users.noreply.github.com> Date: Fri, 18 Sep 2026 18:46:58 +0200 Subject: [PATCH 2/2] Require the date in a released section's heading The changelog keeps the date of each release commit so the file reads on its own, without the release page. A heading without one is refused the way a missing section is: by the release build here first, and by the workflow's notes script. Co-Authored-By: Claude Opus 5 --- AGENTS.md | 6 ++++-- CHANGELOG.md | 7 +++++-- scripts/build.ps1 | 8 +++++--- scripts/release-notes.ps1 | 4 ++-- 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 6d2d7e1..2ecebaf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -123,11 +123,13 @@ from the commit that carries the final documentation, never before it. **Publishing a release** is a tag, and the tag needs explicit approval like any push: bump `version` in `Cargo.toml` in the release commit, turn the -`[Unreleased]` section of `CHANGELOG.md` into that version's, merge it, +`[Unreleased]` section of `CHANGELOG.md` into that version's -- headed +`## [x.y.z] - YYYY-MM-DD`, the date of that commit, so the file reads on +its own without the release page -- merge it, tag that commit `vX.Y.Z`, push the tag. The release workflow runs the same script on a runner and publishes the installer, the zip and their checksums, with the changelog section as the notes; `build.ps1 release` and the -workflow both refuse a version the changelog does not carry. Versions +workflow both refuse a version the changelog does not carry, dated. Versions follow `docs/design/08-distribution.md`: the number moves only in a release commit, and 1.0.0 waits for the criteria written there. diff --git a/CHANGELOG.md b/CHANGELOG.md index 23da7aa..20cc531 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,11 @@ reasoning is in the [design record](docs/design/README.md). The shape is [Keep a Changelog](https://keepachangelog.com/): *Added*, *Changed*, *Fixed*, *Removed*. -The release workflow takes the section for the tag it is given and refuses -to publish without one. `AGENTS.md` says how a section is written. +Each released section is headed with its version and the date of its +release commit, so this file reads on its own; the release page has the +rest. The release workflow takes the section for the tag it is given and +refuses to publish without one, or without its date. `AGENTS.md` says how +a section is written. ## [Unreleased] diff --git a/scripts/build.ps1 b/scripts/build.ps1 index 2f9676b..b6dbde9 100644 --- a/scripts/build.ps1 +++ b/scripts/build.ps1 @@ -264,11 +264,13 @@ function Invoke-Build { } # The section of CHANGELOG.md for one version, heading excluded, or nothing. +# The heading must carry the date of the release commit -- `## [x.y.z] - +# YYYY-MM-DD` -- so the file reads on its own, without the release page. # The workflow's notes script reads it the same way, so a version without -# its section fails here first, on the machine that can still write it. +# its dated section fails here first, on the machine that can still write it. function Get-ChangelogSection([string] $Version) { $text = Get-Content (Join-Path $root 'CHANGELOG.md') -Raw - $pattern = "(?ms)^## \[$([regex]::Escape($Version))\][^\r\n]*\r?\n(.*?)(?=^## |\z)" + $pattern = "(?ms)^## \[$([regex]::Escape($Version))\] - \d{4}-\d{2}-\d{2}[^\r\n]*\r?\n(.*?)(?=^## |\z)" $match = [regex]::Match($text, $pattern) if ($match.Success) { $match.Groups[1].Value.Trim() } else { $null } } @@ -281,7 +283,7 @@ function Invoke-Release { Step "The changelog carries $version" $section = Get-ChangelogSection $version if (-not $section) { - Fail "CHANGELOG.md has no section for $version -- a release commit carries one, see AGENTS.md" + Fail "CHANGELOG.md has no dated section for $version -- a release commit carries ``## [$version] - YYYY-MM-DD``, see AGENTS.md" } Write-Host " $((($section -split "`n") | Where-Object { $_ -match '^- ' }).Count) lines" diff --git a/scripts/release-notes.ps1 b/scripts/release-notes.ps1 index ec5da29..a4acf04 100644 --- a/scripts/release-notes.ps1 +++ b/scripts/release-notes.ps1 @@ -21,9 +21,9 @@ $version = $Matches[1] # heading excluded, up to the next one. Refused when absent, the way a tag # that disagrees with Cargo.toml is refused. $changelog = Get-Content (Join-Path $root 'CHANGELOG.md') -Raw -$pattern = "(?ms)^## \[$([regex]::Escape($version))\][^\r\n]*\r?\n(.*?)(?=^## |\z)" +$pattern = "(?ms)^## \[$([regex]::Escape($version))\] - \d{4}-\d{2}-\d{2}[^\r\n]*\r?\n(.*?)(?=^## |\z)" $section = [regex]::Match($changelog, $pattern) -if (-not $section.Success) { throw "CHANGELOG.md has no section for $version; a release commit carries one" } +if (-not $section.Success) { throw "CHANGELOG.md has no dated section for $version; a release commit carries ``## [$version] - YYYY-MM-DD``" } # The last section is followed by the link definitions Keep a Changelog # keeps at the bottom; those are the file's, not the release's. $notes = (($section.Groups[1].Value -replace '\r\n', "`n") -split "`n" |