Skip to content

feat(release): sign and publish Windows Velopack packages from the signing Mac - #2162

Merged
h4yfans merged 6 commits into
mainfrom
feat/velopack-release-pipeline
Sep 11, 2026
Merged

feat(release): sign and publish Windows Velopack packages from the signing Mac#2162
h4yfans merged 6 commits into
mainfrom
feat/velopack-release-pipeline

Conversation

@h4yfans

@h4yfans h4yfans commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator

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 release drives everything from the Mac.

publish-release.yml builds 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 and release-metadata-<version> with the draft tag, release id, resolved version and commit SHA. Nothing in CI touches the release any more. The dry_run input is gone, since one path was left and it has no side effects. The Homebrew cask bump moves to publish-homebrew-cask.yml, which pnpm release dispatches after it publishes.

New pnpm release -- --humanize --yes sequence

  1. Check the signing prerequisites (vpk, 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.
  2. Check the SimplySign session by listing the PKCS#11 keystore for alias 57D2B94F4B6C4356BD26F757F3855C1F. When it is absent, open SimplySign Desktop, print one instruction and wait for Enter.
  3. Humanize the draft release notes, show the plan, dispatch publish-release.yml and watch the run.
  4. Download the staged assets and the release metadata CI resolved.
  5. Unzip the Windows payload, fetch the previous release's MemryNote-<previous>-full.nupkg so vpk can emit a delta, re-check the SimplySign session, then run vpk '[win]' pack with --signTemplate scripts/sign-windows-binary.sh.
  6. Verify with osslsigncode that MemryNote-win-Setup.exe and the Memrynote.exe inside the full nupkg both report Signature verification: ok against the two committed Certum CA certificates. Abort before upload when either fails.
  7. Upload every asset to the draft with gh release upload --clobber, rename the draft to the publish-day tag, verify the asset set, publish.
  8. Dispatch the Homebrew cask bump, optionally dispatch the Velopack smoke test with --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. --restart discards it.

The one manual step

Logging in to SimplySign Desktop with the OTP from the phone app. --yes does 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 and MemryNote-<version>-win.zip keep existing installs updating through electron-updater; the Velopack set (MemryNote-win-Setup.exe, MemryNote-<version>-full.nupkg, a -delta.nupkg when 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.mjs carries the one-line switch for the app-side updater:

const skipVeloAppCheck = true

Flip it to false in the release that ships VelopackApp.build().run().

Deviations from the brief

The Velopack input is the existing MemryNote-<version>-win.zip, not a second win-unpacked artifact. 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. resolveUnpackedDir locates Memrynote.exe inside the extracted archive and fails loudly when the layout is neither the archive root nor a single nested directory.

velopack-smoke.yml gains an optional update_tag input. 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 --smoke dispatches 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.
  • Real vpk run of the generated argument vector against /tmp/velopack-proto/win-unpacked (1.4 GB, the shipped 2026.910.1 build) with --signTemplate omitted. Exit 0 in 71 s, emitting MemryNote-win-Setup.exe, MemryNote-2026.910.1-full.nupkg, releases.win.json and RELEASES, with 47 file(s) will not be signed plus 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 signed MemryNote-win-Setup.exe printed Signature verification: ok and Number of verified signatures: 1. This is the exact argument vector buildOsslsigncodeVerifyArgs produces.
  • The signing gate against the real keystore: keytool -list -keystore NONE -storetype PKCS11 with the PIN on stdin returned alias 57D2B94F4B6C4356BD26F757F3855C1F, and decideSigningSession reported ready. Read-only. Nothing was signed.
  • pnpm lint — 0 errors (one pre-existing warning in vault-switcher.tsx, untouched here). ESLint ignores scripts/, 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.
  • All three workflows parse as YAML. Everything below steps: in velopack-smoke.yml is unchanged from the proven velopack-windows-smoke branch apart from splitting the asset download across release_tag and update_tag.

Not verifiable here, for whoever reviews this:

  • No signed vpk pack was produced. The signing session belongs to Kaan.
  • No end-to-end pnpm release. The CI changes have not run on a runner.
  • No real *-win.zip was inspected, so the archive layout is handled rather than confirmed.

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.
@github-actions github-actions Bot added documentation Improvements or additions to documentation enhancement New feature or request test labels Sep 11, 2026
@codecov

codecov Bot commented Sep 11, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@h4yfans
h4yfans marked this pull request as ready for review September 11, 2026 19:19
@h4yfans
h4yfans merged commit 1e4f353 into main Sep 11, 2026
14 of 15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation enhancement New feature or request test

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant