Skip to content

Publish releases from one job; correct the server docs and crate metadata - #191

Merged
fylorn merged 5 commits into
mainfrom
fix/release-notes-once-and-remote-docs
Sep 25, 2026
Merged

fylorn merged 5 commits into
mainfrom
fix/release-notes-once-and-remote-docs

Conversation

@fylorn

@fylorn fylorn commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Three factual fixes.

Release notes are written once (release.yml)

The v0.47.0 release body held GitHub's generated notes four times. Every build job attached its own files with softprops/action-gh-release and generate_release_notes: true. When the release already exists, the action asks GitHub for the generated notes again and appends them to the body it found (prepareReleaseMutation: ${body}\n\n${notes}), so each job after the first added a copy. The copy count matches the job count in each release:

Releases Jobs publishing Copies
v0.44.0 – v0.47.0 macOS, Windows, Linux x86_64, Linux aarch64 4
v0.30.0 – v0.43.0 macOS, Windows 2
v0.11.0 one job, but the release was created by hand before the workflow ran 2
all others one 1

Now the build jobs only build, self-check and upload their files as run artifacts. A final publish job:

  • waits for every build (needs), so a failed target publishes nothing rather than an incomplete release;
  • checks that exactly the listed files arrived and that each matches its .sha256 (this also runs in a rehearsal);
  • asks for generated notes only when the release does not exist yet, so re-running the job after a failed upload, or tagging after creating a release by hand, does not add a second copy. Only gh's own release not found counts as missing: a network error, a bad token or a rate limit fails the step, with gh's message in the log, instead of generating a second copy for a release that exists;
  • writes the release once, with fail_on_unmatched_files: true.

This also closes a window. The first job to finish used to create the release and make it releases/latest while the other platforms were still building. For a few minutes, scripts/install.sh and twcore upgrade found no file for those platforms.

Asset names are unchanged. The list now lives in the publish job, and upgrade::tests::the_release_workflow_publishes_what_upgrade_looks_for still finds every name there. The notes are still GitHub's generated list. The old comment said they came from the tag message, but the code never did that, so the comment is replaced.

Rehearsal (workflow_dispatch on this branch, publishes nothing): https://github.com/ThinkWatchProject/ThinkWatch-Core/actions/runs/36047214434. It runs every build, the artifact hand-off and the file and checksum check. It does not run the two tag-only steps. The release-exists step was run locally with gh 2.93.0 against v0.47.0 (exists: generate=false), a missing tag (generate=true), a bad token (HTTP 401: the step fails) and an unreachable proxy (the step fails).

The existing v0.47.0 release has already been edited to hold one copy of its notes. The three duplicates were removed and nothing else changed. Earlier releases with duplicates are unchanged.

Server docs: any desktop OS, and where the key is kept

  • docs/server.md said the desktop app keeps the control key in the Mac's keychain. ThinkWatch Lite 2026.9.16 stores remote connection keys in connection-keys.json in its data directory. On macOS and Linux the file is 0600 in a 0700 directory. On Windows the directory's protected DACL admits only the current user and SYSTEM. The app does not use the system keychain.
  • The page spoke only of a Mac. Lite on macOS, Windows or Linux connects to a remote core, so the wording is now OS-neutral. "This computer's address" matches the app's own message for a refused connection.
  • docs/server.zh-CN.md gets the same corrections.
  • CONTRIBUTING.md no longer calls the remote control port a transport "to come". It shipped in v0.47.0.
  • CONTRIBUTING.md, Cutting a release: release.yml checks that each binary is built for its target and, where the runner can execute it, that it starts and reports the version on the tag. The Windows arm64 binary is cross-compiled on an x64 runner, so only its PE machine field is checked; the Linux binaries are also held to glibc 2.35. The list of crates the desktop app pins gains tw-link.

