diff --git a/AGENTS.md b/AGENTS.md index cb71233..2ecebaf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -122,11 +122,35 @@ 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 -- 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. -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, 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. + +**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..20cc531 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,92 @@ +# 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*. + +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] + +### 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..b6dbde9 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,30 @@ 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 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))\] - \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 } +} + 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 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" + 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..a4acf04 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))\] - \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 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" | + 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')