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
113 changes: 102 additions & 11 deletions .github/workflows/release-sign.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,20 +80,79 @@ jobs:
set -euo pipefail
diff -u dist-original/SHA256SUMS.txt dist-rebuild/SHA256SUMS.txt

desktop-certification:
runs-on: ubuntu-latest
needs: verify-source
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

- name: Setup Node
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4
with:
node-version: 22
cache: npm

- name: Install dependencies
run: npm ci

- name: Run desktop certification tests
run: |
set -euo pipefail
npm run build:quick
node --test \
dist/tests/installer/control-plane-client.test.js \
dist/tests/installer/desktop-control-plane.test.js \
dist/tests/installer/desktop-preview-startup.test.js \
dist/tests/installer/desktop-rollout-validation.test.js \
dist/tests/installer/desktop-shell-ux.test.js \
dist/tests/installer/desktop-updater.test.js

- name: Generate desktop certification report
run: |
set -euo pipefail
./scripts/release/build-artifacts.sh "${GITHUB_REF_NAME}" "${GITHUB_SHA}" dist
node scripts/release/validate-desktop-release.mjs --write-certification-report dist

- name: Upload desktop certification report
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: desktop-certification-${{ github.ref_name }}
path: |
dist/desktop-certification-report.json
if-no-files-found: error

build-desktop:
strategy:
fail-fast: false
matrix:
include:
- os: macos-latest
target: "--mac dmg"
artifact_glob: "dist/desktop-release/*.dmg\ndist/desktop-release/latest-mac.yml\ndist/desktop-release/*.blockmap"
arch: x64
target: "--mac dmg --x64"
artifact_glob: "dist/desktop-release/*-mac-x64.dmg\ndist/desktop-release/latest-mac.yml\ndist/desktop-release/*-mac-x64.dmg.blockmap"
- os: windows-latest
arch: x64
target: "--win nsis --x64"
artifact_glob: "dist/desktop-release/*-win-x64.exe\ndist/desktop-release/latest.yml\ndist/desktop-release/*-win-x64.exe.blockmap"
- os: ubuntu-latest
arch: x64
target: "--linux AppImage --x64"
artifact_glob: "dist/desktop-release/*-linux-x64.AppImage\ndist/desktop-release/latest-linux.yml\ndist/desktop-release/*-linux-x64.AppImage.blockmap"
- os: macos-latest
arch: arm64
target: "--mac dmg --arm64"
artifact_glob: "dist/desktop-release/*-mac-arm64.dmg\ndist/desktop-release/latest-mac.yml\ndist/desktop-release/*-mac-arm64.dmg.blockmap"
- os: windows-latest
target: "--win nsis"
artifact_glob: "dist/desktop-release/*.exe\ndist/desktop-release/latest.yml\ndist/desktop-release/*.blockmap"
arch: arm64
target: "--win nsis --arm64"
artifact_glob: "dist/desktop-release/*-win-arm64.exe\ndist/desktop-release/latest.yml\ndist/desktop-release/*-win-arm64.exe.blockmap"
- os: ubuntu-latest
target: "--linux AppImage"
artifact_glob: "dist/desktop-release/*.AppImage\ndist/desktop-release/latest-linux.yml\ndist/desktop-release/*.blockmap"
arch: arm64
target: "--linux AppImage --arm64"
artifact_glob: "dist/desktop-release/*-linux-arm64.AppImage\ndist/desktop-release/latest-linux.yml\ndist/desktop-release/*-linux-arm64.AppImage.blockmap"
runs-on: ${{ matrix.os }}
permissions:
contents: read
Expand Down Expand Up @@ -130,14 +189,15 @@ jobs:
- name: Upload desktop release assets
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: desktop-${{ matrix.os }}-${{ github.ref_name }}
name: desktop-${{ matrix.os }}-${{ matrix.arch }}-${{ github.ref_name }}
path: ${{ matrix.artifact_glob }}
if-no-files-found: error

