# 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`, or `release` - client type: `web` or `desktop` ## 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: ```bash cd frontend npm run version:set -- 1.8.0 npm run version:check ``` This synchronizes: - `frontend/package.json` - `frontend/package-lock.json` - `frontend/src-tauri/tauri.conf.json` - `frontend/src-tauri/Cargo.toml` - `frontend/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: ```bash 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 1. Merge feature branches into `dev`. 2. Require the shared client/Web and macOS/Windows Desktop CI jobs to pass. 3. Promote the accepted scope from `dev` to `main`. 4. Set the release version and complete regression testing on `main`. 5. Promote `main` to `release`. 6. Create the matching `vX.Y.Z` tag on the accepted `release` commit. 7. Build both Web and Desktop artifacts from that exact tag. Build metadata is injected by CI: ```bash 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: ```text /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: 1. build from the accepted release tag and commit; 2. build the Universal macOS app/DMG and Windows x64 NSIS installer from that tag; 3. 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; 4. produce macOS `.app.tar.gz` and Windows `.nsis.zip` updater artifacts plus their `.sig` files with the shared updater private key; 5. generate `DESKTOP-RELEASE-PROVENANCE.json`, one combined `latest.json`, and a checksum manifest; 6. verify all macOS and Windows entries with `npm run desktop:update-feed:check -- --feed /latest.json --artifacts-dir --require-platform windows-x86_64 --require-provenance`; 7. upload immutable artifacts first; 8. atomically replace `latest.json` last. 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_CERTIFICATE` using `WINDOWS_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=true` and 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.sig` into 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_KEY` and `TAURI_SIGNING_PRIVATE_KEY_PASSWORD`; - client default CTMS origin: repository variable `VITE_DESKTOP_SERVER_URL`; - shared versioned HTTPS artifact prefix: `DESKTOP_UPDATE_BASE_URL` or 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 ```bash 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 --platform-artifact windows-x86_64= --include --include --base-url --output-dir npm run desktop:update-feed:check -- --feed /latest.json --artifacts-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.