Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 49 additions & 24 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -305,9 +305,10 @@ a release labelled with the PREVIOUS version, in four places at once.**
`<AssemblyVersion>`/`<FileVersion>` is the only source: `iscc` reads it off the
built exe for the installer filename, `release.yml` reads the same resource to
name the zip, `Settings.Version` is `Assembly.GetExecutingAssembly()` so the About
tab shows it, and `Updater.cs` compares *that* against `status.txt`. The tag comes
from the `release-notes` branch instead (`scripts/publish_release.py`), and is
created at `main`'s tip.
tab shows it, and `Updater.cs` compares *that* against `status.txt`. Which version
to publish comes from the `release-notes` branch instead
(`scripts/publish_release.py`), and the tag used to be created at `main`'s tip —
which is the whole mechanism below.

**Worked example, PK-0.9.19 (2026-10-02, issue #21).** Published 09:47 UTC; the
bump `cf7e91e` was committed 09:45 but sat on the PR #23 branch and merged at
Expand All @@ -325,27 +326,51 @@ bump `cf7e91e` was committed 09:45 but sat on the PR #23 branch and merged at
About-tab link regrouping) are not in the assets either, because they merged in
the same window.

⚠️ **`publish_release.py`'s version note USED to be informational, and it fired
on exactly this and was read as benign.** It is asymmetric now, which is the
point: *behind* means the version does not exist yet, *ahead* means the tag claims
a version whose code is not what the tree has. Both now die when publishing would
CREATE the release — the build labels its assets from the tree either way, so
`release.yml` refuses them and you are left with a published release carrying no
downloads at all. Re-applying notes to an existing release re-runs no build, so
there a difference stays a note; the script tells the two apart by whether the tag
exists yet. `--allow-version-mismatch` overrides, with that no-assets outcome.
`release.yml` also asserts the built exe's version equals the tag before
attaching anything. Belt and braces, because the
script is only one of three ways a release starts (a hand-pushed tag and a
`workflow_dispatch` recovery do not go through it).

⚠️ **Not fixed structurally, deliberately.** The right fix is to derive the exe's
version from the tag via GitVersion, which is already in the build for
`language.csproj` — then a mislabelled release is unrepresentable. It was not done
because there is no .NET toolchain in the container, so CI is the only compiler
(a wrong guess costs a full round), and because GitVersion's output on an
untagged commit would change what every development build's About tab reads. The
two gates are the cheap half; the derivation is still the real answer.
⚠️ **`publish_release.py` now tags the commit whose csproj DECLARES the version,
not `main`'s tip** — and because `release.yml` checks out the tag, that is what
makes the assets carry the right number. The sibling repos had this (it is
`commit_for_version()`, with four hard-won traps in its docstring: oldest match
not newest, no early exit, `--first-parent`, and a `:(top)` pathspec); this copy
was taken from them in `e894a3a` before the pin existed, which is why #21
happened here and not there. A later merge drifting the csproj forward is then
harmless, and the script says so rather than warning.

⚠️ **The pin cannot help when NO commit declares the version** — the bump has
not merged, so there is nothing to pin to — and that is exactly #21. Creating
the release is refused there. Only when creating: re-applying notes to an
existing release runs no build, so the tree's version is then irrelevant, and
`release_exists()` tells the two apart. ⚠️ **A tag existing is a different
question** — a deleted release, a hand-pushed tag, or a build that died before
`gh release create` each leave a tag with no release. Only a 404 counts as "no
release"; anything else is treated as creating, the stricter reading, because an
auth or network failure reading as success is how the check goes quiet.
⚠️ **A shallow clone reaches the same refusal for a different reason** — the
search could not see the commit — so unshallowing comes before
`--allow-version-mismatch`, which publishes the release the gate exists to
prevent. `release.yml` also asserts the built exe's version equals the tag
before attaching anything, because the script is only one of three ways a
release starts (a hand-pushed tag and a `workflow_dispatch` recovery do not go
through it).

⚠️ **Do NOT make the gate fire on the tree merely differing from the tag.** The
firmware and host tree is normally *ahead* of a prepared tag — every merge
auto-bumps — and the pin already makes that safe, so refusing there blocks
almost every legitimate publish. The condition is "no commit declares it", not
"the numbers differ".

**The GitVersion derivation is still the deeper answer, and still not done.**
Deriving the exe's version from the tag would make a mislabelled release
unrepresentable rather than merely refused. It is not done because there is no
.NET toolchain in the container, so CI is the only compiler (a wrong guess costs
a full round), and because GitVersion's output on an untagged commit would change
what every development build's About tab reads.

⚠️ **`scripts/publish_release.py` is byte-identical across `qmk_firmware`,
`PolyKybdHost` and `wincompose`, and nothing checks it.** It had already
diverged in both directions at once: the siblings held the pin and the
`make_latest` correctness, this copy held the create-time gate, and each was
the newer one for a different thing. `md5sum */scripts/publish_release.py` is
the check.

**`status.txt` is written by CI since 0.9.20** — the `status` job in `release.yml`,
after the assets are attached, from the version resource of the binary it just
Expand Down
63 changes: 48 additions & 15 deletions RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,21 @@ tag gets published — see ③. `GitVersion.yml` sets `tag-prefix: PK-` so GitVe
recognises our tags instead of computing 0.1.0.

