feat(release): sign and publish Windows Velopack packages from the signing Mac - #2162
Merged
Conversation
sign-windows-binary.sh is the Velopack --signTemplate target. It signs one Windows PE with the Certum SimplySign key through jsign and skips the non-win32 native prebuilds Velopack also hands it, which are ELF or Mach-O. The two public Certum CA certificates let osslsigncode verify a signature locally without fetching anything at release time.
The Windows Velopack packages must be Authenticode-signed with a Certum key that only exists in a hardware-backed keystore on the maintainer's Mac, so the runner cannot produce a complete release. publish-release.yml now builds every platform, validates the asset set, and stops at two workflow artifacts: memry-release-<version> with the assets and release-metadata-<version> with the draft tag, release id, resolved version and commit the local publisher needs. dry_run is gone with it. There was one path left and it has no side effects on the release. The cask bump moves to publish-homebrew-cask.yml, because the build no longer learns when the release goes public. pnpm release dispatches it after publish.
… Mac pnpm release now owns the whole release. It checks the signing prerequisites and the SimplySign session, dispatches and watches the CI build, downloads the staged artifacts, packs the Windows payload with vpk against scripts/sign-windows-binary.sh, verifies both the Setup.exe and the packaged Memrynote.exe with osslsigncode before anything is uploaded, then uploads, renames and publishes the draft. Every step is resumable. State lives in .release-state/<draft-tag>/state.json as an ordered step list plus the workflow run id, so a re-run after a failure attaches to a run that already succeeded instead of rebuilding for two hours. The PIN comes from the Keychain and reaches jsign through MEMRY_SIGN_PIN and keytool through stdin. It is never logged and never on a command line. Pure logic lives in scripts/velopack-release-utils.mjs behind unit tests: the resume decisions, the vpk argument vector, the asset set, the metadata parse and the session gate.
Installs a signed Setup.exe on windows-2022 with Defender on, checks the install layout, Authenticode status, Start menu shortcut and uninstall key, then applies a newer package with Update.exe and relaunches. The push trigger that only existed to make the workflow visible on a feature branch is gone, the inputs are required, and contents:read is enough because the release script dispatches it against a published release. update_tag is new: the installer and the newer package normally live on two different releases, so the smoke can exercise a real previous-to-current upgrade rather than reapplying one version to itself.
Covers the prerequisites, the ordered steps, the single OTP login, resuming a failed release, both Windows installer families during the transition, and the smoke test.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
h4yfans
marked this pull request as ready for review
September 11, 2026 19:19
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.
Summary
Windows moves to Velopack packages that are Authenticode-signed with the Certum SimplySign key. That key lives in a hardware-backed cloud keystore reachable only from Kaan's Mac, so GitHub Actions can no longer produce a complete release. CI becomes build-only and
pnpm releasedrives everything from the Mac.publish-release.ymlbuilds macOS (signed and notarized), Linux and Windows exactly as before, validates the asset set, and stops at two workflow artifacts:memry-release-<version>with the assets andrelease-metadata-<version>with the draft tag, release id, resolved version and commit SHA. Nothing in CI touches the release any more. Thedry_runinput is gone, since one path was left and it has no side effects. The Homebrew cask bump moves topublish-homebrew-cask.yml, whichpnpm releasedispatches after it publishes.New
pnpm release -- --humanize --yessequencevpk,jsign,osslsigncode,keytool, the SimplySign PKCS#11 library, the PKCS#11 config, the Certum chain, the Keychain PIN) and fail with the exact install command for whichever is missing.57D2B94F4B6C4356BD26F757F3855C1F. When it is absent, open SimplySign Desktop, print one instruction and wait for Enter.publish-release.ymland watch the run.MemryNote-<previous>-full.nupkgso vpk can emit a delta, re-check the SimplySign session, then runvpk '[win]' packwith--signTemplate scripts/sign-windows-binary.sh.osslsigncodethatMemryNote-win-Setup.exeand theMemrynote.exeinside the full nupkg both reportSignature verification: okagainst the two committed Certum CA certificates. Abort before upload when either fails.gh release upload --clobber, rename the draft to the publish-day tag, verify the asset set, publish.--smoke, print the Reddit follow-up.Every step is idempotent. Progress lives in
.release-state/<draft-tag>/state.json(gitignored) as an ordered step list plus the workflow run id, so a re-run after a failure attaches to a run that already succeeded instead of rebuilding for two hours.--restartdiscards it.The one manual step
Logging in to SimplySign Desktop with the OTP from the phone app.
--yesdoes not skip it, because it is a precondition rather than a confirmation. A session lasts about two hours and a CI build can outlive one, so the script re-checks right before it signs and may ask a second time.Transition
Windows keeps shipping both installer families. The NSIS
MemryNote-<version>-setup.exe,latest.yml, blockmaps andMemryNote-<version>-win.zipkeep existing installs updating through electron-updater; the Velopack set (MemryNote-win-Setup.exe,MemryNote-<version>-full.nupkg, a-delta.nupkgwhen the previous full package was available,releases.win.json,RELEASES) is what new installs and the Velopack updater use. The NSIS assets stop shipping once telemetry shows no NSIS installs remain.scripts/release.mjscarries the one-line switch for the app-side updater:Flip it to
falsein the release that shipsVelopackApp.build().run().Deviations from the brief
The Velopack input is the existing
MemryNote-<version>-win.zip, not a secondwin-unpackedartifact. The zip is the same content and is already built, staged and published, so a second 1.4 GB upload and download per release buys nothing.resolveUnpackedDirlocatesMemrynote.exeinside the extracted archive and fails loudly when the layout is neither the archive root nor a single nested directory.velopack-smoke.ymlgains an optionalupdate_taginput. The installer and the newer package normally live on two different releases, so without it the smoke could only reapply one version to itself.pnpm release --smokedispatches the previous release's Setup.exe against this release's package, and skips when the previous release predates Velopack.Release note
none
Test plan
Run on this Mac against real artifacts. No CI run was dispatched and no release was created, uploaded to or published.
node --test scripts/*.test.mjs— 183 pass, 0 fail. 64 of those cover the new pure logic: resume decisions, the vpk argument vector, asset assembly, metadata and tag parsing, and the session gate.vpkrun of the generated argument vector against/tmp/velopack-proto/win-unpacked(1.4 GB, the shipped 2026.910.1 build) with--signTemplateomitted. Exit 0 in 71 s, emittingMemryNote-win-Setup.exe,MemryNote-2026.910.1-full.nupkg,releases.win.jsonandRELEASES, with47 file(s) will not be signedplus the setup bundle, which is the set the sign template would cover.osslsigncode verify -CAfile <the two committed Certum CAs> -TSA-CAfile <same>against the prototype's signedMemryNote-win-Setup.exeprintedSignature verification: okandNumber of verified signatures: 1. This is the exact argument vectorbuildOsslsigncodeVerifyArgsproduces.keytool -list -keystore NONE -storetype PKCS11with the PIN on stdin returned alias57D2B94F4B6C4356BD26F757F3855C1F, anddecideSigningSessionreported ready. Read-only. Nothing was signed.pnpm lint— 0 errors (one pre-existing warning invault-switcher.tsx, untouched here). ESLint ignoresscripts/, so the new scripts are covered by Prettier and the unit tests instead.pnpm docs:impact --base origin/main --strict,pnpm docs:build,git diff --check— all pass.steps:invelopack-smoke.ymlis unchanged from the provenvelopack-windows-smokebranch apart from splitting the asset download acrossrelease_tagandupdate_tag.Not verifiable here, for whoever reviews this:
vpkpack was produced. The signing session belongs to Kaan.pnpm release. The CI changes have not run on a runner.*-win.zipwas inspected, so the archive layout is handled rather than confirmed.