Cargo metadata

  • [workspace.package] now sets homepage = "https://thinkwat.ch/core/" (the canonical URL; /core redirects there), documentation = "https://thinkwat.ch/docs/core/" and readme = "README.md". Every member inherits them. The crates are not on crates.io, so there is no docs.rs page. (The name tw-api on crates.io belongs to an unrelated Twitch crate.)
  • keywords = ["ai-gateway", "llm", "openai", "anthropic", "gemini"] and categories = ["network-programming", "web-programming::http-server"] are inherited only by twcore, tw-gateway and tw-control. They describe the gateway, and a crate such as the YAML patch layer is not an HTTP server. Both lists follow crates.io's rules: at most five keywords, and category slugs from its list.
  • tw-control description: it serves a unix socket, a loopback port on Windows, and an optional remote control port.
  • twcore description: "Self-contained AI gateway: the local engine behind ThinkWatch Lite, or a standalone gateway on a server". twcore --help now takes its first line from it (#[command(about)]) instead of a separate string that still spoke only of ThinkWatch Lite.

Checks

cargo fmt --all && cargo clippy --workspace --all-targets -- -D warnings && cargo test --workspace pass locally (1581 passed, 4 ignored), with the proxy variables unset for the tests. actionlint finds nothing in release.yml. cargo metadata resolves the inherited fields as listed above.

🤖 Generated with Claude Code

fylorn and others added 3 commits September 25, 2026 03:16
Every build job attached its own files with softprops/action-gh-release
and generate_release_notes. When the release already exists, the action
asks GitHub for the generated notes again and appends them to the body
it found, so each job after the first added another copy: four in
v0.44.0 to v0.47.0 (macOS, Windows, two Linux legs), two in v0.30.0 to
v0.43.0. v0.11.0 has two for the same reason: that release was created
by hand before the workflow ran.

The build jobs now only build, check and upload their files as run
artifacts. A final publish job waits for all of them, checks that
exactly the listed files arrived and that each matches its .sha256, and
writes the release once. It asks for generated notes only when the
release does not exist yet, so re-running it after a failed upload does
not add a second copy.

Publishing in one step also closes a window: the first job to finish
used to create the release, making it `releases/latest` while the other
platforms were still building, so install.sh and `twcore upgrade` found
nothing for those platforms for a few minutes. A failed target now
publishes nothing rather than an incomplete release.

The asset list lives in the publish job, where the upgrade test that
reads release.yml still finds every file name it looks for.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… a file

docs/server.md said the desktop app keeps the control key in the Mac's
keychain. ThinkWatch Lite stores remote connection keys in a file in its
data directory that only the user running the app can read (0600 in a
0700 directory on macOS and Linux; on Windows the directory's protected
DACL admits only the current user and SYSTEM) and does not use the
system keychain.

The page also spoke only of a Mac, while Lite on macOS, Windows and
Linux connects to a remote core. The wording is now OS-neutral; "this
computer's address" matches the app's own message for a refused
connection. The Chinese page gets the same corrections.

CONTRIBUTING.md still called the remote control port a transport "to
come"; it shipped in v0.47.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the gateway

The workspace gains homepage (https://thinkwat.ch/core/, the canonical
form of /core), documentation and readme, and every member inherits
them. The crates are not published to crates.io -- the desktop app and
ThinkWatch Enterprise take them from git by tag -- so there is no docs.rs
page; documentation points at the project's docs instead.

Keywords and categories describe the gateway itself, so only twcore,
tw-gateway and tw-control inherit them; a YAML patch layer or a directory
watcher is not an HTTP server. Both follow crates.io's rules: five
keywords at most, and categories from its list of slugs.

Two descriptions were out of date. tw-control no longer serves only a
unix socket: Windows uses a loopback port, and there is an optional
remote control port. twcore runs as the local gateway behind ThinkWatch
Lite or on its own on a server.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fylorn and others added 2 commits September 25, 2026 08:51
The publish job asks `gh release view` whether the release exists, and
it treated any failure as "no". A network error, a bad token or a rate
limit then generated the notes again for a release that did exist,
which is the duplicate this job is there to prevent.

Now only gh's own `release not found` means the release is missing. Any
other failure fails the step, with gh's message in the log. Tried
locally with gh 2.93.0 against an existing tag, a missing tag, a bad
token (HTTP 401) and an unreachable proxy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rom the crate description

CONTRIBUTING said release.yml checks that every binary runs and reports
the version on the tag. The Windows arm64 binary is cross-compiled on an
x64 runner and cannot run there, so only its PE machine field is
checked. The step now says that, and lists the glibc 2.35 ceiling the
Linux binaries are held to.

The desktop app also pins tw-link, which carries the control-channel
handshake.

`twcore --help` still described only the local engine behind ThinkWatch
Lite. Its first line now comes from the crate description, and the
description is worded to read well in both places: "Self-contained AI
gateway: the local engine behind ThinkWatch Lite, or a standalone
gateway on a server".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@fylorn
fylorn merged commit e626ec6 into main Sep 25, 2026
9 checks passed
@fylorn
fylorn deleted the fix/release-notes-once-and-remote-docs branch September 25, 2026 01:46
fylorn added a commit that referenced this pull request Sep 25, 2026
…summary

#191 made the publish job write the release once, with GitHub's
generated list of pull requests as its only text and the tag as its
title. A release is now titled "ThinkWatch Core <version>", and its text
has, in order:

- an English summary from release-notes/<version>.md, when that file
  exists;
- a table of the files for each platform;
- the commands that install this version on a Linux server and switch
  an existing installation to it (install.sh --version, twcore upgrade
  --version --restart);
- how to verify a download against its .sha256;
- GitHub's generated list of pull requests.

scripts/release_notes.py builds the text; the publish job fetches the
generated list itself (releases/generate-notes) and hands the finished
text to action-gh-release, which no longer generates anything. The
"only once" rule from #191 stays: a release that already exists keeps
its text. A rehearsal (workflow_dispatch) now writes the text into the
run summary, using the version in Cargo.toml.

scripts/release_notes_test.py checks that the table links exactly the
files in release.yml's FILES list, that the install and upgrade options
exist, and that every file in release-notes/ renders; the Linux CI job
runs it before compiling.

release-notes/0.47.0.md summarizes 0.47.0 from #183-#190; the live
v0.47.0 release page now carries it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fylorn added a commit that referenced this pull request Sep 25, 2026
v0.48.0 was published before the template landed, so its page holds
only GitHub's generated list. release-notes/0.48.0.md summarizes it
from #191-#198 so the page can be rewritten from the template:

- upgrade notes: CONTROL_API_VERSION 21, so ThinkWatch Lite 2026.9.16
  (core 0.47.0, protocol 20) does not connect to it and a server used
  with that app stays on 0.47.0; the request store's schema 20, which
  empties the request history on the first start (and again on the way
  back); the /in-flight shape, RequestStarted.session and the removed
  ChatgptUsage fields;
- session and route on the start event, requests a rule decided
  without an upstream, the replayable /in-flight snapshot and the new
  /live fields, and /summary/routes (#197);
- per-group unpriced and no-usage counts, and security log totals
  (#193);
- the signed-in account on a ChatGPT account upstream (#195);
- releases published from one job, the server guide and the crate
  metadata (#191), and the test port fix (#192).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fylorn added a commit that referenced this pull request Sep 25, 2026
…summary

#191 made the publish job write the release once, with GitHub's
generated list of pull requests as its only text and the tag as its
title. A release is now titled "ThinkWatch Core <version>", and its text
has, in order:

- an English summary from release-notes/<version>.md, when that file
  exists;
- a table of the files for each platform;
- the commands that install this version on a Linux server and switch
  an existing installation to it (install.sh --version, twcore upgrade
  --version --restart);
- how to verify a download against its .sha256;
- GitHub's generated list of pull requests.

scripts/release_notes.py builds the text; the publish job fetches the
generated list itself (releases/generate-notes) and hands the finished
text to action-gh-release, which no longer generates anything. The
"only once" rule from #191 stays: a release that already exists keeps
its text. A rehearsal (workflow_dispatch) now writes the text into the
run summary, using the version in Cargo.toml.

scripts/release_notes_test.py checks that the table links exactly the
files in release.yml's FILES list, that the install and upgrade options
exist, and that every file in release-notes/ renders; the Linux CI job
runs it before compiling.

release-notes/0.47.0.md summarizes 0.47.0 from #183-#190; the live
v0.47.0 release page now carries it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fylorn added a commit that referenced this pull request Sep 25, 2026
v0.48.0 was published before the template landed, so its page holds
only GitHub's generated list. release-notes/0.48.0.md summarizes it
from #191-#198 so the page can be rewritten from the template:

- upgrade notes: CONTROL_API_VERSION 21, so ThinkWatch Lite 2026.9.16
  (core 0.47.0, protocol 20) does not connect to it and a server used
  with that app stays on 0.47.0; the request store's schema 20, which
  empties the request history on the first start (and again on the way
  back); the /in-flight shape, RequestStarted.session and the removed
  ChatgptUsage fields;
- session and route on the start event, requests a rule decided
  without an upstream, the replayable /in-flight snapshot and the new
  /live fields, and /summary/routes (#197);
- per-group unpriced and no-usage counts, and security log totals
  (#193);
- the signed-in account on a ChatGPT account upstream (#195);
- releases published from one job, the server guide and the crate
  metadata (#191), and the test port fix (#192).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fylorn added a commit that referenced this pull request Sep 25, 2026
…summary (#196)

* ci(release): write the release page from a template, with an English summary

#191 made the publish job write the release once, with GitHub's
generated list of pull requests as its only text and the tag as its
title. A release is now titled "ThinkWatch Core <version>", and its text
has, in order:

- an English summary from release-notes/<version>.md, when that file
  exists;
- a table of the files for each platform;
- the commands that install this version on a Linux server and switch
  an existing installation to it (install.sh --version, twcore upgrade
  --version --restart);
- how to verify a download against its .sha256;
- GitHub's generated list of pull requests.

scripts/release_notes.py builds the text; the publish job fetches the
generated list itself (releases/generate-notes) and hands the finished
text to action-gh-release, which no longer generates anything. The
"only once" rule from #191 stays: a release that already exists keeps
its text. A rehearsal (workflow_dispatch) now writes the text into the
run summary, using the version in Cargo.toml.

scripts/release_notes_test.py checks that the table links exactly the
files in release.yml's FILES list, that the install and upgrade options
exist, and that every file in release-notes/ renders; the Linux CI job
runs it before compiling.

release-notes/0.47.0.md summarizes 0.47.0 from #183-#190; the live
v0.47.0 release page now carries it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* release-notes: summarize 0.48.0

v0.48.0 was published before the template landed, so its page holds
only GitHub's generated list. release-notes/0.48.0.md summarizes it
from #191-#198 so the page can be rewritten from the template:

- upgrade notes: CONTROL_API_VERSION 21, so ThinkWatch Lite 2026.9.16
  (core 0.47.0, protocol 20) does not connect to it and a server used
  with that app stays on 0.47.0; the request store's schema 20, which
  empties the request history on the first start (and again on the way
  back); the /in-flight shape, RequestStarted.session and the removed
  ChatgptUsage fields;
- session and route on the start event, requests a rule decided
  without an upstream, the replayable /in-flight snapshot and the new
  /live fields, and /summary/routes (#197);
- per-group unpriced and no-usage counts, and security log totals
  (#193);
- the signed-in account on a ChatGPT account upstream (#195);
- releases published from one job, the server guide and the crate
  metadata (#191), and the test port fix (#192).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* release-notes(0.48.0): say where the key is kept without naming the keychain

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant