ci: publish releases through a draft so a failed upload cannot ship - #9
Merged
Conversation
Pressing OK could call CheckAndApplyAutoSwitch() three times: once in the auto-switch settings block, once in the base-mouse-count block, and once in the forced re-apply after the cached device state is discarded. Each call enumerates raw input devices, retrying GetRawInputDeviceList up to three times, so a single click cost up to three enumerations. The repeats were harmless — the direction is persisted before all three calls and ApplyMouseOrientation() sets an absolute value rather than toggling — but only the last call could do useful work in the common case, since the first two early-return on unchanged state. Remove the two earlier calls and keep the forced re-apply at the end of the handler. By that point every setting the check reads is persisted, so one pass applies them all; the first call previously ran before SetBaseMouseCount(), so it could not see a changed base count anyway. Dropping the base-mouse-count call also stops a re-apply from running when the auto-switch flag itself failed to persist: that call was gated on autoSwitchEnabled alone, not on autoSwitchWritten. The surviving call is gated on both, matching 3a00a42. Closes #2 Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Jonathan Bnayahu <bnayahu@il.ibm.com>
build.yml declared no permissions: block, so the workflow ran with the default GITHUB_TOKEN permissions. The job only checks out the repository, builds, and uploads an artifact, so read access to contents is all it needs. actions/upload-artifact authenticates with the Actions runtime token rather than GITHUB_TOKEN, so it is unaffected. Nothing was broken; this narrows the token to what the job actually uses. The comment warns against copying the block into release.yml, which genuinely needs contents: write for `gh release create`. Closes #3 Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Jonathan Bnayahu <bnayahu@il.ibm.com>
…order Both workflows derived the source version with a single alternation piped through `paste`: grep -oP 'APP_VERSION_(MAJOR|MINOR|PATCH)\s+\K\d+' ... | paste -sd. That takes the macros in the order they appear in the header rather than in semantic order. They currently appear as MAJOR, MINOR, PATCH on adjacent lines, so the result is correct today, but nothing enforced it — reordering them silently produced a transposed version string. The existing failure mode was fail-closed: release.yml's tag gate rejects a mismatch loudly rather than publishing something mislabelled. This makes the extraction correct in the first place, so a reorder is a non-event instead of a confusing release failure. Extract each component by name, and assert all three are non-empty — the old form would emit a short "1.2" if a macro went missing, which could then match a substring of the binary's version strings. Closes #6 Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Jonathan Bnayahu <bnayahu@il.ibm.com>
primary.rc references primary.manifest as an opaque file, so windres never runs the preprocessor over it and its assemblyIdentity version cannot be derived from APP_VERSION_*. It has to be bumped by hand, and nothing enforced that — the only safeguard was a manual release-checklist step. Assert it in CI instead: compare the manifest's assemblyIdentity version against MAJOR.MINOR.PATCH.0 from app_strings.h, in build.yml (every push and PR) and in release.yml (refuse to ship a mismatch). Drift now fails loudly instead of shipping silently. Of the options in the issue, this leaves the build path untouched. Generating the manifest from a template in build.sh was rejected because README.md documents a direct windres/g++ invocation that bypasses build.sh — build.yml verifies that command still works — so a generated manifest would break or stale out that documented path. The manifest comment now records that CI enforces the match, and that the check expects name= and version= on one line. Closes #5 Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Jonathan Bnayahu <bnayahu@il.ibm.com>
`gh release create` created the release object and uploaded both assets in
one invocation, so an upload that failed partway could leave a published
release with missing or truncated downloads. Every correctness gate (tag
and source version match, warning check, binary version assertion) already
runs before this point, so only a network or API failure could cause it —
but the window was real and users see published releases immediately.
Split publishing into three steps:
1. create the release as a --draft, with no assets
2. upload the assets (--clobber, so a rerun after a partial upload
replaces the incomplete asset instead of failing on "already exists")
3. verify both assets, then flip --draft=false
A draft is hidden from users and from the "latest release" API, so a
failure at step 1 or 2 no longer produces a visible partial release.
Step 3 accepts an asset only when GitHub reports state == "uploaded" and
its size matches the local file, which catches a truncated upload as well
as a missing one. Anything short of that fails the job with the release
still a draft.
Each step is now independently retryable, and recovery no longer needs a
re-tag: delete the leftover draft with `gh release delete "$TAG"` and rerun.
Closes #4
Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Jonathan Bnayahu <bnayahu@il.ibm.com>
bnayahu
force-pushed
the
ci/atomic-release-publish
branch
from
August 11, 2026 18:43
a04a3d4 to
3fe62db
Compare
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.
Closes #4
Problem
release.ymlpublished with a single call that both created the release object and uploaded its assets:If an upload failed partway, the release could exist — publicly, and as "latest" — with missing or incomplete assets. Every correctness gate already runs before this call, so a bad build can't get here; only a network or API failure can. But a published release is immediately visible, so the window mattered.
Change
The issue suggested draft → upload → publish. That's implemented, plus a gate before publishing:
--draft, no assets. Drafts are hidden from users and from the "latest release" API.--clobber, so a rerun after a partial upload replaces the incomplete asset instead of failing on "already exists".gh release edit --draft=false.Step 3 accepts an asset only when GitHub reports
state == "uploaded"and its reportedsizematches the local file. Presence alone would not catch a truncated upload, which is the exact failure mode the issue describes. If either asset falls short, the job fails with the release still a draft — never published.--verify-tagis retained on the create step.Recovery is simpler now
The issue's procedure required deleting the tag and re-tagging. A failure now leaves only a draft, and the tag is already correct:
This is recorded in a comment in the workflow.
Verification
release.ymlparses as valid YAML; step order is create-draft → upload → verify-and-publish.v1.0.0release: both assets matched exactly (Primary.exe121856/121856,SHA256SUMS.txt78/78) → would publish.exit 1and leave the release unpublished.Not covered: this can't be end-to-end tested without cutting a real tag, so the create/upload/edit sequence itself is exercised for the first time on the next release. The failure mode if something is wrong is a stuck draft — not a bad publish.
Assisted-By: Claude (Anthropic AI) noreply@anthropic.com