Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 82 additions & 29 deletions .github/workflows/desktop-release.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Signed desktop release
name: Desktop release

on:
push:
Expand All @@ -18,31 +18,14 @@ concurrency:

jobs:
validate:
name: Validate release metadata and credentials
name: Validate release metadata
runs-on: ubuntu-latest
env:
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with: { node-version: 22 }
- name: Match tag to every project version
run: node scripts/check-release-version.mjs "$RELEASE_TAG"
- name: Require signing and notarization credentials
shell: bash
run: |
set -euo pipefail
required=(WINDOWS_CERTIFICATE WINDOWS_CERTIFICATE_PASSWORD APPLE_CERTIFICATE APPLE_CERTIFICATE_PASSWORD APPLE_SIGNING_IDENTITY APPLE_ID APPLE_PASSWORD APPLE_TEAM_ID)
for name in "${required[@]}"; do
if [ -z "${!name:-}" ]; then echo "Missing required release secret: $name"; exit 1; fi
done

quality:
name: Release quality gate
Expand All @@ -63,7 +46,7 @@ jobs:
- run: pnpm --filter @jauto/desktop build:frontend

build:
name: Build and sign ${{ matrix.label }}
name: Build ${{ matrix.label }}
needs: [validate, quality]
runs-on: ${{ matrix.os }}
strategy:
Expand Down Expand Up @@ -100,22 +83,89 @@ jobs:
- name: Build workspace packages
run: pnpm --filter @jauto/core --filter @jauto/jff --filter @jauto/simulator --filter @jauto/file-io --filter @jauto/ui build

- name: Build native desktop app
- name: Configure optional Windows signing
if: runner.os == 'Windows'
shell: pwsh
run: |
$values = @($env:WINDOWS_CERTIFICATE, $env:WINDOWS_CERTIFICATE_PASSWORD)
$configured = @($values | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }).Count
if ($configured -eq 0) {
'WINDOWS_SIGNING_MODE=unsigned' | Out-File -FilePath $env:GITHUB_ENV -Append
Write-Output 'Windows signing credentials are unavailable; building unsigned installers.'
} elseif ($configured -ne $values.Count) {
throw 'Windows signing is only partially configured. Set both WINDOWS_CERTIFICATE and WINDOWS_CERTIFICATE_PASSWORD, or leave both unset.'
} else {
$certificatePath = Join-Path $env:RUNNER_TEMP 'jauto-release.pfx'
[IO.File]::WriteAllBytes($certificatePath, [Convert]::FromBase64String($env:WINDOWS_CERTIFICATE))
$password = ConvertTo-SecureString $env:WINDOWS_CERTIFICATE_PASSWORD -AsPlainText -Force
$certificate = Import-PfxCertificate -FilePath $certificatePath -CertStoreLocation Cert:\CurrentUser\My -Password $password
$config = @{ bundle = @{ windows = @{ certificateThumbprint = $certificate.Thumbprint; digestAlgorithm = 'sha256'; timestampUrl = 'http://timestamp.digicert.com' } } }
$config | ConvertTo-Json -Depth 5 | Set-Content apps/desktop/src-tauri/tauri.release.conf.json
'WINDOWS_SIGNING_MODE=signed' | Out-File -FilePath $env:GITHUB_ENV -Append
}

- name: Configure optional Apple signing
if: runner.os == 'macOS'
shell: bash
run: |
set -euo pipefail
values=("$APPLE_CERTIFICATE" "$APPLE_CERTIFICATE_PASSWORD" "$APPLE_SIGNING_IDENTITY" "$APPLE_ID" "$APPLE_PASSWORD" "$APPLE_TEAM_ID")
configured=0
for value in "${values[@]}"; do
[[ -n "$value" ]] && configured=$((configured + 1))
done
if [[ "$configured" -eq 0 ]]; then
echo "APPLE_SIGNING_MODE=adhoc" >> "$GITHUB_ENV"
echo "Apple signing credentials are unavailable; building an ad-hoc signed installer."
elif [[ "$configured" -ne "${#values[@]}" ]]; then
echo "Apple signing is only partially configured. Set all Apple release secrets, or leave all of them unset." >&2
exit 1
else
echo "APPLE_SIGNING_MODE=developer-id" >> "$GITHUB_ENV"
fi

- name: Import Apple Developer ID certificate
if: runner.os == 'macOS' && env.APPLE_SIGNING_MODE == 'developer-id'
shell: bash
run: |
set -euo pipefail
certificate_path="$RUNNER_TEMP/jauto-release.p12"
keychain_path="$RUNNER_TEMP/jauto-signing.keychain-db"
echo "$APPLE_CERTIFICATE" | base64 --decode > "$certificate_path"
security create-keychain -p "$APPLE_CERTIFICATE_PASSWORD" "$keychain_path"
security set-keychain-settings -lut 21600 "$keychain_path"
security unlock-keychain -p "$APPLE_CERTIFICATE_PASSWORD" "$keychain_path"
security import "$certificate_path" -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$keychain_path"
security list-keychain -d user -s "$keychain_path"
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$APPLE_CERTIFICATE_PASSWORD" "$keychain_path"

- name: Build Windows installers
if: runner.os == 'Windows'
shell: pwsh
working-directory: apps/desktop
run: pnpm tauri build --config src-tauri/tauri.release.conf.json
- name: Build signed and notarized universal macOS installer
run: |
if ($env:WINDOWS_SIGNING_MODE -eq 'signed') {
pnpm tauri build --config src-tauri/tauri.release.conf.json
} else {
pnpm tauri build
}

