Publish releases from one job; correct the server docs and crate metadata - #191
Merged
Merged
Conversation
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>
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>
This was referenced Sep 25, 2026
Merged
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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-releaseandgenerate_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:Now the build jobs only build, self-check and upload their files as run artifacts. A final
publishjob:needs), so a failed target publishes nothing rather than an incomplete release;.sha256(this also runs in a rehearsal);release not foundcounts 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;fail_on_unmatched_files: true.This also closes a window. The first job to finish used to create the release and make it
releases/latestwhile the other platforms were still building. For a few minutes,scripts/install.shandtwcore upgradefound no file for those platforms.Asset names are unchanged. The list now lives in the
publishjob, andupgrade::tests::the_release_workflow_publishes_what_upgrade_looks_forstill 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_dispatchon 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.mdsaid the desktop app keeps the control key in the Mac's keychain. ThinkWatch Lite 2026.9.16 stores remote connection keys inconnection-keys.jsonin its data directory. On macOS and Linux the file is0600in a0700directory. On Windows the directory's protected DACL admits only the current user and SYSTEM. The app does not use the system keychain.docs/server.zh-CN.mdgets the same corrections.CONTRIBUTING.mdno longer calls the remote control port a transport "to come". It shipped in v0.47.0.CONTRIBUTING.md, Cutting a release:release.ymlchecks 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 gainstw-link.Cargo metadata
[workspace.package]now setshomepage = "https://thinkwat.ch/core/"(the canonical URL;/coreredirects there),documentation = "https://thinkwat.ch/docs/core/"andreadme = "README.md". Every member inherits them. The crates are not on crates.io, so there is no docs.rs page. (The nametw-apion crates.io belongs to an unrelated Twitch crate.)keywords = ["ai-gateway", "llm", "openai", "anthropic", "gemini"]andcategories = ["network-programming", "web-programming::http-server"]are inherited only bytwcore,tw-gatewayandtw-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-controldescription: it serves a unix socket, a loopback port on Windows, and an optional remote control port.twcoredescription: "Self-contained AI gateway: the local engine behind ThinkWatch Lite, or a standalone gateway on a server".twcore --helpnow 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 --workspacepass locally (1581 passed, 4 ignored), with the proxy variables unset for the tests.actionlintfinds nothing inrelease.yml.cargo metadataresolves the inherited fields as listed above.🤖 Generated with Claude Code