Files
enricobuehler c7d0fd2e03
apple / swift (pull_request) Successful in 1m59s
apple / distribute (pull_request) Skipped
apple / screenshots (pull_request) Skipped
ci / bun-nix (pull_request) Successful in 18s
android / android (pull_request) Successful in 11m19s
ci / rust-arm64 (pull_request) Successful in 7m47s
windows-client / client (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (pull_request) Successful in 3m18s
ci / web (pull_request) Successful in 10m42s
windows-client / client (x64, , x86_64-pc-windows-msvc, C:\t) (pull_request) Successful in 6m53s
ci / docs-site (pull_request) Successful in 11m14s
nix / flake (pull_request) Successful in 15m44s
ci / rust (pull_request) Successful in 1h0m25s
release: 0.28.0 — version bump, notes, CHANGELOG, Play notes
180 commits since v0.27.0. Cut from origin/main 9c133350.

THE NUMBER: 0.28.0, not 0.27.1. The CHANGELOG's in-development section was
titled "v0.27.1", which the release does not support — 17 `feat(...)` commits,
a packager-visible default flip (GameStream opt-in on every route), the
edition-2024 MSRV rise, and now a genuinely BREAKING host change (the built-in
library scanners are deleted). `scripts/ci/pf-version.sh`'s canary rule agrees
independently: CI already stamps canaries `0.28.<run>`.

TWO DEFECTS FOUND AND FIXED WHILE PREPARING, both pre-existing on main:

1. C ABI_VERSION was stale at 18. Two exported symbols landed since v0.27.0
   without a bump — punktfunk_connection_note_frame_index_ex and
   punktfunk_reanchor_gate_arm_expecting_drops (72 -> 74 declarations in
   include/punktfunk_core.h). The constant's own doc history makes the rule
   explicit: v17 and v18 each bumped for adding exactly one symbol. Bumped to 19
   with its doc entry; the header is regenerated (cbindgen, CI-gated) and the
   C ABI harness passes printing abi_version=19.

2. docs-site/public/openapi.json had drifted to 0.21.0 against api/openapi.json,
   missing five endpoints. The copy is a documented manual step that nothing in
   CI enforces (CONTRIBUTING.md says so outright). Re-synced — and then it
   DRIFTED AGAIN inside this same cycle when the scanner-removal regen updated
   api/openapi.json alone, so it is re-synced a second time and the CHANGELOG
   now says to treat the copy as part of regenerating, not a follow-up.

 The final docs batch also invalidated a line in this CHANGELOG: the identity
section still said the P-256 key was "generated by ring via rcgen", which contradicted
this same document's "ring is gone from the tree entirely". Corrected to "rcgen on the
workspace's aws-lc-rs backend", matching 92db6651.

api/openapi.json stays stamped 0.27.0: it cannot be regenerated here
(punktfunk-host does not compile on macOS) and does not need to be — the drift
test normalizes info.version, so only the SURFACE is gated, and the surface is
current.

CHANGELOG: retitled to v0.28.0, gained the version table (wire 2 unchanged; C
ABI 18->19; edition 2021->2024 and MSRV 1.82->1.85; driver protocol 6 and
gamepad channel 3 unchanged; plugin-kit 0.4.0->0.4.1), a breaking-changes
section, and ~29 topics the in-development text predated — including the four
that landed last: the scanner->plugin migration, the Mutter rebuild
serialization, the KWin <=60 Hz readback, and the Apple/Android de-prime fuse.

 THE BREAKING ONE, stated plainly in both halves: the six built-in library
scanners are DELETED and the library is assembled entirely by plugins. There is
deliberately no migration — a plugin claims its store and republishes each title
under the same `<store>:<external_id>` id, so entry ids, GameStream app ids, art
caches, Moonlight pins, per-source toggles and per-entry hides all keep working.
The one visible consequence, and the whole upgrade note: a host with NO library
plugins installed has an empty grid.

 The Mutter two-client segfault this release now fixes (a5c9b7b8) is the one
found during THIS release's on-glass validation: chaining two clients through a
kept display killed gnome-shell in meta_monitor_manager_rebuild. It was A/B'd on
.21 against the released 0.27.0 and shown byte-identical there, so it was never
a 0.28.0 regression — and the fix's own commit message cites that A/B.

GATES RUN, all green on this commit (re-run after the rebase onto 86cbbea0):
  cargo fmt --all --check            clean
  cargo metadata --locked            OK against the new dependency tree
  Cargo.lock                         versions-only vs origin/main, 36/36 lines
  cargo test -p punktfunk-core       210 passed
  c_abi harness                      PASS, abi_version=19 (needs LIBRARY_PATH
                                     for opus on macOS; a link path, not a defect)
  docs-site build                    exit 0 (bun install --frozen-lockfile + build)
  Play notes gate                    440/500 CHARACTERS, not byte-identical to
                                     any other release (`•` is 3 bytes — count
                                     characters, as the gate does)
  notes voice check                  0 hits above `## For developers`; TL;DR at
                                     6 bullets (README caps it at six)

ON-GLASS (against the canary of 14425716, code-identical bar ABI_VERSION):
Windows .173 0.28.13309 + Android and iPad, Linux .21 0.28.0-0.00013300 +
iPhone — both PASS. The idle sleep-blocker fix is proven before/after on .173
(`powercfg /requests` SYSTEM: the mic devnode -> "Keine."), and the GameStream
flip is proven at the socket level on .21 (47984/47989/47999 absent by default,
restored by PUNKTFUNK_GAMESTREAM=1). Old-client compat holds: Android 0.26.0
streams against the 0.28.0 host.

 NOT re-validated: the Mutter fix itself. .21 (VM 103) is stopped — it and
home-bazzite-2 (VM 119, currently running) share one passed-through GPU, so
bringing .21 up would stop the other VM. Owed once .21 is free; the repro is
iPhone 2868x1320 -> SIGTERM -> Android 2800x1260, and the marker to confirm the
build carries the fix is the string "mutter: waited out a monitor-topology
rebuild before releasing the lock".

NOT INCLUDED: the 14 unpushed pf-capture/pf-vdisplay sweep commits on the local
main. Never through CI; pushing them is the user's call.
2026-08-13 18:06:18 +02:00
..

Release notes

One file per stable release: docs/releases/vX.Y.Z.md. Its contents become the Gitea release body verbatim and are the source of the Discord #releases announcement.

Why this exists

Releases used to be created by CI with an empty body; the notes were pasted in by hand afterward. That left a window where the release — and anything announcing it — carried no notes. Now the notes are authored before the tag is pushed, as part of the version bump, so the release is born complete and the announcement always has something to say.

The flow

  1. Write the notes. Add docs/releases/vX.Y.Z.md in the same commit (or PR) as the version bump. Copy TEMPLATE.md and fill it in. This file is the single source of truth for the body.
  2. Tag & push. git tag -a vX.Y.Z … && git push origin vX.Y.Z fans out to the build workflows. Whichever one wins the create race seeds the release body from this file (scripts/ci/gitea-release.shensure_release, and its PowerShell twin). The release page shows the notes immediately.
  3. Wait for green. Let every platform's CI finish and go green.
  4. Announce. Dispatch the announce workflow (.gitea/workflows/announce.yml) with the tag. It re-asserts this file over the live release (so any late edit wins) and posts an embed to Discord #releases. Pressing "go" is the quality gate — a half-built release is never announced. Stable-only; a -rc tag is refused unless allow_prerelease=true.

Editing the notes after the tag is fine: update this file, then re-run step 4 (or PATCH the body via the API) — the announce step always re-syncs from the file, so the file stays authoritative even across a tag re-point.

Canary / -rc builds have no file here on purpose: they get no curated body and are not announced.

Google Play "What's new": whatsnew/vX.Y.Z.txt

Play shows its own release notes on the Play Store listing and in the Play Store app, and caps them at 500 characters per language — the vX.Y.Z.md body is two orders of magnitude too long, so it gets its own short file: docs/releases/whatsnew/vX.Y.Z.txt.

Write it for a phone/TV user, not a host operator: only what changed in the Android app is worth their 500 characters. Plain text (Play renders no markdown), one bullet per line, same voice rules as below. Copy whatsnew/TEMPLATE.txt.

A vX.Y.Z tag without this file fails the android job before it builds. This is a hard gate, not a warning, because the failure it prevents is silent: when the file is missing Play does not show an empty "What's new" — it carries the previous release's text onto the new version, so the store listing describes a build nobody is getting, and nothing surfaces that but reading the listing. It is the same shape as the v0.22.3 notes announcing a feature that release never contained. The gate also rejects a file byte-identical to another release's, which is that bug reached by copy-paste instead of by omission.

The gate runs first in the job, so a miss costs a second and leaves nothing half-published — no build, no assets on the Gitea release, nothing on Play. Two more checks sit downstream: play-upload.py refuses text over the 500-char cap (printing the real count) before it uploads, because the API only rejects oversized notes at commit, by which point the AAB is already on Play.

Canary is exempt: it has no curated notes, and Play reusing text for internal testers costs nothing.

Same freeze rule as the notes: once the tag exists, this file is the record of what that versionCode shipped.

Voice & format

Write for the people who USE Punktfunk to stream their games and desktops — not for the people who build it. A non-engineer should finish knowing what's new and whether it affects them; an engineer should never be confused or forced to decode internals. (See any recent vX.Y.Z.md for the target.)

  1. Lead with the benefit. Each entry = what the user can now do, what now works, or what stopped going wrong — in their words. Implementation is not the story.
  2. No internal vocabulary in the body. No protocol/message names, code type names, hex codes or hardware IDs, crate/component names, or API symbols. Translate any essential detail to plain language. Name things users recognize (iPad, Apple Pencil, Steam Deck, Android TV, the Windows sign-in screen) — not subsystems.
  3. Group as New / Improved / Fixed, each a bold one-line lead-in + a tight plain explanation. Skimmable. The lead-in text before the first ## is what the Discord announcement shows, so make it a real, plain-language summary.
  4. Be specific and honest — no vague "various improvements"; a reader should know exactly what changed.
  5. Compatibility line up top, in plain terms: can they update one side at a time? does their existing setup keep working? No version numbers in the lead.
  6. No protocol / ABI / driver / embedder detail in this file at all. It goes in the root CHANGELOG.md (see below), and the notes carry a single short ## For developers section linking there. Nothing else in vX.Y.Z.md may use an internal name.
  7. Open with a ## TL;DR — three to six bullets naming only what most readers would be sorry to miss, each one line. A large release is exactly where a reader gives up, and the TL;DR is what they read instead of giving up. If something needs the reader to act, it belongs here and in ## Before you update, not buried in ## Fixed.

The technical half: root CHANGELOG.md

Why it is separate. Through v0.24.0 the engineering detail lived in an ## Under the hood (for developers) section at the bottom of each release's notes. That worked while releases were small. It stopped working: v0.25.0 is 300+ commits, and the section had grown long enough to bury the user-facing half it was appended to — the exact failure the voice rules exist to prevent. The two audiences also want different shapes. A user reads one release and wants prose; an embedder wants to diff across releases and see when the ABI moved, which is a table, not a paragraph.

So: vX.Y.Z.md is for people who use Punktfunk, CHANGELOG.md is for people who build against it, and neither has to compromise for the other.

Format. Newest release first, one ## vX.Y.Z section each. Lead with a version table (wire protocol, C ABI, driver protocol, gamepad channel — every row, marked unchanged where it did not move, because "unchanged" is the answer an embedder most often needs). Then breaking changes, then whatever else matters: capability bits, new environment variables, wire additions, workspace members. Internal names are the point here — use them.

Linking. The notes link to the file at the tag, not at main: https://git.unom.io/unom/punktfunk/src/tag/vX.Y.Z/CHANGELOG.md. A release's notes are frozen; a link to main would silently start describing a later release.

Same freeze rule. Add the release's section in the version-bump commit, alongside the notes.

The short annotated-tag message stays separate and short (a headline + a paragraph); it is the tag object's message, not this file.