publish-release:
runs-on: ubuntu-latest
needs:
- verify-source
- desktop-certification
- build-desktop
permissions:
contents: write
Expand All @@ -155,21 +215,48 @@ jobs:
- name: Download macOS desktop assets
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: desktop-macos-latest-${{ github.ref_name }}
name: desktop-macos-latest-x64-${{ github.ref_name }}
path: dist/desktop

- name: Download macOS arm64 desktop assets
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: desktop-macos-latest-arm64-${{ github.ref_name }}
path: dist/desktop

- name: Download Windows desktop assets
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: desktop-windows-latest-${{ github.ref_name }}
name: desktop-windows-latest-x64-${{ github.ref_name }}
path: dist/desktop

- name: Download Windows arm64 desktop assets
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: desktop-windows-latest-arm64-${{ github.ref_name }}
path: dist/desktop

- name: Download Linux desktop assets
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: desktop-ubuntu-latest-${{ github.ref_name }}
name: desktop-ubuntu-latest-x64-${{ github.ref_name }}
path: dist/desktop

- name: Download Linux arm64 desktop assets
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: desktop-ubuntu-latest-arm64-${{ github.ref_name }}
path: dist/desktop

- name: Download desktop certification report
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: desktop-certification-${{ github.ref_name }}
path: dist

- name: Validate desktop certification gate before draft release
run: npm run validate:desktop:release -- dist

- name: Install Cosign
uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3.7.0

Expand All @@ -182,7 +269,7 @@ jobs:
--output-signature "${file}.sig" \
--output-certificate "${file}.pem" \
"${file}"
done < <(find dist -type f \( -name '*.gz' -o -name '*.zip' -o -name '*.dmg' -o -name '*.exe' -o -name '*.AppImage' -o -name 'latest*.yml' -o -name '*.blockmap' -o -name 'SHA256SUMS.txt' -o -name 'desktop-validation-matrix.json' \) | sort)
done < <(find dist -type f \( -name '*.gz' -o -name '*.zip' -o -name '*.dmg' -o -name '*.exe' -o -name '*.AppImage' -o -name 'latest*.yml' -o -name '*.blockmap' -o -name 'SHA256SUMS.txt' -o -name 'desktop-validation-matrix.json' -o -name 'desktop-certification-report.json' \) | sort)

- name: Create draft GitHub release
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2
Expand All @@ -195,6 +282,7 @@ jobs:
dist/ica-${{ github.ref_name }}-source.zip
dist/SHA256SUMS.txt
dist/desktop-validation-matrix.json
dist/desktop-certification-report.json
dist/desktop/**/*.dmg
dist/desktop/**/*.exe
dist/desktop/**/*.AppImage
Expand All @@ -203,5 +291,8 @@ jobs:
dist/**/*.sig
dist/**/*.pem

- name: Re-validate desktop certification gate before release promotion
run: npm run validate:desktop:release -- dist

- name: Publish release only after validation passes
run: gh release edit "${GITHUB_REF_NAME}" --draft=false
33 changes: 28 additions & 5 deletions docs/release-signing.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Release Signing and Desktop Promotion
# Release Signing, Certification, and Desktop Promotion

This repository publishes releases from `.github/workflows/release-sign.yml` when a SemVer tag such as `v12.3.0` is pushed.

Expand All @@ -10,6 +10,7 @@ The workflow still produces deterministic source archives and verifies them in a
- `ica-<tag>-source.zip`
- `SHA256SUMS.txt`
- `desktop-validation-matrix.json`
- `desktop-certification-report.json`

These source artifacts are rebuilt and compared before any desktop release publication continues.

Expand All @@ -23,21 +24,42 @@ Desktop packaging now runs on platform-native runners instead of shipping placeh

The Electron packaging contract lives in `electron-builder.json` and publishes to GitHub Releases only.

## Desktop Certification

Desktop releases now have an explicit certification gate before draft creation and before draft promotion. The gate is driven by:

- `desktop-validation-matrix.json`: target matrix, required acceptance checks, certification gates, updater artifacts, and credential expectations
- `desktop-certification-report.json`: CI-produced pass report for required desktop acceptance checks
- `scripts/release/validate-desktop-release.mjs`: fail-closed validator that checks manifests, certification evidence, and packaged desktop artifacts

