Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 403776cc1c |
@@ -19,35 +19,8 @@ concurrency:
|
|||||||
cancel-in-progress: false
|
cancel-in-progress: false
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release-policy:
|
|
||||||
name: Resolve desktop release signing policy
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
outputs:
|
|
||||||
platform_signing_mode: ${{ steps.policy.outputs.platform_signing_mode }}
|
|
||||||
macos_signing: ${{ steps.policy.outputs.macos_signing }}
|
|
||||||
windows_signing: ${{ steps.policy.outputs.windows_signing }}
|
|
||||||
artifact_label: ${{ steps.policy.outputs.artifact_label }}
|
|
||||||
warning_required: ${{ steps.policy.outputs.warning_required }}
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: frontend
|
|
||||||
steps:
|
|
||||||
- name: Checkout release source
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Node
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: "22.13"
|
|
||||||
|
|
||||||
- name: Resolve version-scoped signing policy
|
|
||||||
id: policy
|
|
||||||
shell: bash
|
|
||||||
run: npm run desktop:release-policy:check -- --require-tag --github-output "${GITHUB_OUTPUT}"
|
|
||||||
|
|
||||||
macos-release-candidate:
|
macos-release-candidate:
|
||||||
name: macOS release candidate
|
name: Signed macOS release candidate
|
||||||
needs: release-policy
|
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
@@ -56,11 +29,7 @@ jobs:
|
|||||||
VITE_BUILD_CHANNEL: release
|
VITE_BUILD_CHANNEL: release
|
||||||
VITE_BUILD_COMMIT: ${{ github.sha }}
|
VITE_BUILD_COMMIT: ${{ github.sha }}
|
||||||
RELEASE_BUILD: "true"
|
RELEASE_BUILD: "true"
|
||||||
DESKTOP_RELEASE_PLATFORM: macos
|
REQUIRE_DESKTOP_SIGNING: "true"
|
||||||
DESKTOP_PLATFORM_SIGNING_MODE: ${{ needs.release-policy.outputs.platform_signing_mode }}
|
|
||||||
REQUIRE_UPDATER_SIGNING: "true"
|
|
||||||
REQUIRE_DESKTOP_SIGNING: ${{ needs.release-policy.outputs.platform_signing_mode == 'signed' && 'true' || 'false' }}
|
|
||||||
ALLOW_UNSIGNED_PLATFORM_RELEASE: ${{ needs.release-policy.outputs.platform_signing_mode == 'unsigned-exception' && 'true' || 'false' }}
|
|
||||||
DESKTOP_UPDATE_BASE_URL: ${{ github.event_name == 'workflow_dispatch' && inputs.artifact_base_url || vars.DESKTOP_UPDATE_BASE_URL }}
|
DESKTOP_UPDATE_BASE_URL: ${{ github.event_name == 'workflow_dispatch' && inputs.artifact_base_url || vars.DESKTOP_UPDATE_BASE_URL }}
|
||||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||||
@@ -85,7 +54,7 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
if [[ "${GITHUB_REF_TYPE}" != "tag" ]]; then
|
if [[ "${GITHUB_REF_TYPE}" != "tag" ]]; then
|
||||||
echo "Desktop release candidates must run from a vX.Y.Z tag."
|
echo "Signed desktop release candidates must run from a vX.Y.Z tag."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
expected_tag="v$(node -p "require('./package.json').version")"
|
expected_tag="v$(node -p "require('./package.json').version")"
|
||||||
@@ -108,7 +77,7 @@ jobs:
|
|||||||
- name: Check release build metadata and signing environment
|
- name: Check release build metadata and signing environment
|
||||||
run: npm run release:env:check
|
run: npm run release:env:check
|
||||||
|
|
||||||
- name: Check desktop release readiness
|
- name: Check signed desktop release readiness
|
||||||
run: npm run desktop:release-readiness:check
|
run: npm run desktop:release-readiness:check
|
||||||
|
|
||||||
- name: Check synchronized client version
|
- name: Check synchronized client version
|
||||||
@@ -132,14 +101,9 @@ jobs:
|
|||||||
- name: Build Web artifact
|
- name: Build Web artifact
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
|
||||||
- name: Build Apple-signed and notarized Universal macOS artifacts
|
- name: Build signed and notarized Universal macOS artifacts
|
||||||
if: needs.release-policy.outputs.platform_signing_mode == 'signed'
|
|
||||||
run: npm run desktop:build:macos-release -- --ci
|
run: npm run desktop:build:macos-release -- --ci
|
||||||
|
|
||||||
- name: Build ad-hoc Universal macOS artifacts
|
|
||||||
if: needs.release-policy.outputs.platform_signing_mode == 'unsigned-exception'
|
|
||||||
run: npm run desktop:build:macos-unsigned-release -- --ci
|
|
||||||
|
|
||||||
- name: Verify and stage macOS artifacts
|
- name: Verify and stage macOS artifacts
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
@@ -147,46 +111,28 @@ jobs:
|
|||||||
artifact="$(find src-tauri/target -path '*/release/bundle/macos/*.app.tar.gz' -print -quit)"
|
artifact="$(find src-tauri/target -path '*/release/bundle/macos/*.app.tar.gz' -print -quit)"
|
||||||
dmg="$(find src-tauri/target -path '*/release/bundle/dmg/*.dmg' -print -quit)"
|
dmg="$(find src-tauri/target -path '*/release/bundle/dmg/*.dmg' -print -quit)"
|
||||||
if [[ -z "${app}" || -z "${artifact}" || -z "${dmg}" || ! -s "${artifact}.sig" ]]; then
|
if [[ -z "${app}" || -z "${artifact}" || -z "${dmg}" || ! -s "${artifact}.sig" ]]; then
|
||||||
echo "macOS app, updater artifact/signature, and DMG are all required."
|
echo "Signed macOS app, updater artifact/signature, and DMG are all required."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
codesign --verify --deep --strict --verbose=2 "${app}"
|
codesign --verify --deep --strict --verbose=2 "${app}"
|
||||||
if [[ "${DESKTOP_PLATFORM_SIGNING_MODE}" == "signed" ]]; then
|
|
||||||
spctl --assess --type execute --verbose=2 "${app}"
|
spctl --assess --type execute --verbose=2 "${app}"
|
||||||
xcrun stapler validate "${app}"
|
xcrun stapler validate "${app}"
|
||||||
xcrun stapler validate "${dmg}"
|
xcrun stapler validate "${dmg}"
|
||||||
else
|
|
||||||
signature_info="$(codesign -dv --verbose=4 "${app}" 2>&1)"
|
|
||||||
if ! grep -q "Signature=adhoc" <<<"${signature_info}"; then
|
|
||||||
echo "The macOS unsigned-platform exception must produce an ad-hoc signed app."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
stage="src-tauri/target/desktop-release-macos"
|
stage="src-tauri/target/desktop-release-macos"
|
||||||
mkdir -p "${stage}"
|
mkdir -p "${stage}"
|
||||||
if [[ "${DESKTOP_PLATFORM_SIGNING_MODE}" == "unsigned-exception" ]]; then
|
|
||||||
artifact_name="$(basename "${artifact}" .app.tar.gz)_UNSIGNED.app.tar.gz"
|
|
||||||
dmg_name="$(basename "${dmg}" .dmg)_UNSIGNED.dmg"
|
|
||||||
cp "${artifact}" "${stage}/${artifact_name}"
|
|
||||||
cp "${artifact}.sig" "${stage}/${artifact_name}.sig"
|
|
||||||
cp "${dmg}" "${stage}/${dmg_name}"
|
|
||||||
cp desktop-release-unsigned-warning.txt "${stage}/UNSIGNED-PLATFORM-RELEASE.txt"
|
|
||||||
else
|
|
||||||
cp "${artifact}" "${artifact}.sig" "${dmg}" "${stage}/"
|
cp "${artifact}" "${artifact}.sig" "${dmg}" "${stage}/"
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Upload macOS candidate artifacts
|
- name: Upload signed macOS candidate artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ctms-desktop-macos-${{ github.ref_name }}-${{ needs.release-policy.outputs.artifact_label }}
|
name: ctms-desktop-macos-${{ github.ref_name }}
|
||||||
path: frontend/src-tauri/target/desktop-release-macos/*
|
path: frontend/src-tauri/target/desktop-release-macos/*
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
windows-release-candidate:
|
windows-release-candidate:
|
||||||
name: Windows release candidate
|
name: Signed Windows release candidate
|
||||||
needs: release-policy
|
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
@@ -195,11 +141,7 @@ jobs:
|
|||||||
VITE_BUILD_CHANNEL: release
|
VITE_BUILD_CHANNEL: release
|
||||||
VITE_BUILD_COMMIT: ${{ github.sha }}
|
VITE_BUILD_COMMIT: ${{ github.sha }}
|
||||||
RELEASE_BUILD: "true"
|
RELEASE_BUILD: "true"
|
||||||
DESKTOP_RELEASE_PLATFORM: windows
|
REQUIRE_WINDOWS_SIGNING: "true"
|
||||||
DESKTOP_PLATFORM_SIGNING_MODE: ${{ needs.release-policy.outputs.platform_signing_mode }}
|
|
||||||
REQUIRE_UPDATER_SIGNING: "true"
|
|
||||||
REQUIRE_WINDOWS_SIGNING: ${{ needs.release-policy.outputs.platform_signing_mode == 'signed' && 'true' || 'false' }}
|
|
||||||
ALLOW_UNSIGNED_PLATFORM_RELEASE: ${{ needs.release-policy.outputs.platform_signing_mode == 'unsigned-exception' && 'true' || 'false' }}
|
|
||||||
DESKTOP_UPDATE_BASE_URL: ${{ github.event_name == 'workflow_dispatch' && inputs.artifact_base_url || vars.DESKTOP_UPDATE_BASE_URL }}
|
DESKTOP_UPDATE_BASE_URL: ${{ github.event_name == 'workflow_dispatch' && inputs.artifact_base_url || vars.DESKTOP_UPDATE_BASE_URL }}
|
||||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||||
@@ -221,7 +163,7 @@ jobs:
|
|||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: |
|
run: |
|
||||||
if ($env:GITHUB_REF_TYPE -ne "tag") {
|
if ($env:GITHUB_REF_TYPE -ne "tag") {
|
||||||
throw "Desktop release candidates must run from a vX.Y.Z tag."
|
throw "Signed desktop release candidates must run from a vX.Y.Z tag."
|
||||||
}
|
}
|
||||||
$expectedTag = "v$(node -p "require('./package.json').version")"
|
$expectedTag = "v$(node -p "require('./package.json').version")"
|
||||||
if ($env:GITHUB_REF_NAME -ne $expectedTag) {
|
if ($env:GITHUB_REF_NAME -ne $expectedTag) {
|
||||||
@@ -240,7 +182,7 @@ jobs:
|
|||||||
- name: Check release build metadata and signing environment
|
- name: Check release build metadata and signing environment
|
||||||
run: npm run release:env:check
|
run: npm run release:env:check
|
||||||
|
|
||||||
- name: Check desktop release readiness
|
- name: Check signed desktop release readiness
|
||||||
run: npm run desktop:release-readiness:check
|
run: npm run desktop:release-readiness:check
|
||||||
|
|
||||||
- name: Check synchronized client version
|
- name: Check synchronized client version
|
||||||
@@ -265,7 +207,6 @@ jobs:
|
|||||||
run: npm run build
|
run: npm run build
|
||||||
|
|
||||||
- name: Import Windows code-signing certificate
|
- name: Import Windows code-signing certificate
|
||||||
if: needs.release-policy.outputs.platform_signing_mode == 'signed'
|
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: |
|
run: |
|
||||||
$pfxPath = Join-Path $env:RUNNER_TEMP "ctms-windows-signing.pfx"
|
$pfxPath = Join-Path $env:RUNNER_TEMP "ctms-windows-signing.pfx"
|
||||||
@@ -292,7 +233,6 @@ jobs:
|
|||||||
Remove-Item $pfxPath -Force
|
Remove-Item $pfxPath -Force
|
||||||
|
|
||||||
- name: Write signed Windows Tauri config
|
- name: Write signed Windows Tauri config
|
||||||
if: needs.release-policy.outputs.platform_signing_mode == 'signed'
|
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: |
|
run: |
|
||||||
$config = @{
|
$config = @{
|
||||||
@@ -309,16 +249,10 @@ jobs:
|
|||||||
Get-Content "tauri.windows.release.conf.json"
|
Get-Content "tauri.windows.release.conf.json"
|
||||||
|
|
||||||
- name: Build signed Windows NSIS artifacts
|
- name: Build signed Windows NSIS artifacts
|
||||||
if: needs.release-policy.outputs.platform_signing_mode == 'signed'
|
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
run: npm run desktop:build:windows-release -- --config tauri.windows.release.conf.json --ci
|
run: npm run desktop:build:windows-release -- --config tauri.windows.release.conf.json --ci
|
||||||
|
|
||||||
- name: Build unsigned Windows NSIS artifacts
|
- name: Verify Authenticode and stage Windows artifacts
|
||||||
if: needs.release-policy.outputs.platform_signing_mode == 'unsigned-exception'
|
|
||||||
timeout-minutes: 30
|
|
||||||
run: npm run desktop:build:windows-release -- --ci
|
|
||||||
|
|
||||||
- name: Verify platform signing mode and stage Windows artifacts
|
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: |
|
run: |
|
||||||
$bundleDir = "src-tauri/target/release/bundle/nsis"
|
$bundleDir = "src-tauri/target/release/bundle/nsis"
|
||||||
@@ -336,27 +270,14 @@ jobs:
|
|||||||
|
|
||||||
foreach ($executable in @($appExecutables + $installers)) {
|
foreach ($executable in @($appExecutables + $installers)) {
|
||||||
$signature = Get-AuthenticodeSignature -FilePath $executable.FullName
|
$signature = Get-AuthenticodeSignature -FilePath $executable.FullName
|
||||||
if ($env:DESKTOP_PLATFORM_SIGNING_MODE -eq "signed") {
|
|
||||||
if ($signature.Status -ne "Valid" -or -not $signature.SignerCertificate -or -not $signature.TimeStamperCertificate) {
|
if ($signature.Status -ne "Valid" -or -not $signature.SignerCertificate -or -not $signature.TimeStamperCertificate) {
|
||||||
throw "Authenticode signature or RFC 3161 timestamp is invalid for $($executable.FullName): $($signature.StatusMessage)"
|
throw "Authenticode signature or RFC 3161 timestamp is invalid for $($executable.FullName): $($signature.StatusMessage)"
|
||||||
}
|
}
|
||||||
} elseif ($signature.Status -ne "NotSigned") {
|
|
||||||
throw "The Windows unsigned-platform exception must produce an Authenticode-unsigned executable: $($executable.FullName) returned $($signature.Status)."
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$stage = "src-tauri/target/desktop-release-windows"
|
$stage = "src-tauri/target/desktop-release-windows"
|
||||||
New-Item -ItemType Directory -Path $stage -Force | Out-Null
|
New-Item -ItemType Directory -Path $stage -Force | Out-Null
|
||||||
if ($env:DESKTOP_PLATFORM_SIGNING_MODE -eq "unsigned-exception") {
|
|
||||||
$installerName = $installers[0].Name -replace '\.exe$', '_UNSIGNED.exe'
|
|
||||||
$updaterName = $updaters[0].Name -replace '\.nsis\.zip$', '_UNSIGNED.nsis.zip'
|
|
||||||
Copy-Item $installers[0].FullName -Destination (Join-Path $stage $installerName)
|
|
||||||
Copy-Item $updaters[0].FullName -Destination (Join-Path $stage $updaterName)
|
|
||||||
Copy-Item $signaturePath -Destination (Join-Path $stage "$updaterName.sig")
|
|
||||||
Copy-Item "desktop-release-unsigned-warning.txt" -Destination (Join-Path $stage "UNSIGNED-PLATFORM-RELEASE.txt")
|
|
||||||
} else {
|
|
||||||
Copy-Item $installers[0].FullName, $updaters[0].FullName, $signaturePath -Destination $stage
|
Copy-Item $installers[0].FullName, $updaters[0].FullName, $signaturePath -Destination $stage
|
||||||
}
|
|
||||||
|
|
||||||
- name: Remove Windows signing certificate
|
- name: Remove Windows signing certificate
|
||||||
if: always()
|
if: always()
|
||||||
@@ -368,17 +289,16 @@ jobs:
|
|||||||
Remove-Item (Join-Path $env:RUNNER_TEMP "ctms-windows-signing.pfx") -Force -ErrorAction SilentlyContinue
|
Remove-Item (Join-Path $env:RUNNER_TEMP "ctms-windows-signing.pfx") -Force -ErrorAction SilentlyContinue
|
||||||
Remove-Item "tauri.windows.release.conf.json" -Force -ErrorAction SilentlyContinue
|
Remove-Item "tauri.windows.release.conf.json" -Force -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
- name: Upload Windows candidate artifacts
|
- name: Upload signed Windows candidate artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ctms-desktop-windows-${{ github.ref_name }}-${{ needs.release-policy.outputs.artifact_label }}
|
name: ctms-desktop-windows-${{ github.ref_name }}
|
||||||
path: frontend/src-tauri/target/desktop-release-windows/*
|
path: frontend/src-tauri/target/desktop-release-windows/*
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
aggregate-release-candidate:
|
aggregate-release-candidate:
|
||||||
name: Verify combined desktop release directory
|
name: Verify combined desktop release directory
|
||||||
needs:
|
needs:
|
||||||
- release-policy
|
|
||||||
- macos-release-candidate
|
- macos-release-candidate
|
||||||
- windows-release-candidate
|
- windows-release-candidate
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -396,21 +316,18 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: "22.13"
|
node-version: "22.13"
|
||||||
|
|
||||||
- name: Download macOS candidate artifacts
|
- name: Download signed macOS candidate artifacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ctms-desktop-macos-${{ github.ref_name }}-${{ needs.release-policy.outputs.artifact_label }}
|
name: ctms-desktop-macos-${{ github.ref_name }}
|
||||||
path: frontend/src-tauri/target/desktop-release-input/macos
|
path: frontend/src-tauri/target/desktop-release-input/macos
|
||||||
|
|
||||||
- name: Download Windows candidate artifacts
|
- name: Download signed Windows candidate artifacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ctms-desktop-windows-${{ github.ref_name }}-${{ needs.release-policy.outputs.artifact_label }}
|
name: ctms-desktop-windows-${{ github.ref_name }}
|
||||||
path: frontend/src-tauri/target/desktop-release-input/windows
|
path: frontend/src-tauri/target/desktop-release-input/windows
|
||||||
|
|
||||||
- name: Create desktop release provenance
|
|
||||||
run: npm run desktop:release-provenance:create -- --macos-signing "${{ needs.release-policy.outputs.macos_signing }}" --windows-signing "${{ needs.release-policy.outputs.windows_signing }}" --output src-tauri/target/desktop-release-input/DESKTOP-RELEASE-PROVENANCE.json
|
|
||||||
|
|
||||||
- name: Create combined desktop update feed
|
- name: Create combined desktop update feed
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
@@ -423,32 +340,20 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
include_args=(--include "${dmgs[0]}" --include "${installers[0]}" --include src-tauri/target/desktop-release-input/DESKTOP-RELEASE-PROVENANCE.json)
|
|
||||||
notes_args=()
|
|
||||||
if [[ "${{ needs.release-policy.outputs.warning_required }}" == "true" ]]; then
|
|
||||||
mapfile -t warnings < <(find src-tauri/target/desktop-release-input/macos -name 'UNSIGNED-PLATFORM-RELEASE.txt' -type f)
|
|
||||||
if [[ ${#warnings[@]} -ne 1 ]]; then
|
|
||||||
echo "The unsigned-platform release warning is required."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
include_args+=(--include "${warnings[0]}")
|
|
||||||
notes_args+=(--notes "UNSIGNED PLATFORM RELEASE: macOS is ad-hoc signed and not notarized; Windows is not Authenticode-signed. Gatekeeper and SmartScreen warnings are expected. Verify tag, commit, updater signatures, and SHA256SUMS.txt before installation.")
|
|
||||||
fi
|
|
||||||
|
|
||||||
npm run desktop:update-feed:create -- \
|
npm run desktop:update-feed:create -- \
|
||||||
--artifact "${mac_artifacts[0]}" \
|
--artifact "${mac_artifacts[0]}" \
|
||||||
--platform-artifact "windows-x86_64=${windows_artifacts[0]}" \
|
--platform-artifact "windows-x86_64=${windows_artifacts[0]}" \
|
||||||
"${include_args[@]}" \
|
--include "${dmgs[0]}" \
|
||||||
"${notes_args[@]}" \
|
--include "${installers[0]}" \
|
||||||
--output-dir src-tauri/target/desktop-release-feed \
|
--output-dir src-tauri/target/desktop-release-feed \
|
||||||
--base-url "${DESKTOP_UPDATE_BASE_URL}"
|
--base-url "${DESKTOP_UPDATE_BASE_URL}"
|
||||||
|
|
||||||
- name: Verify combined desktop update feed
|
- name: Verify combined desktop update feed
|
||||||
run: npm run desktop:update-feed:check -- --feed src-tauri/target/desktop-release-feed/latest.json --artifacts-dir src-tauri/target/desktop-release-feed --base-url "${DESKTOP_UPDATE_BASE_URL}" --require-platform windows-x86_64 --require-provenance
|
run: npm run desktop:update-feed:check -- --feed src-tauri/target/desktop-release-feed/latest.json --artifacts-dir src-tauri/target/desktop-release-feed --base-url "${DESKTOP_UPDATE_BASE_URL}" --require-platform windows-x86_64
|
||||||
|
|
||||||
- name: Upload verified desktop release directory
|
- name: Upload verified desktop release directory
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ctms-desktop-release-${{ github.ref_name }}-${{ needs.release-policy.outputs.artifact_label }}
|
name: ctms-desktop-release-${{ github.ref_name }}
|
||||||
path: frontend/src-tauri/target/desktop-release-feed/*
|
path: frontend/src-tauri/target/desktop-release-feed/*
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
- 未完成后端 token 校验和 `/me` 身份确认前,不得展示业务缓存;登出、切换服务器、切换用户、401/403、权限上下文变化或缓存 schema 变化时必须清理或失效相关缓存。
|
- 未完成后端 token 校验和 `/me` 身份确认前,不得展示业务缓存;登出、切换服务器、切换用户、401/403、权限上下文变化或缓存 schema 变化时必须清理或失效相关缓存。
|
||||||
- 新增或调整 Tauri command、capability、CSP、updater、凭据、文件、通知、本地缓存持久化或底层存储能力时,必须同步评估 `frontend/scripts/verify-desktop-release.mjs`、`npm run runtime:check` 和桌面发布检查清单是否需要更新。
|
- 新增或调整 Tauri command、capability、CSP、updater、凭据、文件、通知、本地缓存持久化或底层存储能力时,必须同步评估 `frontend/scripts/verify-desktop-release.mjs`、`npm run runtime:check` 和桌面发布检查清单是否需要更新。
|
||||||
- token、附件下载凭据和敏感业务信息不得写入 URL、日志、系统通知正文或明文浏览器存储。
|
- token、附件下载凭据和敏感业务信息不得写入 URL、日志、系统通知正文或明文浏览器存储。
|
||||||
- Windows x64 NSIS 已获准作为正式桌面发布目标;正式制品必须由 `.github/workflows/desktop-release-candidate.yml` 从与 macOS/Web 相同的 `vX.Y.Z` tag 和 SHA 构建。平台签名默认要求组织 Windows 代码签名证书、RFC 3161 时间戳和 Authenticode 校验;只有 `frontend/desktop-release-policy.json` 中按精确版本记录、经发布负责人批准的例外可跳过平台签名。无论是否采用平台签名例外,都必须使用 updater 私钥、校验 updater feed,并明确标记平台未签名风险。`.github/workflows/desktop-windows-internal.yml` 仍只用于分支上的无签名兼容性验证,不得生成生产 `latest.json`、updater feed 或正式发布制品。
|
- Windows x64 NSIS 已获准作为正式桌面发布目标;正式制品必须由 `.github/workflows/desktop-release-candidate.yml` 从与 macOS/Web 相同的 `vX.Y.Z` tag 和 SHA 构建,使用组织 Windows 代码签名证书、RFC 3161 时间戳和 updater 私钥,并通过 Authenticode 与 updater feed 校验。`.github/workflows/desktop-windows-internal.yml` 仍只用于分支上的无签名兼容性验证,不得生成生产 `latest.json`、updater feed 或正式发布制品。
|
||||||
|
|
||||||
## 分支与发布治理
|
## 分支与发布治理
|
||||||
|
|
||||||
@@ -57,4 +57,4 @@ npm run desktop:build:app
|
|||||||
|
|
||||||
本地缓存相关变更至少执行 `runtime:check`、`desktop:release:check`、`ui:contract`、`type-check`、`test:unit` 和 `build`;若新增 Tauri command、capability、CSP 或底层持久化存储,还需执行 `desktop:build:app` 并在真实桌面 App 中验证缓存清理和诊断入口。
|
本地缓存相关变更至少执行 `runtime:check`、`desktop:release:check`、`ui:contract`、`type-check`、`test:unit` 和 `build`;若新增 Tauri command、capability、CSP 或底层持久化存储,还需执行 `desktop:build:app` 并在真实桌面 App 中验证缓存清理和诊断入口。
|
||||||
|
|
||||||
正式桌面发布构建必须使用组织批准的 updater 签名私钥,updater 签名不可因平台签名例外而关闭。平台签名默认要求 macOS 完成 Apple 签名/公证、Windows 完成组织代码签名、RFC 3161 时间戳和 Authenticode 校验。当前仅批准 v0.1.0 采用受控分发例外:macOS 使用 ad-hoc 签名且不公证,Windows 应用和安装器保持 Authenticode 未签名;制品名、发布说明、校验清单和 provenance 必须清楚标注 `UNSIGNED-PLATFORM`,并提示 Gatekeeper/SmartScreen 警告。该例外不自动适用于后续版本。
|
正式桌面发布构建必须使用组织批准的 updater 签名私钥;macOS 必须完成 Apple 签名/公证,Windows 必须完成组织代码签名、RFC 3161 时间戳和 Authenticode 校验。未签名或 ad-hoc 构建只能作为内部验证构建描述。
|
||||||
|
|||||||
@@ -28,14 +28,14 @@ npm run desktop:build:app
|
|||||||
|
|
||||||
- [ ] `frontend/package.json`、`package-lock.json`、Tauri 配置、Cargo manifest/lock 版本一致。
|
- [ ] `frontend/package.json`、`package-lock.json`、Tauri 配置、Cargo manifest/lock 版本一致。
|
||||||
- [ ] `VITE_BUILD_CHANNEL=release` 和 `VITE_BUILD_COMMIT=<release tag commit>` 由 CI 注入,且 `npm run release:env:check` 通过。
|
- [ ] `VITE_BUILD_CHANNEL=release` 和 `VITE_BUILD_COMMIT=<release tag commit>` 由 CI 注入,且 `npm run release:env:check` 通过。
|
||||||
- [ ] 在正式 release tag 环境中执行 `npm run desktop:release-readiness:check`,确认 tag、构建元数据、当前精确版本的平台签名策略、updater 私钥和生产 artifact HTTPS 基址齐备。
|
- [ ] 在正式 release tag 和签名环境中执行 `npm run desktop:release-readiness:check`,确认 tag、构建元数据、签名/公证变量、updater 私钥和生产 artifact HTTPS 基址齐备。
|
||||||
- [ ] macOS app 已完成 Apple 签名/公证;若当前精确版本获批平台签名例外,则已验证 `Signature=adhoc`、确认未公证并附带 Gatekeeper 风险说明。
|
- [ ] macOS app 已签名和公证。
|
||||||
- [ ] Windows 应用和 NSIS 安装器已使用组织证书签名并带有效 RFC 3161 时间戳,`Get-AuthenticodeSignature` 返回 `Valid`;若当前精确版本获批例外,则应用和安装器必须返回 `NotSigned` 并附带 SmartScreen 风险说明。
|
- [ ] Windows 应用和 NSIS 安装器已使用组织证书签名并带有效 RFC 3161 时间戳,`Get-AuthenticodeSignature` 对应用和安装器均返回 `Valid`。
|
||||||
- [ ] updater `.sig` 使用组织 CI secret 或密钥库中的私钥生成,私钥未进入仓库。
|
- [ ] updater `.sig` 使用组织 CI secret 或密钥库中的私钥生成,私钥未进入仓库。
|
||||||
- [ ] 始终设置 `TAURI_SIGNING_PRIVATE_KEY`、`TAURI_SIGNING_PRIVATE_KEY_PASSWORD` 和 `REQUIRE_UPDATER_SIGNING=true`;默认 macOS 路径再设置 Apple 变量和 `REQUIRE_DESKTOP_SIGNING=true`,例外路径则设置 `DESKTOP_PLATFORM_SIGNING_MODE=unsigned-exception`、`ALLOW_UNSIGNED_PLATFORM_RELEASE=true` 并执行 `npm run desktop:build:macos-unsigned-release -- --ci`。
|
- [ ] 设置 `TAURI_SIGNING_PRIVATE_KEY`、`TAURI_SIGNING_PRIVATE_KEY_PASSWORD` 和 Apple 签名/公证变量后,以 `REQUIRE_DESKTOP_SIGNING=true` 再次执行 `npm run release:env:check`,随后执行 `npm run desktop:build:macos-release -- --ci`。
|
||||||
- [ ] 默认 Windows 路径设置 PFX、密码、`WINDOWS_TIMESTAMP_URL` 和 `REQUIRE_WINDOWS_SIGNING=true`;例外路径不得伪装签名状态,构建后必须对应用和安装器验证 `NotSigned`。
|
- [ ] 设置 Windows PFX、密码、`WINDOWS_TIMESTAMP_URL` 和 updater 私钥后,以 `REQUIRE_WINDOWS_SIGNING=true` 执行 readiness,再执行 `npm run desktop:build:windows-release -- --ci`。
|
||||||
- [ ] 正式 updater feed 使用 `--artifact <CTMS.app.tar.gz>` 和 `--platform-artifact windows-x86_64=<CTMS.nsis.zip>` 汇总生成同一个 `latest.json` 与 `SHA256SUMS.txt`。
|
- [ ] 正式 updater feed 使用 `--artifact <CTMS.app.tar.gz>` 和 `--platform-artifact windows-x86_64=<CTMS.nsis.zip>` 汇总生成同一个 `latest.json` 与 `SHA256SUMS.txt`。
|
||||||
- [ ] 正式 updater feed 执行 `npm run desktop:update-feed:check -- --feed <release-dir>/latest.json --artifacts-dir <release-dir> --require-platform windows-x86_64 --require-provenance`,并确认两个平台的 updater artifact、`.sig`、安装包、provenance、checksum manifest 和 `latest.json` 均通过校验。
|
- [ ] 正式 updater feed 执行 `npm run desktop:update-feed:check -- --feed <release-dir>/latest.json --artifacts-dir <release-dir> --require-platform windows-x86_64`,并确认两个平台的 updater artifact、`.sig`、安装包、checksum manifest 和 `latest.json` 均通过校验。
|
||||||
- [ ] 不可变制品先上传,`latest.json` 最后原子替换;若 feed 校验未通过,不替换线上 `latest.json`。
|
- [ ] 不可变制品先上传,`latest.json` 最后原子替换;若 feed 校验未通过,不替换线上 `latest.json`。
|
||||||
- [ ] Web 与 Desktop 制品记录同一产品版本、Git 标签和完整提交 SHA。
|
- [ ] Web 与 Desktop 制品记录同一产品版本、Git 标签和完整提交 SHA。
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ npm run desktop:build:app
|
|||||||
- [ ] updater capability 不直接暴露给 WebView,自动更新只走受控 Tauri command。
|
- [ ] updater capability 不直接暴露给 WebView,自动更新只走受控 Tauri command。
|
||||||
- [ ] 更新弹窗 release notes 过滤 URL、token 查询参数和 Authorization/Bearer 形态文本。
|
- [ ] 更新弹窗 release notes 过滤 URL、token 查询参数和 Authorization/Bearer 形态文本。
|
||||||
- [ ] CI release 候选 workflow 包含 version/runtime/desktop/ui/type/unit/build/desktop app smoke 门禁。
|
- [ ] CI release 候选 workflow 包含 version/runtime/desktop/ui/type/unit/build/desktop app smoke 门禁。
|
||||||
- [ ] Desktop release candidate workflow 只允许从 `vX.Y.Z` tag 运行;先按精确版本解析平台签名策略,macOS 和 Windows 原生 job 分别验证实际签名状态,再由聚合 job 生成包含 `darwin-aarch64`、`darwin-x86_64` 和 `windows-x86_64` 的 feed、provenance、checksum 清单和 verified release directory。
|
- [ ] signed Desktop release candidate workflow 只允许从 `vX.Y.Z` tag 运行;macOS 和 Windows 原生 job 分别完成签名验证,再由聚合 job 生成包含 `darwin-aarch64`、`darwin-x86_64` 和 `windows-x86_64` 的 feed、checksum 清单和 verified release directory。
|
||||||
|
|
||||||
人工复审还必须确认:
|
人工复审还必须确认:
|
||||||
|
|
||||||
@@ -276,19 +276,6 @@ npm run desktop:build:app
|
|||||||
|
|
||||||
创建正式 tag 前仍必须由发布负责人确认:
|
创建正式 tag 前仍必须由发布负责人确认:
|
||||||
|
|
||||||
- GitHub Actions 已配置 updater secrets 和 versioned artifact base URL;默认平台签名路径还必须配置 Apple 与 Windows secrets 和 Windows timestamp URL。
|
- GitHub Actions 已配置 updater、Apple 与 Windows 签名 secrets,以及 versioned artifact base URL 和 Windows timestamp URL。
|
||||||
- 若使用默认 Windows 签名路径,组织证书允许 PFX 导入 CI;若为硬件或云托管密钥,先将 workflow 切换为经审计的 Tauri `signCommand` provider。
|
- 组织 Windows 证书允许 PFX 导入 CI;若为硬件或云托管密钥,先将 workflow 切换为经审计的 Tauri `signCommand` provider。
|
||||||
- macOS/Windows 原生 candidate job、联合 feed 校验、真实安装/升级和生产下载源上传全部通过。
|
- macOS/Windows 原生 candidate job、联合 feed 校验、真实安装/升级和生产下载源上传全部通过。
|
||||||
|
|
||||||
## 13. 2026-07-17 v0.1.0 平台签名例外记录
|
|
||||||
|
|
||||||
经发布负责人明确批准,v0.1.0 可在无法提供 Apple Developer 和 Windows 组织代码签名凭据时受控分发。该批准仅覆盖精确版本 `0.1.0`:
|
|
||||||
|
|
||||||
- `frontend/desktop-release-policy.json` 默认仍为 `signed`,仅将 v0.1.0 列入 `unsignedPlatformExceptions`;后续版本不会继承本例外。
|
|
||||||
- macOS 使用 Tauri `signingIdentity: "-"` 生成 ad-hoc 签名 Universal app/DMG,必须通过 `codesign --verify` 并确认 `Signature=adhoc`,不执行或声称 Apple 公证。
|
|
||||||
- Windows 应用和 NSIS 安装器不做 Authenticode 签名,必须通过 `Get-AuthenticodeSignature` 确认 `NotSigned`,不执行或声称 RFC 3161 时间戳。
|
|
||||||
- 两端仍必须从同一 `v0.1.0` tag 和 SHA 构建,使用同一 updater 私钥生成 `.sig`,并通过联合 feed、checksum 和 provenance 校验。
|
|
||||||
- 制品与 Actions artifact 名称必须含 `_UNSIGNED` 或 `UNSIGNED-PLATFORM`,发布目录必须携带 `UNSIGNED-PLATFORM-RELEASE.txt` 和 `DESKTOP-RELEASE-PROVENANCE.json`。
|
|
||||||
- 受控分发说明必须明确提示 macOS Gatekeeper 与 Windows SmartScreen 警告;平台未签名制品不得描述为 Apple/Microsoft 信任或已公证/已 Authenticode 签名。
|
|
||||||
|
|
||||||
本地实物验证已完成:使用当前 updater 私钥成功构建 Universal `x86_64 arm64` macOS app、DMG、`.app.tar.gz` 和 `.sig`;`codesign --verify --deep --strict` 通过,签名详情为 `Signature=adhoc`、`TeamIdentifier=not set`,Gatekeeper 拒绝符合预期。Windows `NotSigned`、NSIS 和 updater `.sig` 仍须由 `windows-latest` 原生 job 在正式 tag 上验证。
|
|
||||||
|
|||||||
@@ -305,18 +305,9 @@ Before promoting `main` to `release`, confirm:
|
|||||||
|
|
||||||
- release checklist is complete
|
- release checklist is complete
|
||||||
- production initialization, build, and smoke verification are complete
|
- production initialization, build, and smoke verification are complete
|
||||||
- Desktop updater signatures are present and verified
|
|
||||||
- platform signing is either complete or the exact version has an approved,
|
|
||||||
checked-in exception in `frontend/desktop-release-policy.json`; the current
|
|
||||||
v0.1.0 exception requires macOS ad-hoc signing, unsigned Windows artifacts,
|
|
||||||
controlled distribution, and explicit trust warnings
|
|
||||||
- rollback or remediation path is clear
|
- rollback or remediation path is clear
|
||||||
- release tag has been prepared
|
- release tag has been prepared
|
||||||
|
|
||||||
Platform signing remains the default for versions without an exact approved
|
|
||||||
exception. An exception never permits disabling Tauri updater signatures or
|
|
||||||
building Web, macOS, and Windows from different tags or commits.
|
|
||||||
|
|
||||||
Reference:
|
Reference:
|
||||||
|
|
||||||
- [docs/guides/release-checklist.md](guides/release-checklist.md)
|
- [docs/guides/release-checklist.md](guides/release-checklist.md)
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ npm run desktop:build:app
|
|||||||
npm run desktop:build:app
|
npm run desktop:build:app
|
||||||
```
|
```
|
||||||
|
|
||||||
正式桌面发布构建必须从同一正式 tag 分别在 macOS 和 Windows 原生 CI job 执行。两端都必须设置 updater 签名私钥,updater 签名不可关闭。平台签名默认路径仍要求 macOS 设置 Apple 签名/公证变量并以 `REQUIRE_DESKTOP_SIGNING=true` 执行,Windows 设置 PFX、密码和 RFC 3161 时间戳变量并以 `REQUIRE_WINDOWS_SIGNING=true` 执行。只有 `frontend/desktop-release-policy.json` 对当前精确版本存在已批准例外时,workflow 才可改为 macOS ad-hoc、Windows Authenticode 未签名路径;例外制品必须标注 `UNSIGNED-PLATFORM`、生成 provenance 和风险说明。两个平台与联合 feed 校验通过后才能汇总正式 updater feed。
|
正式桌面发布构建必须从同一正式 tag 分别在 macOS 和 Windows 原生 CI job 执行。两端都必须设置 updater 签名私钥;macOS 设置 Apple 签名/公证变量并以 `REQUIRE_DESKTOP_SIGNING=true` 执行 readiness 与 Universal 构建,Windows 设置 PFX 证书、密码和 RFC 3161 时间戳变量并以 `REQUIRE_WINDOWS_SIGNING=true` 执行 readiness、签名 NSIS 构建和 Authenticode 校验。两个平台通过后才能汇总正式 updater feed。
|
||||||
|
|
||||||
后端改动应补充执行受影响模块的后端测试、迁移检查和接口回归。
|
后端改动应补充执行受影响模块的后端测试、迁移检查和接口回归。
|
||||||
|
|
||||||
@@ -206,7 +206,7 @@ dev -> main
|
|||||||
- 前后端测试通过
|
- 前后端测试通过
|
||||||
- 网页端构建通过
|
- 网页端构建通过
|
||||||
- macOS 桌面端构建通过
|
- macOS 桌面端构建通过
|
||||||
- Windows x64 NSIS 兼容性构建通过;正式发布时平台签名和时间戳验证通过,或当前精确版本的已批准平台签名例外验证通过
|
- Windows x64 NSIS 兼容性构建通过;正式发布时签名和时间戳验证通过
|
||||||
- 数据库迁移经过验证
|
- 数据库迁移经过验证
|
||||||
- 已知风险和回滚方式已记录
|
- 已知风险和回滚方式已记录
|
||||||
|
|
||||||
@@ -295,7 +295,7 @@ git tag -a v1.2.0 -m "CTMS v1.2.0"
|
|||||||
git push origin v1.2.0
|
git push origin v1.2.0
|
||||||
```
|
```
|
||||||
|
|
||||||
网页端、macOS 和 Windows 桌面端必须从同一个 `v1.2.0` 标签构建。不得从不同分支、不同提交或本地未提交状态构建正式制品。两个平台的 updater 签名制品必须始终验证通过;macOS 签名/公证和 Windows 代码签名/RFC 3161 时间戳必须验证通过,除非 `frontend/desktop-release-policy.json` 对该精确版本记录了已批准例外。采用例外时必须验证 macOS 确为 ad-hoc、Windows 确为 `NotSigned`,并随安装包分发 `UNSIGNED-PLATFORM` 风险说明、provenance 和 checksum,之后才能最后原子替换生产 `latest.json`。
|
网页端、macOS 和 Windows 桌面端必须从同一个 `v1.2.0` 标签构建。不得从不同分支、不同提交或本地未提交状态构建正式制品。macOS 签名/公证、Windows 代码签名/RFC 3161 时间戳以及两个平台的 updater 签名制品必须全部验证通过后,才能分发安装包并最后原子替换生产 `latest.json`。
|
||||||
|
|
||||||
发布记录至少包含:
|
发布记录至少包含:
|
||||||
|
|
||||||
@@ -450,7 +450,6 @@ git log --oneline --decorate --graph --all -30
|
|||||||
- [ ] `npm run build` 通过
|
- [ ] `npm run build` 通过
|
||||||
- [ ] `npm run desktop:build:app` 通过
|
- [ ] `npm run desktop:build:app` 通过
|
||||||
- [ ] 正式桌面发布构建已使用 updater 签名私钥执行
|
- [ ] 正式桌面发布构建已使用 updater 签名私钥执行
|
||||||
- [ ] 平台签名已验证,或当前精确版本已在 `frontend/desktop-release-policy.json` 获批例外且 `UNSIGNED-PLATFORM` 风险说明、provenance 和 checksum 齐备
|
|
||||||
- [ ] 数据库迁移与回滚方案确认
|
- [ ] 数据库迁移与回滚方案确认
|
||||||
- [ ] 发布说明完成
|
- [ ] 发布说明完成
|
||||||
- [ ] `main -> release` 合并完成
|
- [ ] `main -> release` 合并完成
|
||||||
|
|||||||
@@ -87,11 +87,7 @@ npm run desktop:build:app
|
|||||||
|
|
||||||
`release:env:check` verifies build channel and commit metadata, and becomes
|
`release:env:check` verifies build channel and commit metadata, and becomes
|
||||||
platform-strict with `REQUIRE_DESKTOP_SIGNING=true` on macOS or
|
platform-strict with `REQUIRE_DESKTOP_SIGNING=true` on macOS or
|
||||||
`REQUIRE_WINDOWS_SIGNING=true` on Windows. Formal native jobs also set
|
`REQUIRE_WINDOWS_SIGNING=true` on Windows.
|
||||||
`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
|
`desktop:release:check` statically verifies the Tauri bundle, updater public
|
||||||
key, CSP, capability scopes, command allowlist, query-token ban, generic system
|
key, CSP, capability scopes, command allowlist, query-token ban, generic system
|
||||||
notification boundary, CI gate coverage, and secure session token boundary. The
|
notification boundary, CI gate coverage, and secure session token boundary. The
|
||||||
@@ -136,10 +132,10 @@ The release pipeline must:
|
|||||||
|
|
||||||
1. build from the accepted release tag and commit;
|
1. build from the accepted release tag and commit;
|
||||||
2. build the Universal macOS app/DMG and Windows x64 NSIS installer from that tag;
|
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;
|
3. sign and notarize macOS, and Authenticode-sign Windows with an RFC 3161 timestamp;
|
||||||
4. produce macOS `.app.tar.gz` and Windows `.nsis.zip` updater artifacts plus their `.sig` files with the shared updater private key;
|
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;
|
5. generate one combined `latest.json` and checksum manifest with `npm run desktop:update-feed:create`;
|
||||||
6. 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`;
|
6. 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`;
|
||||||
7. upload immutable artifacts first;
|
7. upload immutable artifacts first;
|
||||||
8. atomically replace `latest.json` last.
|
8. atomically replace `latest.json` last.
|
||||||
|
|
||||||
@@ -147,33 +143,22 @@ For Universal macOS artifacts, `latest.json` must provide both
|
|||||||
`darwin-aarch64` and `darwin-x86_64` entries pointing at the same Universal
|
`darwin-aarch64` and `darwin-x86_64` entries pointing at the same Universal
|
||||||
update package.
|
update package.
|
||||||
|
|
||||||
The same feed must also contain `windows-x86_64`, pointing to the updater-signed
|
The same feed must also contain `windows-x86_64`, pointing to the signed NSIS
|
||||||
NSIS `.nsis.zip` package. The Windows `.exe` installer is distributed next to
|
`.nsis.zip` updater package. The Windows `.exe` installer is distributed next
|
||||||
the updater package but is not used as the updater URL. Tauri updater signing is
|
to the updater package but is not used as the updater URL.
|
||||||
mandatory in both platform-signed and platform-signing-exception modes.
|
|
||||||
|
|
||||||
The formal release candidate workflow lives at
|
The signed release candidate workflow lives at
|
||||||
`.github/workflows/desktop-release-candidate.yml`. It must be run from a
|
`.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
|
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
|
artifact. That artifact is still only a release candidate; the release owner
|
||||||
must upload immutable files to the production download origin and replace
|
must upload immutable files to the production download origin and replace
|
||||||
`latest.json` atomically after validation.
|
`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 release directory contain `UNSIGNED-PLATFORM`; the directory must include
|
|
||||||
`UNSIGNED-PLATFORM-RELEASE.txt`, `DESKTOP-RELEASE-PROVENANCE.json`, and
|
|
||||||
`SHA256SUMS.txt`. 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.
|
|
||||||
|
|
||||||
## Windows Release and Internal Validation
|
## Windows Release and Internal Validation
|
||||||
|
|
||||||
Windows x64 NSIS is an approved formal Desktop target. Formal Windows builds
|
Windows x64 NSIS is an approved formal Desktop target. Formal Windows builds
|
||||||
run in `.github/workflows/desktop-release-candidate.yml` from the same exact
|
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:
|
`vX.Y.Z` tag and SHA as Web and macOS. They must:
|
||||||
|
|
||||||
- import a Base64-encoded PFX from `WINDOWS_CERTIFICATE` using
|
- import a Base64-encoded PFX from `WINDOWS_CERTIFICATE` using
|
||||||
`WINDOWS_CERTIFICATE_PASSWORD`;
|
`WINDOWS_CERTIFICATE_PASSWORD`;
|
||||||
@@ -185,12 +170,6 @@ run in `.github/workflows/desktop-release-candidate.yml` from the same exact
|
|||||||
- publish the signed NSIS `.exe`, `.nsis.zip`, and `.nsis.zip.sig` into the
|
- publish the signed NSIS `.exe`, `.nsis.zip`, and `.nsis.zip.sig` into the
|
||||||
combined verified Desktop release directory.
|
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:
|
Windows release validation also covers:
|
||||||
|
|
||||||
- WebView2 runtime prerequisite behavior;
|
- WebView2 runtime prerequisite behavior;
|
||||||
@@ -207,23 +186,19 @@ only, runs on `windows-latest`, injects `VITE_BUILD_CHANNEL` and
|
|||||||
and Desktop safety gates, builds an unsigned NSIS installer with updater
|
and Desktop safety gates, builds an unsigned NSIS installer with updater
|
||||||
artifacts disabled, and uploads the `.exe` plus `SHA256SUMS.txt` as a GitHub
|
artifacts disabled, and uploads the `.exe` plus `SHA256SUMS.txt` as a GitHub
|
||||||
Actions artifact. This workflow is for internal compatibility verification
|
Actions artifact. This workflow is for internal compatibility verification
|
||||||
only; it must not generate `latest.json`, update feeds, verified release
|
only; it must not generate `latest.json`, update feeds, signed release
|
||||||
directories, or formal Windows release artifacts. A successful internal build
|
directories, or formal Windows release artifacts. A successful internal build
|
||||||
does not substitute for the tag-only formal workflow, including when that
|
does not substitute for the signed tag-only release workflow.
|
||||||
formal workflow uses an approved platform-signing exception.
|
|
||||||
|
|
||||||
The formal workflow always requires these organization settings:
|
The formal workflow requires these organization settings:
|
||||||
|
|
||||||
- secrets: `TAURI_SIGNING_PRIVATE_KEY` and
|
- secrets: `TAURI_SIGNING_PRIVATE_KEY`,
|
||||||
`TAURI_SIGNING_PRIVATE_KEY_PASSWORD`;
|
`TAURI_SIGNING_PRIVATE_KEY_PASSWORD`, `WINDOWS_CERTIFICATE`, and
|
||||||
|
`WINDOWS_CERTIFICATE_PASSWORD`;
|
||||||
|
- variable: `WINDOWS_TIMESTAMP_URL`;
|
||||||
- shared versioned HTTPS artifact prefix: `DESKTOP_UPDATE_BASE_URL` or the
|
- shared versioned HTTPS artifact prefix: `DESKTOP_UPDATE_BASE_URL` or the
|
||||||
manual workflow input.
|
manual workflow input.
|
||||||
|
|
||||||
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
|
The checked-in workflow implements the exportable PFX path. Confirm that the
|
||||||
organization's certificate policy permits an exportable CI certificate before
|
organization's certificate policy permits an exportable CI certificate before
|
||||||
provisioning it. If the selected CA provides only hardware- or cloud-backed
|
provisioning it. If the selected CA provides only hardware- or cloud-backed
|
||||||
@@ -249,21 +224,16 @@ npm run build
|
|||||||
npm run desktop:build:app
|
npm run desktop:build:app
|
||||||
export TAURI_SIGNING_PRIVATE_KEY="$UPDATER_PRIVATE_KEY"
|
export TAURI_SIGNING_PRIVATE_KEY="$UPDATER_PRIVATE_KEY"
|
||||||
export TAURI_SIGNING_PRIVATE_KEY_PASSWORD="$UPDATER_PRIVATE_KEY_PASSWORD"
|
export TAURI_SIGNING_PRIVATE_KEY_PASSWORD="$UPDATER_PRIVATE_KEY_PASSWORD"
|
||||||
export REQUIRE_UPDATER_SIGNING=true
|
export REQUIRE_DESKTOP_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 release:env:check
|
||||||
npm run desktop:release-readiness:check
|
npm run desktop:release-readiness:check
|
||||||
npm run desktop:build:macos-unsigned-release -- --ci
|
npm run desktop:build:macos-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: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
|
npm run desktop:update-feed:check -- --feed <release-dir>/latest.json --artifacts-dir <release-dir> --require-platform windows-x86_64
|
||||||
```
|
```
|
||||||
|
|
||||||
The macOS and Windows builds run in their native CI jobs and always use the same
|
The macOS and Windows signed builds run in their native CI jobs. macOS requires
|
||||||
updater signing key. In the default path, macOS requires Apple
|
the Apple signing/notarization credentials defined by the release owner;
|
||||||
signing/notarization credentials and Windows requires the PFX/password and
|
Windows requires the PFX certificate/password and timestamp URL above. Both use
|
||||||
timestamp URL. In the approved v0.1.0 exception, macOS must verify
|
the same updater signing key and are aggregated only after both native jobs
|
||||||
`Signature=adhoc`, Windows must verify `NotSigned`, and both must carry explicit
|
pass. Unsigned internal builds are not formal distributions.
|
||||||
platform-trust warnings. Native artifacts are aggregated only after both jobs
|
|
||||||
and the combined updater feed pass.
|
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"schemaVersion": 1,
|
|
||||||
"defaultPlatformSigningMode": "signed",
|
|
||||||
"updaterSigningRequired": true,
|
|
||||||
"unsignedPlatformExceptions": [
|
|
||||||
{
|
|
||||||
"version": "0.1.0",
|
|
||||||
"status": "approved",
|
|
||||||
"macos": "ad-hoc",
|
|
||||||
"windows": "unsigned",
|
|
||||||
"distribution": "controlled",
|
|
||||||
"approvedOn": "2026-07-17",
|
|
||||||
"reason": "The v0.1.0 release owner cannot currently provide Apple Developer signing/notarization credentials or an organization Windows code-signing certificate."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
CTMS DESKTOP PLATFORM SIGNING WARNING
|
|
||||||
|
|
||||||
This release is distributed under the version-scoped v0.1.0 platform-signing exception.
|
|
||||||
|
|
||||||
- The macOS application uses an ad-hoc signature and is not Apple-notarized.
|
|
||||||
- The Windows application and installer have no Authenticode signature or RFC 3161 timestamp.
|
|
||||||
- macOS Gatekeeper and Windows SmartScreen warnings are expected.
|
|
||||||
- Tauri updater artifacts remain cryptographically signed and must pass updater feed verification.
|
|
||||||
- Install only after verifying the release tag, source commit, SHA256SUMS.txt, and DESKTOP-RELEASE-PROVENANCE.json through a trusted channel.
|
|
||||||
|
|
||||||
This exception does not apply to later versions unless they are separately approved in desktop-release-policy.json.
|
|
||||||
@@ -15,10 +15,7 @@
|
|||||||
"desktop:build": "tauri build",
|
"desktop:build": "tauri build",
|
||||||
"desktop:build:app": "tauri build --config '{\"bundle\":{\"createUpdaterArtifacts\":false}}' --bundles app",
|
"desktop:build:app": "tauri build --config '{\"bundle\":{\"createUpdaterArtifacts\":false}}' --bundles app",
|
||||||
"desktop:build:macos-release": "tauri build --target universal-apple-darwin --bundles app,dmg",
|
"desktop:build:macos-release": "tauri build --target universal-apple-darwin --bundles app,dmg",
|
||||||
"desktop:build:macos-unsigned-release": "tauri build --target universal-apple-darwin --bundles app,dmg --config '{\"bundle\":{\"macOS\":{\"signingIdentity\":\"-\"}}}'",
|
|
||||||
"desktop:build:windows-release": "tauri build --bundles nsis",
|
"desktop:build:windows-release": "tauri build --bundles nsis",
|
||||||
"desktop:release-policy:check": "node scripts/resolve-desktop-release-policy.mjs",
|
|
||||||
"desktop:release-provenance:create": "node scripts/create-desktop-release-provenance.mjs",
|
|
||||||
"desktop:update-feed:create": "node scripts/create-desktop-update-feed.mjs",
|
"desktop:update-feed:create": "node scripts/create-desktop-update-feed.mjs",
|
||||||
"desktop:bundle:dmg": "tauri build --bundles dmg",
|
"desktop:bundle:dmg": "tauri build --bundles dmg",
|
||||||
"desktop:update-feed:check": "node scripts/verify-desktop-update-feed.mjs",
|
"desktop:update-feed:check": "node scripts/verify-desktop-update-feed.mjs",
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
||||||
import { dirname, resolve } from "node:path";
|
|
||||||
import { fileURLToPath } from "node:url";
|
|
||||||
import { loadDesktopReleasePolicy, resolveDesktopReleasePolicy } from "./desktop-release-policy.mjs";
|
|
||||||
|
|
||||||
const frontendDir = fileURLToPath(new URL("../", import.meta.url));
|
|
||||||
const packageInfo = JSON.parse(await readFile(resolve(frontendDir, "package.json"), "utf8"));
|
|
||||||
const args = process.argv.slice(2);
|
|
||||||
const value = (name) => {
|
|
||||||
const index = args.indexOf(name);
|
|
||||||
return index >= 0 ? args[index + 1] : undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
const policy = await loadDesktopReleasePolicy();
|
|
||||||
const resolution = resolveDesktopReleasePolicy(packageInfo.version, policy);
|
|
||||||
const tag = value("--tag") || process.env.GITHUB_REF_NAME;
|
|
||||||
const commit = value("--commit") || process.env.GITHUB_SHA;
|
|
||||||
const macosSigning = value("--macos-signing");
|
|
||||||
const windowsSigning = value("--windows-signing");
|
|
||||||
const outputPath = resolve(
|
|
||||||
frontendDir,
|
|
||||||
value("--output") || "src-tauri/target/desktop-release-feed/DESKTOP-RELEASE-PROVENANCE.json",
|
|
||||||
);
|
|
||||||
|
|
||||||
const failures = [];
|
|
||||||
const assert = (condition, message) => {
|
|
||||||
if (!condition) failures.push(message);
|
|
||||||
};
|
|
||||||
|
|
||||||
assert(tag === `v${packageInfo.version}`, `Release provenance tag must be v${packageInfo.version}.`);
|
|
||||||
assert(/^[0-9a-f]{40}$/i.test(commit || ""), "Release provenance commit must be a full 40-character SHA.");
|
|
||||||
assert(macosSigning === resolution.macosSigning, `macOS signing mode must be ${resolution.macosSigning}.`);
|
|
||||||
assert(windowsSigning === resolution.windowsSigning, `Windows signing mode must be ${resolution.windowsSigning}.`);
|
|
||||||
|
|
||||||
if (failures.length > 0) {
|
|
||||||
throw new Error(`Desktop release provenance creation failed:\n${failures.map((item) => ` - ${item}`).join("\n")}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const provenance = {
|
|
||||||
schemaVersion: 1,
|
|
||||||
product: "CTMS Desktop",
|
|
||||||
version: packageInfo.version,
|
|
||||||
tag,
|
|
||||||
commit,
|
|
||||||
platformSigningMode: resolution.platformSigningMode,
|
|
||||||
platformSigning: {
|
|
||||||
macos: macosSigning,
|
|
||||||
windows: windowsSigning,
|
|
||||||
},
|
|
||||||
updaterSigning: "required-and-verified",
|
|
||||||
artifactLabel: resolution.artifactLabel,
|
|
||||||
distribution: resolution.distribution,
|
|
||||||
warnings: resolution.warningRequired
|
|
||||||
? [
|
|
||||||
"macOS is ad-hoc signed and not Apple-notarized; Gatekeeper warnings are expected.",
|
|
||||||
"Windows is not Authenticode-signed or RFC 3161 timestamped; SmartScreen warnings are expected.",
|
|
||||||
"Verify the release tag, commit, updater signatures, and SHA256SUMS.txt through a trusted channel before installation.",
|
|
||||||
]
|
|
||||||
: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
await mkdir(dirname(outputPath), { recursive: true });
|
|
||||||
await writeFile(outputPath, `${JSON.stringify(provenance, null, 2)}\n`);
|
|
||||||
console.log(`Desktop release provenance created at ${outputPath}`);
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
import { readFile } from "node:fs/promises";
|
|
||||||
import { resolve } from "node:path";
|
|
||||||
import { fileURLToPath } from "node:url";
|
|
||||||
|
|
||||||
const frontendDir = fileURLToPath(new URL("../", import.meta.url));
|
|
||||||
export const desktopReleasePolicyPath = resolve(frontendDir, "desktop-release-policy.json");
|
|
||||||
|
|
||||||
const semverPattern = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$/;
|
|
||||||
|
|
||||||
const validatePolicy = (policy) => {
|
|
||||||
const failures = [];
|
|
||||||
const assert = (condition, message) => {
|
|
||||||
if (!condition) failures.push(message);
|
|
||||||
};
|
|
||||||
|
|
||||||
assert(policy?.schemaVersion === 1, "desktop release policy schemaVersion must be 1.");
|
|
||||||
assert(
|
|
||||||
policy?.defaultPlatformSigningMode === "signed",
|
|
||||||
"desktop release policy must default platform signing to signed.",
|
|
||||||
);
|
|
||||||
assert(policy?.updaterSigningRequired === true, "desktop release policy must always require updater signing.");
|
|
||||||
assert(
|
|
||||||
Array.isArray(policy?.unsignedPlatformExceptions),
|
|
||||||
"desktop release policy unsignedPlatformExceptions must be an array.",
|
|
||||||
);
|
|
||||||
|
|
||||||
const versions = new Set();
|
|
||||||
for (const exception of policy?.unsignedPlatformExceptions || []) {
|
|
||||||
const prefix = `unsigned platform exception ${exception?.version || "<missing>"}`;
|
|
||||||
assert(semverPattern.test(exception?.version || ""), `${prefix} must use an exact X.Y.Z version.`);
|
|
||||||
assert(!versions.has(exception?.version), `${prefix} is duplicated.`);
|
|
||||||
versions.add(exception?.version);
|
|
||||||
assert(exception?.status === "approved", `${prefix} must have status approved.`);
|
|
||||||
assert(exception?.macos === "ad-hoc", `${prefix} must constrain macOS to ad-hoc signing.`);
|
|
||||||
assert(exception?.windows === "unsigned", `${prefix} must constrain Windows to unsigned.`);
|
|
||||||
assert(exception?.distribution === "controlled", `${prefix} must constrain distribution to controlled.`);
|
|
||||||
assert(/^\d{4}-\d{2}-\d{2}$/.test(exception?.approvedOn || ""), `${prefix} must record an approval date.`);
|
|
||||||
assert(
|
|
||||||
typeof exception?.reason === "string" && exception.reason.trim().length >= 30,
|
|
||||||
`${prefix} must record a substantive reason.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (failures.length > 0) {
|
|
||||||
throw new Error(failures.join("\n"));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const loadDesktopReleasePolicy = async () => {
|
|
||||||
const policy = JSON.parse(await readFile(desktopReleasePolicyPath, "utf8"));
|
|
||||||
validatePolicy(policy);
|
|
||||||
return policy;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const resolveDesktopReleasePolicy = (version, policy) => {
|
|
||||||
if (!semverPattern.test(version || "")) {
|
|
||||||
throw new Error(`Desktop release version must use exact X.Y.Z semver; found ${version || "<missing>"}.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const exception = policy.unsignedPlatformExceptions.find(
|
|
||||||
(candidate) => candidate.version === version && candidate.status === "approved",
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!exception) {
|
|
||||||
return {
|
|
||||||
version,
|
|
||||||
platformSigningMode: "signed",
|
|
||||||
macosSigning: "apple-developer",
|
|
||||||
windowsSigning: "authenticode",
|
|
||||||
artifactLabel: "SIGNED-PLATFORM",
|
|
||||||
distribution: "formal",
|
|
||||||
warningRequired: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
version,
|
|
||||||
platformSigningMode: "unsigned-exception",
|
|
||||||
macosSigning: exception.macos,
|
|
||||||
windowsSigning: exception.windows,
|
|
||||||
artifactLabel: "UNSIGNED-PLATFORM",
|
|
||||||
distribution: exception.distribution,
|
|
||||||
warningRequired: true,
|
|
||||||
exception,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
import { appendFile, readFile } from "node:fs/promises";
|
|
||||||
import { resolve } from "node:path";
|
|
||||||
import { fileURLToPath } from "node:url";
|
|
||||||
import { loadDesktopReleasePolicy, resolveDesktopReleasePolicy } from "./desktop-release-policy.mjs";
|
|
||||||
|
|
||||||
const frontendDir = fileURLToPath(new URL("../", import.meta.url));
|
|
||||||
const packageInfo = JSON.parse(await readFile(resolve(frontendDir, "package.json"), "utf8"));
|
|
||||||
const args = process.argv.slice(2);
|
|
||||||
const value = (name) => {
|
|
||||||
const index = args.indexOf(name);
|
|
||||||
return index >= 0 ? args[index + 1] : undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
const policy = await loadDesktopReleasePolicy();
|
|
||||||
const resolution = resolveDesktopReleasePolicy(packageInfo.version, policy);
|
|
||||||
const expectedTag = `v${packageInfo.version}`;
|
|
||||||
|
|
||||||
if (args.includes("--require-tag")) {
|
|
||||||
if (process.env.GITHUB_REF_TYPE !== "tag" || process.env.GITHUB_REF_NAME !== expectedTag) {
|
|
||||||
throw new Error(
|
|
||||||
`Desktop release policy must be resolved from tag ${expectedTag}; found ${process.env.GITHUB_REF_TYPE || "<missing>"} ${process.env.GITHUB_REF_NAME || "<missing>"}.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const outputs = {
|
|
||||||
version: resolution.version,
|
|
||||||
platform_signing_mode: resolution.platformSigningMode,
|
|
||||||
macos_signing: resolution.macosSigning,
|
|
||||||
windows_signing: resolution.windowsSigning,
|
|
||||||
artifact_label: resolution.artifactLabel,
|
|
||||||
distribution: resolution.distribution,
|
|
||||||
warning_required: String(resolution.warningRequired),
|
|
||||||
updater_signing_required: String(policy.updaterSigningRequired),
|
|
||||||
};
|
|
||||||
|
|
||||||
const githubOutput = value("--github-output");
|
|
||||||
if (githubOutput) {
|
|
||||||
await appendFile(githubOutput, `${Object.entries(outputs).map(([key, output]) => `${key}=${output}`).join("\n")}\n`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Desktop release policy check passed.");
|
|
||||||
console.log(` version: ${resolution.version}`);
|
|
||||||
console.log(` platform signing mode: ${resolution.platformSigningMode}`);
|
|
||||||
console.log(` macOS: ${resolution.macosSigning}`);
|
|
||||||
console.log(` Windows: ${resolution.windowsSigning}`);
|
|
||||||
console.log(" updater signing: required");
|
|
||||||
@@ -2,13 +2,10 @@ import { execFileSync } from "node:child_process";
|
|||||||
import { readFile } from "node:fs/promises";
|
import { readFile } from "node:fs/promises";
|
||||||
import { resolve } from "node:path";
|
import { resolve } from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import { loadDesktopReleasePolicy, resolveDesktopReleasePolicy } from "./desktop-release-policy.mjs";
|
|
||||||
|
|
||||||
const frontendDir = fileURLToPath(new URL("../", import.meta.url));
|
const frontendDir = fileURLToPath(new URL("../", import.meta.url));
|
||||||
const rootDir = resolve(frontendDir, "..");
|
const rootDir = resolve(frontendDir, "..");
|
||||||
const packageInfo = JSON.parse(await readFile(resolve(frontendDir, "package.json"), "utf8"));
|
const packageInfo = JSON.parse(await readFile(resolve(frontendDir, "package.json"), "utf8"));
|
||||||
const desktopReleasePolicy = await loadDesktopReleasePolicy();
|
|
||||||
const desktopReleaseResolution = resolveDesktopReleasePolicy(packageInfo.version, desktopReleasePolicy);
|
|
||||||
const failures = [];
|
const failures = [];
|
||||||
|
|
||||||
const env = process.env;
|
const env = process.env;
|
||||||
@@ -16,10 +13,6 @@ const fullShaPattern = /^[0-9a-f]{40}$/i;
|
|||||||
const expectedTag = `v${packageInfo.version}`;
|
const expectedTag = `v${packageInfo.version}`;
|
||||||
const requiresMacosSigning = env.REQUIRE_DESKTOP_SIGNING === "true";
|
const requiresMacosSigning = env.REQUIRE_DESKTOP_SIGNING === "true";
|
||||||
const requiresWindowsSigning = env.REQUIRE_WINDOWS_SIGNING === "true";
|
const requiresWindowsSigning = env.REQUIRE_WINDOWS_SIGNING === "true";
|
||||||
const requiresUpdaterSigning = env.REQUIRE_UPDATER_SIGNING === "true";
|
|
||||||
const allowsUnsignedPlatformRelease = env.ALLOW_UNSIGNED_PLATFORM_RELEASE === "true";
|
|
||||||
const desktopPlatformSigningMode = env.DESKTOP_PLATFORM_SIGNING_MODE;
|
|
||||||
const desktopReleasePlatform = env.DESKTOP_RELEASE_PLATFORM;
|
|
||||||
const requiredUpdaterEnv = ["TAURI_SIGNING_PRIVATE_KEY", "TAURI_SIGNING_PRIVATE_KEY_PASSWORD"];
|
const requiredUpdaterEnv = ["TAURI_SIGNING_PRIVATE_KEY", "TAURI_SIGNING_PRIVATE_KEY_PASSWORD"];
|
||||||
const requiredMacosEnv = ["APPLE_ID", "APPLE_PASSWORD", "APPLE_TEAM_ID"];
|
const requiredMacosEnv = ["APPLE_ID", "APPLE_PASSWORD", "APPLE_TEAM_ID"];
|
||||||
const requiredWindowsEnv = ["WINDOWS_CERTIFICATE", "WINDOWS_CERTIFICATE_PASSWORD"];
|
const requiredWindowsEnv = ["WINDOWS_CERTIFICATE", "WINDOWS_CERTIFICATE_PASSWORD"];
|
||||||
@@ -45,7 +38,7 @@ const gitMaybe = (args) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const requireEnv = (name) => {
|
const requireEnv = (name) => {
|
||||||
assert(Boolean(env[name]), `${name} must be configured for desktop release readiness.`);
|
assert(Boolean(env[name]), `${name} must be configured for signed desktop release readiness.`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateBaseUrl = () => {
|
const validateBaseUrl = () => {
|
||||||
@@ -102,54 +95,19 @@ if (headSha && env.VITE_BUILD_COMMIT) {
|
|||||||
assert(env.VITE_BUILD_COMMIT === headSha, "VITE_BUILD_COMMIT must match the current release commit.");
|
assert(env.VITE_BUILD_COMMIT === headSha, "VITE_BUILD_COMMIT must match the current release commit.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assert(
|
||||||
|
requiresMacosSigning || requiresWindowsSigning,
|
||||||
|
"Release readiness requires REQUIRE_DESKTOP_SIGNING=true or REQUIRE_WINDOWS_SIGNING=true.",
|
||||||
|
);
|
||||||
assert(
|
assert(
|
||||||
!(requiresMacosSigning && requiresWindowsSigning),
|
!(requiresMacosSigning && requiresWindowsSigning),
|
||||||
"macOS and Windows signing readiness must be checked in their native jobs.",
|
"macOS and Windows signing readiness must be checked in their native jobs.",
|
||||||
);
|
);
|
||||||
assert(
|
|
||||||
desktopReleasePlatform === "macos" || desktopReleasePlatform === "windows",
|
|
||||||
"DESKTOP_RELEASE_PLATFORM must be macos or windows.",
|
|
||||||
);
|
|
||||||
assert(
|
|
||||||
desktopPlatformSigningMode === desktopReleaseResolution.platformSigningMode,
|
|
||||||
`DESKTOP_PLATFORM_SIGNING_MODE must be ${desktopReleaseResolution.platformSigningMode} for v${packageInfo.version}.`,
|
|
||||||
);
|
|
||||||
assert(
|
|
||||||
desktopReleasePolicy.updaterSigningRequired === true && requiresUpdaterSigning,
|
|
||||||
"Release readiness requires REQUIRE_UPDATER_SIGNING=true; updater signing cannot be disabled.",
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const name of requiredUpdaterEnv) {
|
for (const name of requiredUpdaterEnv) {
|
||||||
requireEnv(name);
|
requireEnv(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (desktopPlatformSigningMode === "signed") {
|
|
||||||
assert(!allowsUnsignedPlatformRelease, "Signed releases must not enable ALLOW_UNSIGNED_PLATFORM_RELEASE.");
|
|
||||||
assert(
|
|
||||||
(desktopReleasePlatform === "macos" && requiresMacosSigning && !requiresWindowsSigning) ||
|
|
||||||
(desktopReleasePlatform === "windows" && requiresWindowsSigning && !requiresMacosSigning),
|
|
||||||
"Signed readiness must enable only the matching native platform signing requirement.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (desktopPlatformSigningMode === "unsigned-exception") {
|
|
||||||
assert(
|
|
||||||
desktopReleaseResolution.platformSigningMode === "unsigned-exception",
|
|
||||||
`v${packageInfo.version} has no approved unsigned platform release exception.`,
|
|
||||||
);
|
|
||||||
assert(allowsUnsignedPlatformRelease, "The approved exception requires ALLOW_UNSIGNED_PLATFORM_RELEASE=true.");
|
|
||||||
assert(
|
|
||||||
!requiresMacosSigning && !requiresWindowsSigning,
|
|
||||||
"Unsigned platform readiness must not claim Apple or Windows platform signing.",
|
|
||||||
);
|
|
||||||
if (desktopReleasePlatform === "macos") {
|
|
||||||
assert(process.platform === "darwin", "The macOS ad-hoc readiness check must run on macOS.");
|
|
||||||
}
|
|
||||||
if (desktopReleasePlatform === "windows") {
|
|
||||||
assert(process.platform === "win32", "The unsigned Windows readiness check must run on Windows.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (requiresMacosSigning) {
|
if (requiresMacosSigning) {
|
||||||
assert(process.platform === "darwin", "Signed macOS desktop release readiness must run on macOS.");
|
assert(process.platform === "darwin", "Signed macOS desktop release readiness must run on macOS.");
|
||||||
for (const name of requiredMacosEnv) {
|
for (const name of requiredMacosEnv) {
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ const verifyTauriConfig = async () => {
|
|||||||
assert(targetList.includes("dmg"), "Tauri bundle targets must include dmg for macOS distribution.");
|
assert(targetList.includes("dmg"), "Tauri bundle targets must include dmg for macOS distribution.");
|
||||||
assert(
|
assert(
|
||||||
tauriConfig.bundle?.createUpdaterArtifacts === true,
|
tauriConfig.bundle?.createUpdaterArtifacts === true,
|
||||||
"Tauri must create signed updater artifacts for formal desktop release builds.",
|
"Tauri must create updater artifacts for signed desktop release builds.",
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
typeof tauriConfig.plugins?.updater?.pubkey === "string" && tauriConfig.plugins.updater.pubkey.length > 80,
|
typeof tauriConfig.plugins?.updater?.pubkey === "string" && tauriConfig.plugins.updater.pubkey.length > 80,
|
||||||
@@ -392,10 +392,7 @@ const verifyWorkflowGates = async () => {
|
|||||||
assert(packageInfo.engines?.node === ">=22.13.0", "package.json must require Node.js >=22.13.0.");
|
assert(packageInfo.engines?.node === ">=22.13.0", "package.json must require Node.js >=22.13.0.");
|
||||||
const requiredScripts = [
|
const requiredScripts = [
|
||||||
"desktop:build:macos-release",
|
"desktop:build:macos-release",
|
||||||
"desktop:build:macos-unsigned-release",
|
|
||||||
"desktop:build:windows-release",
|
"desktop:build:windows-release",
|
||||||
"desktop:release-policy:check",
|
|
||||||
"desktop:release-provenance:create",
|
|
||||||
"desktop:update-feed:create",
|
"desktop:update-feed:create",
|
||||||
"desktop:update-feed:check",
|
"desktop:update-feed:check",
|
||||||
"desktop:release-readiness:check",
|
"desktop:release-readiness:check",
|
||||||
@@ -406,26 +403,6 @@ const verifyWorkflowGates = async () => {
|
|||||||
assert(Boolean(packageInfo.scripts?.[script]), `package.json must define ${script}.`);
|
assert(Boolean(packageInfo.scripts?.[script]), `package.json must define ${script}.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const releasePolicy = await readJson(resolve(frontendDir, "desktop-release-policy.json"));
|
|
||||||
assert(releasePolicy.schemaVersion === 1, "Desktop release policy schemaVersion must be 1.");
|
|
||||||
assert(
|
|
||||||
releasePolicy.defaultPlatformSigningMode === "signed",
|
|
||||||
"Desktop release policy must default future versions to signed platform releases.",
|
|
||||||
);
|
|
||||||
assert(releasePolicy.updaterSigningRequired === true, "Desktop release policy must require updater signing.");
|
|
||||||
const currentUnsignedException = releasePolicy.unsignedPlatformExceptions?.find(
|
|
||||||
(exception) => exception.version === packageInfo.version,
|
|
||||||
);
|
|
||||||
if (packageInfo.version === "0.1.0") {
|
|
||||||
assert(Boolean(currentUnsignedException), "Desktop release policy must record the approved v0.1.0 exception.");
|
|
||||||
}
|
|
||||||
if (currentUnsignedException) {
|
|
||||||
assert(
|
|
||||||
currentUnsignedException.macos === "ad-hoc" && currentUnsignedException.windows === "unsigned",
|
|
||||||
`The v${packageInfo.version} exception must use macOS ad-hoc and unsigned Windows modes.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const workflow = await readFile(resolve(rootDir, ".github/workflows/client-quality-gates.yml"), "utf8");
|
const workflow = await readFile(resolve(rootDir, ".github/workflows/client-quality-gates.yml"), "utf8");
|
||||||
const requiredCommands = [
|
const requiredCommands = [
|
||||||
"npm run version:check",
|
"npm run version:check",
|
||||||
@@ -452,16 +429,6 @@ const verifyWorkflowGates = async () => {
|
|||||||
"runs-on: windows-latest",
|
"runs-on: windows-latest",
|
||||||
"REQUIRE_DESKTOP_SIGNING",
|
"REQUIRE_DESKTOP_SIGNING",
|
||||||
"REQUIRE_WINDOWS_SIGNING",
|
"REQUIRE_WINDOWS_SIGNING",
|
||||||
"REQUIRE_UPDATER_SIGNING",
|
|
||||||
"ALLOW_UNSIGNED_PLATFORM_RELEASE",
|
|
||||||
"DESKTOP_PLATFORM_SIGNING_MODE",
|
|
||||||
"desktop:release-policy:check",
|
|
||||||
"desktop:build:macos-unsigned-release",
|
|
||||||
"Signature=adhoc",
|
|
||||||
'"NotSigned"',
|
|
||||||
"UNSIGNED-PLATFORM",
|
|
||||||
"DESKTOP-RELEASE-PROVENANCE.json",
|
|
||||||
"--require-provenance",
|
|
||||||
"TAURI_SIGNING_PRIVATE_KEY",
|
"TAURI_SIGNING_PRIVATE_KEY",
|
||||||
"APPLE_ID",
|
"APPLE_ID",
|
||||||
"APPLE_PASSWORD",
|
"APPLE_PASSWORD",
|
||||||
|
|||||||
@@ -2,12 +2,9 @@ import { access, readFile } from "node:fs/promises";
|
|||||||
import { createHash } from "node:crypto";
|
import { createHash } from "node:crypto";
|
||||||
import { basename, resolve } from "node:path";
|
import { basename, resolve } from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import { loadDesktopReleasePolicy, resolveDesktopReleasePolicy } from "./desktop-release-policy.mjs";
|
|
||||||
|
|
||||||
const frontendDir = fileURLToPath(new URL("../", import.meta.url));
|
const frontendDir = fileURLToPath(new URL("../", import.meta.url));
|
||||||
const packageInfo = JSON.parse(await readFile(resolve(frontendDir, "package.json"), "utf8"));
|
const packageInfo = JSON.parse(await readFile(resolve(frontendDir, "package.json"), "utf8"));
|
||||||
const desktopReleasePolicy = await loadDesktopReleasePolicy();
|
|
||||||
const desktopReleaseResolution = resolveDesktopReleasePolicy(packageInfo.version, desktopReleasePolicy);
|
|
||||||
const failures = [];
|
const failures = [];
|
||||||
|
|
||||||
const args = process.argv.slice(2);
|
const args = process.argv.slice(2);
|
||||||
@@ -24,11 +21,6 @@ const feedPath = resolve(
|
|||||||
);
|
);
|
||||||
const artifactDir = optionValue("--artifacts-dir") || process.env.DESKTOP_UPDATE_ARTIFACTS_DIR;
|
const artifactDir = optionValue("--artifacts-dir") || process.env.DESKTOP_UPDATE_ARTIFACTS_DIR;
|
||||||
const expectedBaseUrl = optionValue("--base-url") || process.env.DESKTOP_UPDATE_BASE_URL;
|
const expectedBaseUrl = optionValue("--base-url") || process.env.DESKTOP_UPDATE_BASE_URL;
|
||||||
const requiresProvenance = args.includes("--require-provenance");
|
|
||||||
const provenancePath =
|
|
||||||
optionValue("--provenance") ||
|
|
||||||
process.env.DESKTOP_RELEASE_PROVENANCE ||
|
|
||||||
(artifactDir ? resolve(artifactDir, "DESKTOP-RELEASE-PROVENANCE.json") : undefined);
|
|
||||||
const requiredPlatforms = new Set([
|
const requiredPlatforms = new Set([
|
||||||
"darwin-aarch64",
|
"darwin-aarch64",
|
||||||
"darwin-x86_64",
|
"darwin-x86_64",
|
||||||
@@ -102,56 +94,6 @@ const assertManifestEntries = async (checksums) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const verifyProvenance = async (checksums) => {
|
|
||||||
if (!requiresProvenance) return;
|
|
||||||
if (!provenancePath) {
|
|
||||||
fail("Desktop release provenance path is required.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let provenance;
|
|
||||||
try {
|
|
||||||
provenance = JSON.parse(await readFile(provenancePath, "utf8"));
|
|
||||||
} catch (error) {
|
|
||||||
fail(`Cannot read desktop release provenance ${provenancePath}: ${error.message}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(provenance.schemaVersion === 1, "Desktop release provenance schemaVersion must be 1.");
|
|
||||||
assert(provenance.version === packageInfo.version, "Desktop release provenance version must match the package version.");
|
|
||||||
assert(provenance.tag === `v${packageInfo.version}`, "Desktop release provenance tag must match the package version.");
|
|
||||||
assert(/^[0-9a-f]{40}$/i.test(provenance.commit || ""), "Desktop release provenance must contain a full commit SHA.");
|
|
||||||
assert(
|
|
||||||
provenance.platformSigningMode === desktopReleaseResolution.platformSigningMode,
|
|
||||||
`Desktop release provenance platformSigningMode must be ${desktopReleaseResolution.platformSigningMode}.`,
|
|
||||||
);
|
|
||||||
assert(
|
|
||||||
provenance.platformSigning?.macos === desktopReleaseResolution.macosSigning,
|
|
||||||
`Desktop release provenance macOS signing must be ${desktopReleaseResolution.macosSigning}.`,
|
|
||||||
);
|
|
||||||
assert(
|
|
||||||
provenance.platformSigning?.windows === desktopReleaseResolution.windowsSigning,
|
|
||||||
`Desktop release provenance Windows signing must be ${desktopReleaseResolution.windowsSigning}.`,
|
|
||||||
);
|
|
||||||
assert(
|
|
||||||
provenance.updaterSigning === "required-and-verified",
|
|
||||||
"Desktop release provenance must record required-and-verified updater signing.",
|
|
||||||
);
|
|
||||||
assert(
|
|
||||||
provenance.artifactLabel === desktopReleaseResolution.artifactLabel,
|
|
||||||
`Desktop release provenance artifactLabel must be ${desktopReleaseResolution.artifactLabel}.`,
|
|
||||||
);
|
|
||||||
if (desktopReleaseResolution.warningRequired) {
|
|
||||||
assert(
|
|
||||||
Array.isArray(provenance.warnings) && provenance.warnings.length >= 2,
|
|
||||||
"Unsigned platform release provenance must include installation trust warnings.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (artifactDir) {
|
|
||||||
await assertChecksum(checksums, provenancePath, "desktop release provenance");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let feed;
|
let feed;
|
||||||
try {
|
try {
|
||||||
feed = JSON.parse(await readFile(feedPath, "utf8"));
|
feed = JSON.parse(await readFile(feedPath, "utf8"));
|
||||||
@@ -162,7 +104,6 @@ try {
|
|||||||
if (feed) {
|
if (feed) {
|
||||||
const checksums = await readChecksumManifest();
|
const checksums = await readChecksumManifest();
|
||||||
await assertManifestEntries(checksums);
|
await assertManifestEntries(checksums);
|
||||||
await verifyProvenance(checksums);
|
|
||||||
const normalizedFeedVersion = String(feed.version || "").replace(/^v/, "");
|
const normalizedFeedVersion = String(feed.version || "").replace(/^v/, "");
|
||||||
const platforms = feed.platforms || {};
|
const platforms = feed.platforms || {};
|
||||||
const darwinArm = platforms["darwin-aarch64"];
|
const darwinArm = platforms["darwin-aarch64"];
|
||||||
|
|||||||
@@ -2,13 +2,10 @@ import { execFileSync } from "node:child_process";
|
|||||||
import { readFile } from "node:fs/promises";
|
import { readFile } from "node:fs/promises";
|
||||||
import { resolve } from "node:path";
|
import { resolve } from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import { loadDesktopReleasePolicy, resolveDesktopReleasePolicy } from "./desktop-release-policy.mjs";
|
|
||||||
|
|
||||||
const frontendDir = fileURLToPath(new URL("../", import.meta.url));
|
const frontendDir = fileURLToPath(new URL("../", import.meta.url));
|
||||||
const rootDir = resolve(frontendDir, "..");
|
const rootDir = resolve(frontendDir, "..");
|
||||||
const packageInfo = JSON.parse(await readFile(resolve(frontendDir, "package.json"), "utf8"));
|
const packageInfo = JSON.parse(await readFile(resolve(frontendDir, "package.json"), "utf8"));
|
||||||
const desktopReleasePolicy = await loadDesktopReleasePolicy();
|
|
||||||
const desktopReleaseResolution = resolveDesktopReleasePolicy(packageInfo.version, desktopReleasePolicy);
|
|
||||||
const failures = [];
|
const failures = [];
|
||||||
|
|
||||||
const allowedChannels = new Set(["dev", "main", "release", "local"]);
|
const allowedChannels = new Set(["dev", "main", "release", "local"]);
|
||||||
@@ -23,17 +20,6 @@ const isTagBuild = env.GITHUB_REF_TYPE === "tag";
|
|||||||
const isReleaseBuild = env.RELEASE_BUILD === "true" || isTagBuild || channel === "release";
|
const isReleaseBuild = env.RELEASE_BUILD === "true" || isTagBuild || channel === "release";
|
||||||
const requiresMacosSigning = env.REQUIRE_DESKTOP_SIGNING === "true";
|
const requiresMacosSigning = env.REQUIRE_DESKTOP_SIGNING === "true";
|
||||||
const requiresWindowsSigning = env.REQUIRE_WINDOWS_SIGNING === "true";
|
const requiresWindowsSigning = env.REQUIRE_WINDOWS_SIGNING === "true";
|
||||||
const requiresUpdaterSigning = env.REQUIRE_UPDATER_SIGNING === "true";
|
|
||||||
const allowsUnsignedPlatformRelease = env.ALLOW_UNSIGNED_PLATFORM_RELEASE === "true";
|
|
||||||
const desktopPlatformSigningMode = env.DESKTOP_PLATFORM_SIGNING_MODE;
|
|
||||||
const desktopReleasePlatform = env.DESKTOP_RELEASE_PLATFORM;
|
|
||||||
const isNativeDesktopRelease = Boolean(
|
|
||||||
desktopReleasePlatform ||
|
|
||||||
desktopPlatformSigningMode ||
|
|
||||||
requiresMacosSigning ||
|
|
||||||
requiresWindowsSigning ||
|
|
||||||
requiresUpdaterSigning,
|
|
||||||
);
|
|
||||||
|
|
||||||
const fail = (message) => failures.push(message);
|
const fail = (message) => failures.push(message);
|
||||||
const assert = (condition, message) => {
|
const assert = (condition, message) => {
|
||||||
@@ -41,7 +27,7 @@ const assert = (condition, message) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const requireEnv = (name) => {
|
const requireEnv = (name) => {
|
||||||
assert(Boolean(env[name]), `${name} must be configured for this desktop release build.`);
|
assert(Boolean(env[name]), `${name} must be configured for signed desktop release builds.`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const gitHead = () => {
|
const gitHead = () => {
|
||||||
@@ -88,59 +74,13 @@ assert(
|
|||||||
"macOS and Windows signing requirements must be checked in their native jobs.",
|
"macOS and Windows signing requirements must be checked in their native jobs.",
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isNativeDesktopRelease) {
|
|
||||||
assert(isReleaseBuild, "Native desktop release settings may only be used for release builds.");
|
|
||||||
assert(
|
|
||||||
desktopReleasePolicy.updaterSigningRequired === true && requiresUpdaterSigning,
|
|
||||||
"Formal desktop releases must set REQUIRE_UPDATER_SIGNING=true; updater signing cannot be disabled.",
|
|
||||||
);
|
|
||||||
assert(
|
|
||||||
desktopPlatformSigningMode === desktopReleaseResolution.platformSigningMode,
|
|
||||||
`DESKTOP_PLATFORM_SIGNING_MODE must be ${desktopReleaseResolution.platformSigningMode} for v${packageInfo.version}.`,
|
|
||||||
);
|
|
||||||
assert(
|
|
||||||
desktopReleasePlatform === "macos" || desktopReleasePlatform === "windows",
|
|
||||||
"DESKTOP_RELEASE_PLATFORM must be macos or windows for native desktop release builds.",
|
|
||||||
);
|
|
||||||
if (isCi) {
|
|
||||||
assert(isTagBuild, "Native desktop release candidate builds in CI must run from a release tag.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (requiresUpdaterSigning) {
|
|
||||||
requireEnv("TAURI_SIGNING_PRIVATE_KEY");
|
|
||||||
requireEnv("TAURI_SIGNING_PRIVATE_KEY_PASSWORD");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (desktopPlatformSigningMode === "signed") {
|
|
||||||
assert(!allowsUnsignedPlatformRelease, "Signed releases must not enable ALLOW_UNSIGNED_PLATFORM_RELEASE.");
|
|
||||||
assert(
|
|
||||||
(desktopReleasePlatform === "macos" && requiresMacosSigning && !requiresWindowsSigning) ||
|
|
||||||
(desktopReleasePlatform === "windows" && requiresWindowsSigning && !requiresMacosSigning),
|
|
||||||
"Signed native release jobs must enable only their matching platform signing requirement.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (desktopPlatformSigningMode === "unsigned-exception") {
|
|
||||||
assert(
|
|
||||||
desktopReleaseResolution.platformSigningMode === "unsigned-exception",
|
|
||||||
`v${packageInfo.version} has no approved unsigned platform release exception.`,
|
|
||||||
);
|
|
||||||
assert(allowsUnsignedPlatformRelease, "The approved exception requires ALLOW_UNSIGNED_PLATFORM_RELEASE=true.");
|
|
||||||
assert(
|
|
||||||
!requiresMacosSigning && !requiresWindowsSigning,
|
|
||||||
"Unsigned platform exceptions must not claim Apple or Windows platform signing.",
|
|
||||||
);
|
|
||||||
if (desktopReleasePlatform === "macos") {
|
|
||||||
assert(process.platform === "darwin", "The macOS ad-hoc release exception must run on macOS.");
|
|
||||||
}
|
|
||||||
if (desktopReleasePlatform === "windows") {
|
|
||||||
assert(process.platform === "win32", "The unsigned Windows release exception must run on Windows.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (requiresMacosSigning) {
|
if (requiresMacosSigning) {
|
||||||
assert(process.platform === "darwin", "Signed macOS desktop release builds must run on macOS.");
|
assert(process.platform === "darwin", "Signed macOS desktop release builds must run on macOS.");
|
||||||
|
if (isCi) {
|
||||||
|
assert(isTagBuild, "Signed desktop release candidate builds in CI must run from a release tag.");
|
||||||
|
}
|
||||||
|
requireEnv("TAURI_SIGNING_PRIVATE_KEY");
|
||||||
|
requireEnv("TAURI_SIGNING_PRIVATE_KEY_PASSWORD");
|
||||||
requireEnv("APPLE_ID");
|
requireEnv("APPLE_ID");
|
||||||
requireEnv("APPLE_PASSWORD");
|
requireEnv("APPLE_PASSWORD");
|
||||||
requireEnv("APPLE_TEAM_ID");
|
requireEnv("APPLE_TEAM_ID");
|
||||||
@@ -155,6 +95,11 @@ if (requiresMacosSigning) {
|
|||||||
|
|
||||||
if (requiresWindowsSigning) {
|
if (requiresWindowsSigning) {
|
||||||
assert(process.platform === "win32", "Signed Windows desktop release builds must run on Windows.");
|
assert(process.platform === "win32", "Signed Windows desktop release builds must run on Windows.");
|
||||||
|
if (isCi) {
|
||||||
|
assert(isTagBuild, "Signed Windows desktop release candidate builds in CI must run from a release tag.");
|
||||||
|
}
|
||||||
|
requireEnv("TAURI_SIGNING_PRIVATE_KEY");
|
||||||
|
requireEnv("TAURI_SIGNING_PRIVATE_KEY_PASSWORD");
|
||||||
requireEnv("WINDOWS_CERTIFICATE");
|
requireEnv("WINDOWS_CERTIFICATE");
|
||||||
requireEnv("WINDOWS_CERTIFICATE_PASSWORD");
|
requireEnv("WINDOWS_CERTIFICATE_PASSWORD");
|
||||||
requireEnv("WINDOWS_TIMESTAMP_URL");
|
requireEnv("WINDOWS_TIMESTAMP_URL");
|
||||||
|
|||||||
Reference in New Issue
Block a user