⚠️ **The bump has to be MERGED to `main` before you publish.** The release builds
from the tag, and the tag is created at `main`'s tip, so a bump still sitting on a
branch means the release ships assets carrying the *previous* version. 0.9.19 did
this (issue #21): published 50 minutes before its bump merged, it carries
from the tag, and the tag can only be pinned to a commit that already declares the
version, so a bump still sitting on a branch leaves nothing to pin to — publishing
is refused (it used to ship assets carrying the *previous* version instead).
0.9.19 did this (issue #21): published 50 minutes before its bump merged, it carries
`WinCompose-Setup-0.9.18.exe`, reads 0.9.18 in its About tab, is missing two
changes its own notes describe, and could not be announced at all — an install
reporting 0.9.18 would have been offered an endless update to itself.

Two things now refuse rather than let that through, so this is a check you no
longer have to remember. `scripts/publish_release.py` dies when `main`'s version
is not the one the tag claims — in either direction, and only when publishing
would CREATE the release, since re-applying notes to one that already exists
re-runs no build. `release.yml` fails before attaching anything if the built
exe's version is not the tag's.
Three things now stand between you and that, so it is a check you no longer have
to remember. `scripts/publish_release.py` pins the tag to the commit that declares
the version, so a csproj that has drifted forward since the notes were prepared is
harmless. It dies when **no** commit declares it, and only when publishing would
CREATE the release, since re-applying notes to one that already exists re-runs no
build. And `release.yml` fails before attaching anything if the built exe's
version is not the tag's.

⚠️ `--allow-version-mismatch` overrides the first of those and leaves you a
published release with **no assets**, because the second still refuses them. It
Expand All @@ -53,12 +55,23 @@ that file; without it the release falls back to GitHub's auto-generated notes.
python scripts/publish_release.py --dry-run # show what it would do
python scripts/publish_release.py --tag PK-0.9.16

The tag comes from the `release-notes` branch, **not** from the csproj version:
with no `--tag`, it publishes the newest prepared `PK-<X.Y.Z>.md` found there. The
version on the default branch is read only to print a note when the two disagree,
so a csproj bump that landed after the notes were prepared does not change what
gets published. Pass `--tag` whenever more than one is prepared, and read the
"newest prepared tag" line it prints before letting it publish.
Which version to publish comes from the `release-notes` branch, **not** from the
csproj: with no `--tag`, it publishes the newest prepared `PK-<X.Y.Z>.md` found
there. Pass `--tag` whenever more than one is prepared, and read the "newest
prepared tag" line it prints before letting it publish.

**Where the tag lands is decided by the csproj, though.** The script tags the
commit whose `wincompose.csproj` declares that version, not `main`'s tip — and
since the release workflow checks out the tag, that is what makes the assets
carry the right number. A later merge drifting the csproj forward is then
harmless, and it says so.

If **no** commit declares the version, the bump has not merged and there is
nothing to pin to, so creating the release is refused. Merge the bump and
re-run. `--allow-version-mismatch` overrides, and publishes notes with no
assets — the release workflow refuses to attach assets whose version does not
match the tag. In a shallow clone the search can miss the commit, so
`git fetch origin --unshallow` comes before reaching for that flag.

Auth comes from `GH_TOKEN` / `GITHUB_TOKEN`, else `gh auth token`.

Expand All @@ -77,6 +90,14 @@ download. It never moves `Latest` backwards, so re-dispatching an old tag to
re-attach its assets cannot un-announce a newer release. Check the job went green;
if it could not push, its error says so and names the one-line edit.

⚠️ **Deleting a release does not un-set it.** The job asks whether the release
exists *before* it bumps, and nothing looks again afterwards. So a release deleted
after the job has run leaves `Latest` naming a version with no release — the
early-bump failure below, arriving by another route. Revert `status.txt` in the
same breath as the deletion, or re-create the release (see *If the release build
fails*). 0.9.20 was deleted and re-created on 7 October, and for the window in
between every install below 0.9.20 was offered the 0.9.19 download.

It was a manual step until 0.9.20, and it went wrong in both directions. Early,
every install is pointed at a release that is not there — and the `Installer:` /
`Portable:` URLs resolve `releases/latest`, so nobody gets a 404, they are handed
Expand All @@ -94,6 +115,18 @@ included. Fix the default branch, then start the workflow by hand
that already exists. Leaving `tag` empty builds the assets as artifacts and
releases nothing — that is the safe smoke test.

Two things about that dispatch are easy to get wrong:

- **It re-applies the notes from `release-notes`.** Whenever a `PK-<version>.md`
is on that branch, the dispatch runs `gh release edit --title --notes-file`, so
a title or body edited by hand on GitHub is thrown away. Fix the file on the
branch *first*, then dispatch. (With no notes file, the existing notes are kept
and only the assets are replaced.)
- **It creates the release when there is none** (`gh release create`), so the same
dispatch recovers a release that was deleted, not only one whose build failed.
It builds from the ref you dispatch on, normally `main`, never from the tag's
own commit — which is what the version assertion compares the tag against.

Building locally
----------------

Expand Down
Loading
Loading