The certification suite covers these required behaviors for every supported target:

- install flow availability
- startup and launch diagnostics
- control-plane availability
- sync flow availability
- publish flow availability
- updater feed and updater lifecycle behavior
- failure and recovery UX

## Signing and Notarization

- macOS uses Apple code signing and notarization when `APPLE_ID`, `APPLE_APP_SPECIFIC_PASSWORD`, and `APPLE_TEAM_ID` are configured.
- Windows uses Authenticode-compatible signing inputs via `CSC_LINK` and `CSC_KEY_PASSWORD`.
- Linux artifacts and updater metadata are signed with Cosign in the release workflow.
- Release assets are signed only after the platform matrix and source verification complete.
- Local builds can emit release artifacts without production signing credentials, but desktop certification keeps those signing and notarization checks marked as CI-required and not locally exercised.

## Draft Promotion Flow

Desktop releases are created as a draft release first. The release is published only after validation passes:

1. source reproducibility succeeds
2. every desktop platform job uploads its signed artifact set
3. updater metadata files (`latest*.yml`) are present for the packaged outputs
4. `desktop-validation-matrix.json` ships with the signed release asset set
2. the desktop certification test suite passes and emits `desktop-certification-report.json`
3. every desktop platform job uploads its signed artifact set
4. updater metadata files (`latest*.yml`) are present for the packaged outputs
5. `desktop-validation-matrix.json` and `desktop-certification-report.json` ship with the signed release asset set
6. `scripts/release/validate-desktop-release.mjs` passes before draft creation and again before draft-to-published promotion

If any platform packaging, signing, or updater verification step fails, the release remains unpublished.

Expand All @@ -46,4 +68,5 @@ If any platform packaging, signing, or updater verification step fails, the rele
1. Install dependencies: `npm ci`
2. Build preview bundle: `npm run build:desktop:preview`
3. Build release artifacts locally: `npm run build:desktop:release`
4. Publish from CI using the tag-driven workflow
4. Run local certification validation when artifacts are available: `npm run validate:desktop:release -- dist`
5. Publish from CI using the tag-driven workflow
22 changes: 15 additions & 7 deletions docs/testing/desktop-rollout-validation.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Desktop Rollout Validation

This repository publishes a `desktop-validation-matrix.json` artifact alongside the desktop release metadata so release operators can verify platform coverage and rollout gates before promoting a desktop release.
This repository publishes a `desktop-validation-matrix.json` artifact alongside a CI-generated `desktop-certification-report.json` so release operators can verify platform coverage and certification gates before promoting a desktop release.

## Validation Matrix Contents

Expand All @@ -13,25 +13,33 @@ The validation matrix covers every supported desktop target:
- `linux/x64`
- `linux/arm64`

Each target includes these required smoke checks:
Each target includes these required acceptance checks:

- `package-contract`: the per-target desktop package artifact exists and matches the release plan
- `updater-feed`: the updater feed path is stable and publishable
- `desktop-startup`: the packaged dashboard bundle boots successfully in the desktop shell
- `desktop-control-plane`: the Electron bridge and control-plane path stay available
- `install-flow`: install requests remain reachable through the desktop control-plane contract
- `sync-flow`: sync requests remain reachable through the desktop control-plane contract
- `publish-flow`: publish requests remain reachable through the desktop shell
- `updater-lifecycle`: check/download/quit-install flows remain explicit for preview and packaged runtimes
- `failure-recovery-ux`: startup and update failures expose recovery diagnostics instead of silent breakage

## Rollout Gates
## Certification Gates

Every release must satisfy these rollout gates before promotion:
Every release must satisfy these certification gates before promotion:

- `reproducible-build`: release artifacts rebuild deterministically before signing
- `signed-release-metadata`: signed release metadata is keylessly produced and verified in CI
- `validation-matrix-published`: `desktop-validation-matrix.json` is uploaded with the signed release asset set
- `desktop-artifacts-present`: every supported target publishes its package plus updater metadata outputs
- `desktop-acceptance-passed`: CI emits a `desktop-certification-report.json` marking every required acceptance check as passed

## Operator Flow

