Skip to content

ci: fail the build when primary.manifest drifts from app_strings.h - #10

Merged
bnayahu merged 4 commits into
mainfrom
ci/manifest-version-drift-check
Aug 11, 2026
Merged

ci: fail the build when primary.manifest drifts from app_strings.h#10
bnayahu merged 4 commits into
mainfrom
ci/manifest-version-drift-check

Conversation

@bnayahu

@bnayahu bnayahu commented Aug 11, 2026

Copy link
Copy Markdown
Owner

Closes #5

Problem

app_strings.h is the single source of truth for the version, and primary.rc picks it up through the preprocessor. But primary.rc references the manifest as an opaque file (1 RT_MANIFEST "primary.manifest"), so windres never preprocesses it and its assemblyIdentity version is a hardcoded literal that must be bumped by hand. Nothing enforced that.

Currently in sync; the risk is the next version bump.

Which option, and why

The issue listed three. This PR implements the CI assertion.

  • Leave it — keeps the manual checklist step as the only safeguard.
  • Generate the manifest from a template in build.shrejected. README.md documents a direct windres/g++ invocation that bypasses build.sh entirely, and build.yml has a step verifying that documented command still works. A generated manifest would either be missing on that path or silently stale, so this option trades a version-drift risk for a build-path divergence.
  • CI assertionchosen. Leaves the build path completely untouched, and turns drift into a loud failure.

Change

A source-only step comparing the manifest's assemblyIdentity version against MAJOR.MINOR.PATCH.0 from app_strings.h (mirroring what VERSIONINFO emits), added to:

  • build.yml — placed first, before the apt install and build, so drift fails in seconds on every push and PR.
  • release.yml — as a separate step from the existing tag gate, so a failure names which of the two is wrong. This is the step that keeps a mismatch from shipping.

The extraction uses per-macro greps rather than the order-dependent one-liner, so this new step isn't subject to #6.

The manifest comment now records that CI enforces the match.

Verification

Ran the check against mutated inputs — all five drift cases fail closed, and the in-sync tree passes:

Case want got result
current tree 1.0.0.0 1.0.0.0 pass
app_strings.h bumped, manifest left behind (the real scenario) 1.1.0.0 1.0.0.0 fail
manifest bumped, app_strings.h left behind 1.0.0.0 1.1.0.0 fail
three-part version by mistake 1.0.0.0 1.0.0 fail
version= attribute removed 1.0.0.0 (not found) fail

Also confirmed:

  • The pattern matches only the name="Primary" identity, not the Microsoft.Windows.Common-Controls dependency (exactly 1 match in the file).
  • Both workflows parse as valid YAML with all steps intact.
  • ./build.sh still succeeds and the manifest is still embedded (dpiAwareness, Common-Controls, asInvoker all present in the binary); the manifest is still well-formed XML.

Known limitation, documented in the manifest comment: the check expects name= and version= on the same line of the element. Reformatting that element makes the check fail rather than silently skip.

Assisted-By: Claude (Anthropic AI) noreply@anthropic.com

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>
@bnayahu
bnayahu force-pushed the ci/manifest-version-drift-check branch from cb6a634 to 67d3eea Compare August 11, 2026 18:42
@bnayahu
bnayahu merged commit 67d3eea into main Aug 11, 2026
1 check passed
@bnayahu
bnayahu deleted the ci/manifest-version-drift-check branch August 11, 2026 18:47
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.

resources/primary.manifest duplicates the version number (windres does not preprocess it)

1 participant