- name: Build universal macOS installer
if: runner.os == 'macOS'
working-directory: apps/desktop
run: pnpm tauri build --target universal-apple-darwin
- name: Verify Windows signatures and installers

- name: Verify Windows installers and configured signatures
if: runner.os == 'Windows'
shell: pwsh
run: |
$files = Get-ChildItem apps/desktop/src-tauri/target/release/bundle -Recurse -Include *.exe,*.msi
if (-not $files) { throw 'No Windows installers were produced.' }
foreach ($file in $files) {
$signature = Get-AuthenticodeSignature $file.FullName
if ($signature.Status -ne 'Valid') { throw "Invalid signature on $($file.Name): $($signature.Status)" }
if ($env:WINDOWS_SIGNING_MODE -eq 'signed') {
foreach ($file in $files) {
$signature = Get-AuthenticodeSignature $file.FullName
if ($signature.Status -ne 'Valid') { throw "Invalid signature on $($file.Name): $($signature.Status)" }
}
}
- name: Smoke-test installed Windows application
if: runner.os == 'Windows'
Expand All @@ -131,7 +181,7 @@ jobs:
Start-Sleep -Seconds 5
if ($app.HasExited -and $app.ExitCode -ne 0) { throw "Installed app exited with $($app.ExitCode)" }
if (-not $app.HasExited) { Stop-Process -Id $app.Id }
- name: Verify macOS signature and notarization
- name: Verify macOS signature and optional notarization
if: runner.os == 'macOS'
shell: bash
run: |
Expand All @@ -140,7 +190,10 @@ jobs:
dmg=$(find apps/desktop/src-tauri/target/universal-apple-darwin/release/bundle -name '*.dmg' -type f | head -1)
test -n "$app" && test -n "$dmg"
codesign --verify --deep --strict --verbose=2 "$app"
xcrun stapler validate "$dmg"
if [[ "$APPLE_SIGNING_MODE" == "developer-id" ]]; then
spctl --assess --type execute --verbose=2 "$app"
xcrun stapler validate "$dmg"
fi
- name: Smoke-test installed macOS application
if: runner.os == 'macOS'
shell: bash
Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,10 @@ notes and replaces its assets.
See [the release runbook](docs/RELEASING.md) for required signing secrets,
quality gates, checksums, and signature verification.

Stable desktop releases require Windows signing and Apple Developer ID signing/notarization
credentials in GitHub Actions. The release workflow refuses to publish stable installers when
those credentials are unavailable.
Windows Authenticode signing and Apple Developer ID signing/notarization are
applied when their complete credential sets are configured in GitHub Actions.
Otherwise, the workflow publishes unsigned Windows installers and an ad-hoc
signed macOS installer.

## Project Structure

Expand Down
12 changes: 6 additions & 6 deletions docs/RELEASING.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Desktop release process

Jauto publishes signed Windows x64 and notarized universal macOS installers from `.github/workflows/desktop-release.yml`. A release is created only after dependency audit, lint, tests, type checks, web and desktop frontend builds, native builds, signature verification, and notarization validation succeed.
Jauto publishes Windows x64 and universal macOS installers from `.github/workflows/desktop-release.yml`. A release is created only after dependency audit, lint, tests, type checks, web and desktop frontend builds, native builds, installer smoke tests, and all applicable signature checks succeed.

## Required repository secrets
## Optional repository secrets

- `WINDOWS_CERTIFICATE`: base64-encoded PFX Authenticode certificate.
- `WINDOWS_CERTIFICATE_PASSWORD`: PFX password.
Expand All @@ -13,14 +13,14 @@ Jauto publishes signed Windows x64 and notarized universal macOS installers from
- `APPLE_PASSWORD`: app-specific Apple ID password.
- `APPLE_TEAM_ID`: Apple Developer team identifier.

The workflow rejects publication when any credential is missing. Secrets are read only by the release workflow and are never available to pull-request jobs.
If all Windows credentials are configured, the Windows installers are Authenticode-signed. If all Apple credentials are configured, the macOS application is Developer ID-signed and notarized. Without credentials, the workflow publishes unsigned Windows installers and an ad-hoc signed macOS installer. A partially configured credential set is rejected to avoid silently producing an incorrectly signed release. Secrets are read only by the release workflow and are never available to pull-request jobs.

## Publish a release

1. Update every version together. `pnpm check:release-version` verifies the root, workspace packages, Tauri configuration, and Cargo package.
2. Update `CHANGELOG.md` and `.github/release-notes/desktop.md`.
3. Run `pnpm install --frozen-lockfile`, `pnpm audit --prod --audit-level moderate`, `pnpm lint`, `pnpm test`, and `pnpm typecheck`.
4. Push a matching tag such as `v0.2.0`, or run **Signed desktop release** manually with that tag.
5. Confirm the release contains signed `.msi` and `.exe` installers, a notarized `.dmg`, and `SHA256SUMS.txt`.
4. Merge the release commit into `master`, or run **Desktop release** manually.
5. Confirm the release contains `.msi` and `.exe` installers, a `.dmg`, and `SHA256SUMS.txt`.

Tags containing a hyphen, such as `v0.3.0-beta.1`, are marked as prereleases. Stable and prerelease installers use the same signing requirements.
The release tag is configured in the workflow. Signing and notarization are applied automatically when the complete platform credential set is available.