1. Push the release tag and wait for `.github/workflows/release-sign.yml` to finish.
2. Download `desktop-release-plan.json`, `desktop-updater-manifest.json`, and `desktop-validation-matrix.json`.
2. Download `desktop-release-plan.json`, `desktop-updater-manifest.json`, `desktop-validation-matrix.json`, and `desktop-certification-report.json`.
3. Confirm the target list matches the intended OS and architecture matrix.
4. Verify the rollout gates were enforced by CI before publishing or promoting the release.
5. Use the matrix as the release-readiness checklist for desktop rollout approval.
4. Verify the certification report marks install, startup, control-plane, sync, publish, updater, failure, and recovery checks as passed.
5. Verify the certification gates were enforced by CI before publishing or promoting the release.
6. Use the matrix as the release-readiness checklist for desktop rollout approval.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"build:desktop:preview": "npm run build:quick && npm run build:dashboard:web",
"build:desktop:release": "npm run build:desktop:preview && electron-builder --config electron-builder.json --publish never",
"build:desktop:publish": "npm run build:desktop:preview && electron-builder --config electron-builder.json --publish always",
"validate:desktop:release": "node scripts/release/validate-desktop-release.mjs",
"start:desktop": "npm run build:desktop:preview && electron dist/src/desktop-electron/app.js",
"start:desktop:dev": "npm run build:quick && ICA_DESKTOP_MODE=dev electron dist/src/desktop-electron/app.js",
"start:dashboard": "node dist/src/installer-dashboard/server/index.js",
Expand Down
21 changes: 17 additions & 4 deletions scripts/release/build-desktop-manifests.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@

import fs from "node:fs";
import path from "node:path";
import { createDesktopSmokeChecks, desktopRolloutGates, desktopTargets } from "./desktop-targets.mjs";
import {
createDesktopAcceptanceChecks,
desktopCertificationGates,
desktopTargets,
getDesktopSigningCredentialStatus,
getDesktopUpdaterArtifacts,
} from "./desktop-targets.mjs";

const [versionTag, outputDirArg] = process.argv.slice(2);

Expand All @@ -24,8 +30,9 @@ const generatedAt = resolveGeneratedAt(process.env.SOURCE_DATE_EPOCH);

const releaseTargets = desktopTargets.map((target) => {
const id = `${target.platform}-${target.arch}`;
const artifactName = `ica-desktop-${versionTag}-${target.osToken}-${target.arch}.${target.artifactFormat}`;
const artifactName = `ica-desktop-${versionTag}-${target.artifactNameToken}-${target.arch}.${target.artifactFormat}`;
const publishPath = `desktop/stable/${target.platform}/${target.arch}/${artifactName}`;
const updaterArtifacts = getDesktopUpdaterArtifacts(target).map((artifact) => artifact.replace("ica-desktop-<tag>", `ica-desktop-${versionTag}`));
return {
id,
platform: target.platform,
Expand All @@ -34,9 +41,11 @@ const releaseTargets = desktopTargets.map((target) => {
artifactFormat: target.artifactFormat,
publishPath,
updaterChannel: "stable",
updaterArtifacts,
signing: {
provider: "sigstore-keyless",
requirements: target.signingRequirements,
credentials: getDesktopSigningCredentialStatus(target),
},
};
});
Expand Down Expand Up @@ -65,14 +74,18 @@ const validationMatrix = {
schemaVersion: 1,
generatedAt,
version,
rolloutGates: desktopRolloutGates,
certificationGates: desktopCertificationGates,
targets: releaseTargets.map((target) => ({
id: target.id,
platform: target.platform,
arch: target.arch,
packageArtifactName: target.artifactName,
packageFormat: target.artifactFormat,
updaterFeedPath: `desktop/stable/${target.platform}/${target.arch}/latest.json`,
smokeChecks: createDesktopSmokeChecks(),
updaterArtifacts: target.updaterArtifacts,
signingRequirements: target.signing.requirements,
signingCredentials: target.signing.credentials,
acceptanceChecks: createDesktopAcceptanceChecks(),
})),
};

Expand Down
Loading
Loading