14 KiB
CTMS Web and Desktop Release Guide
Release Unit
CTMS uses one repository, one promotion path, and one product version. Web and Desktop are build targets from the same source commit, not separately versioned products.
The release identity consists of:
- semantic version, for example
1.8.0 - Git tag, for example
v1.8.0 - full Git commit SHA
- build channel:
dev,main, orrelease - client type:
webordesktop
Runtime Boundary
Shared Vue business code lives under frontend/src/. Platform decisions are
exposed through frontend/src/runtime/index.ts.
The runtime contract currently provides:
- API base URL resolution
- Web, macOS, Windows, and Linux runtime identification
- app version, source commit, build channel, and client type metadata
- explicit capability flags
- Desktop server address configuration
- secure session storage
- file picker/save/open adapters
- desktop system notification adapters
- desktop updater adapters
- native desktop application menu and shortcut synchronization
- desktop window lifecycle and system appearance synchronization
Business modules must use this public runtime entry point. They must not inspect
Tauri globals or import Tauri packages directly. Native files, notifications,
secure session storage, and automatic updates must remain behind
frontend/src/runtime/ and explicit capability flags.
Desktop menu accelerators and window appearance are synchronized through narrow
Tauri commands owned by frontend/src/runtime/. macOS close/reopen behavior is
handled by the Tauri event loop so a genuinely closed main window can be rebuilt from the Dock;
these concerns must not move into Vue business modules.
Version Change
Update every client manifest with one command:
cd frontend
npm run version:set -- 1.8.0
npm run version:check
This synchronizes:
frontend/package.jsonfrontend/package-lock.jsonfrontend/src-tauri/tauri.conf.jsonfrontend/src-tauri/Cargo.tomlfrontend/src-tauri/Cargo.lock
Manual edits that leave these files inconsistent fail CI.
Stabilization Gates
Client builds require Node.js 22.13.0 or newer. The development frontend container, Web CI, macOS and Windows release candidates, Windows internal validation, and the production Web image use the same Node 22.13 baseline.
Client release candidates must pass the shared Web checks and the Desktop release/security gate before promotion:
cd frontend
npm run version:check
npm run release:env:check
npm run runtime:check
npm run desktop:release:check
npm run ui:contract
npm run type-check
npm run test:unit
npm run build
npm run desktop:build:app
release:env:check verifies build channel and commit metadata, and becomes
platform-strict with REQUIRE_DESKTOP_SIGNING=true on macOS or
REQUIRE_WINDOWS_SIGNING=true on Windows. Formal native jobs also set
REQUIRE_UPDATER_SIGNING=true, DESKTOP_RELEASE_PLATFORM, and the
version-derived DESKTOP_PLATFORM_SIGNING_MODE. The exact-version policy in
frontend/desktop-release-policy.json is checked with
npm run desktop:release-policy:check.
desktop:release:check statically verifies the Tauri bundle, updater public
key, CSP, capability scopes, command allowlist, query-token ban, generic system
notification boundary, CI gate coverage, and secure session token boundary. The
full manual release, security, regression, and Desktop UX checklist lives in
docs/audits/desktop-release-stabilization-checklist.md.
Promotion
- Merge feature branches into
dev. - Require the shared client/Web and macOS/Windows Desktop CI jobs to pass.
- Promote the accepted scope from
devtomain. - Set the release version and complete regression testing on
main. - Promote
maintorelease. - Create the matching
vX.Y.Ztag on the acceptedreleasecommit. - Build both Web and Desktop artifacts from that exact tag.
Build metadata is injected by CI:
VITE_BUILD_CHANNEL=release
VITE_BUILD_COMMIT="$(git rev-parse HEAD)"
These values support diagnostics but do not replace the semantic version.
Desktop Updater
Formal Desktop release builds must use Tauri updater signatures. The application
embeds the updater public key in frontend/src-tauri/tauri.conf.json; the
private key must live only in the organization key vault or CI secret store.
Runtime update checks derive the feed from the currently configured CTMS origin:
/desktop-updates/stable/latest.json
Production update feeds must use HTTPS. Local update testing may use HTTP only
for localhost, 127.0.0.1, or ::1.
The release pipeline must:
- build from the accepted release tag and commit;
- build the Universal macOS app/DMG and Windows x64 NSIS installer from that tag;
- use Apple signing/notarization and Windows Authenticode/RFC 3161 signing by default, or use a checked-in exact-version exception that constrains macOS to ad-hoc signing and Windows to Authenticode-unsigned output;
- produce macOS
.app.tar.gzand Windows.nsis.zipupdater artifacts plus their.sigfiles with the shared updater private key; - generate
DESKTOP-RELEASE-PROVENANCE.json, one combinedlatest.json, and a checksum manifest; - verify all macOS and Windows entries with
npm run desktop:update-feed:check -- --feed <release-dir>/latest.json --artifacts-dir <release-dir> --require-platform windows-x86_64 --require-provenance; - upload immutable artifacts first;
- atomically replace
latest.jsonlast.
For Universal macOS artifacts, latest.json must provide both
darwin-aarch64 and darwin-x86_64 entries pointing at the same Universal
update package.
The same feed must also contain windows-x86_64, pointing to the updater-signed
NSIS .nsis.zip package. The Windows .exe installer is distributed next to
the updater package but is not used as the updater URL. Tauri updater signing is
mandatory in both platform-signed and platform-signing-exception modes.
The formal release candidate workflow lives at
.github/workflows/desktop-release-candidate.yml. It must be run from a
matching vX.Y.Z tag and produces a verified release directory as a GitHub
artifact. That artifact is still only a release candidate; the release owner
must upload immutable files to the production download origin and replace
latest.json atomically after validation.
Platform signing defaults to signed. An exception is allowed only when
frontend/desktop-release-policy.json names the exact product version and
records release-owner approval. The current v0.1.0 exception uses macOS ad-hoc
signing and unsigned Windows application/installer binaries. Its artifact names
and verified updater release directory contain UNSIGNED-PLATFORM; the private
evidence/update directory must include UNSIGNED-PLATFORM-RELEASE.txt,
DESKTOP-RELEASE-PROVENANCE.json, and the full SHA256SUMS.txt. The
user-facing GitHub Release may use the exact-version
installer-and-checksum-only profile, with platform warnings in Release Notes
and a separate checksum manifest covering only the displayed installer. This
does not establish Apple or Microsoft publisher trust, and Gatekeeper or
SmartScreen warnings are expected. Later versions return to the signed default
unless separately approved.
v0.1.0 staged macOS contingency
The release owner approved one additional v0.1.0 contingency for unavailable
hosted Actions capacity. A controlled local macOS host may build the macOS
ad-hoc artifacts from the immutable final v0.1.0 tag and publish them first.
That initial user-facing GitHub Release contains only the DMG and a
SHA256SUMS.txt that covers the DMG, in addition to GitHub's automatic source
archives. The Release Notes must state that macOS is ad-hoc/not notarized and
that Windows remains pending. The macOS updater package and .sig, full
checksum manifest, provenance, unsigned-platform warning, and Windows-pending
notice must be copied to a permission-restricted, Git-ignored private release
directory before any visible asset is removed. Windows must later be built from
the same tag and SHA.
The staged macOS release is an installer distribution, not an activated
cross-platform updater release. Do not upload or replace production
latest.json until the Windows NotSigned installer/updater has been built and
the combined macOS/Windows feed passes full verification. The combined updater
artifacts, signatures, provenance, full checksum manifest, and latest.json
must use a separately configured HTTPS update origin that anonymous production
clients can read; a private GitHub Release is not a valid production updater
origin. This fallback is limited to v0.1.0 and does not authorize local formal
builds for later versions.
Windows Release and Internal Validation
Windows x64 NSIS is an approved formal Desktop target. Formal Windows builds
run in .github/workflows/desktop-release-candidate.yml from the same exact
vX.Y.Z tag and SHA as Web and macOS. The default signed path must:
- import a Base64-encoded PFX from
WINDOWS_CERTIFICATEusingWINDOWS_CERTIFICATE_PASSWORD; - configure the imported certificate thumbprint, SHA-256 digest,
WINDOWS_TIMESTAMP_URL, and RFC 3161 timestamp mode dynamically in CI; - set
REQUIRE_WINDOWS_SIGNING=trueand use the updater signing secrets; - verify the application and installer with
Get-AuthenticodeSignature, including signer and timestamp certificates; - publish the signed NSIS
.exe,.nsis.zip, and.nsis.zip.siginto the combined verified Desktop release directory.
For an approved exact-version unsigned-platform exception, the Windows job
must instead leave the application and installer Authenticode-unsigned, require
Get-AuthenticodeSignature to return NotSigned, append _UNSIGNED to the
installer and updater artifact names, and still create and verify the updater
.sig. A certificate secret and timestamp URL are not required in this mode.
Windows release validation also covers:
- WebView2 runtime prerequisite behavior;
- user-level installer assumptions;
- Windows Credential Manager session storage;
- path handling and temporary file cleanup;
- notification and updater compilation;
- code-signing trust, timestamp validity, and updater signature verification.
The manual internal Windows validation workflow lives at
.github/workflows/desktop-windows-internal.yml. It is workflow_dispatch
only, runs on windows-latest, injects VITE_BUILD_CHANNEL and
VITE_BUILD_COMMIT from the selected branch context, executes the shared client
and Desktop safety gates, builds an unsigned NSIS installer with updater
artifacts disabled, and uploads the .exe plus SHA256SUMS.txt as a GitHub
Actions artifact. This workflow is for internal compatibility verification
only; it must not generate latest.json, update feeds, verified release
directories, or formal Windows release artifacts. A successful internal build
does not substitute for the tag-only formal workflow, including when that
formal workflow uses an approved platform-signing exception.
The formal workflow always requires these organization settings:
- secrets:
TAURI_SIGNING_PRIVATE_KEYandTAURI_SIGNING_PRIVATE_KEY_PASSWORD; - client default CTMS origin: repository variable
VITE_DESKTOP_SERVER_URL; - shared versioned HTTPS artifact prefix:
DESKTOP_UPDATE_BASE_URLor the manual workflow input.
VITE_DESKTOP_SERVER_URL must be an HTTPS origin without credentials, path,
query, or fragment. Vite embeds it as the Desktop client's first-run default;
the runtime source must not contain the production hostname. A user can still
override this default in Desktop server settings, where the persisted manual
value takes precedence and switching origins clears the existing session and
server-scoped client state. Do not reuse this value as
DESKTOP_UPDATE_BASE_URL: the latter points to immutable updater artifacts,
not the CTMS business API.
The default signed path additionally requires WINDOWS_CERTIFICATE,
WINDOWS_CERTIFICATE_PASSWORD, and WINDOWS_TIMESTAMP_URL, plus the Apple
credentials documented by the release owner. The v0.1.0 exception does not
require those platform certificate settings.
The checked-in workflow implements the exportable PFX path. Confirm that the
organization's certificate policy permits an exportable CI certificate before
provisioning it. If the selected CA provides only hardware- or cloud-backed
keys, replace the PFX import with a reviewed Tauri signCommand integration
(for example, the organization's Azure signing provider) while retaining the
same Authenticode and timestamp verification gates.
Required Checks
cd frontend
npm ci
npm run version:check
export VITE_BUILD_CHANNEL=release
export VITE_BUILD_COMMIT="$(git rev-parse HEAD)"
export VITE_DESKTOP_SERVER_URL="https://ctms.example.com"
npm run release:env:check
npm run runtime:check
npm run desktop:release:check
npm run ui:contract
npm run type-check
npm run test:unit
npm run build
npm run desktop:build:app
export TAURI_SIGNING_PRIVATE_KEY="$UPDATER_PRIVATE_KEY"
export TAURI_SIGNING_PRIVATE_KEY_PASSWORD="$UPDATER_PRIVATE_KEY_PASSWORD"
export REQUIRE_UPDATER_SIGNING=true
export DESKTOP_RELEASE_PLATFORM=macos
export DESKTOP_PLATFORM_SIGNING_MODE=unsigned-exception
export ALLOW_UNSIGNED_PLATFORM_RELEASE=true
npm run release:env:check
npm run desktop:release-readiness:check
npm run desktop:build:macos-unsigned-release -- --ci
npm run desktop:update-feed:create -- --artifact <CTMS.app.tar.gz> --platform-artifact windows-x86_64=<CTMS.nsis.zip> --include <CTMS.dmg> --include <CTMS-installer.exe> --base-url <versioned-https-artifact-prefix> --output-dir <release-dir>
npm run desktop:update-feed:check -- --feed <release-dir>/latest.json --artifacts-dir <release-dir> --require-platform windows-x86_64 --require-provenance
The macOS and Windows builds run in their native CI jobs and always use the same
updater signing key. In the default path, macOS requires Apple
signing/notarization credentials and Windows requires the PFX/password and
timestamp URL. In the approved v0.1.0 exception, macOS must verify
Signature=adhoc, Windows must verify NotSigned, and both must carry explicit
platform-trust warnings. Native artifacts are aggregated only after both jobs
and the combined updater feed pass.