name: Release Desktop on: push: tags: ['v*.*.*'] workflow_dispatch: inputs: draft: description: 'Create as draft release' required: false default: true type: boolean permissions: contents: write concurrency: group: release-desktop-${{ github.ref }} cancel-in-progress: true jobs: quality-preflight: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Bun uses: oven-sh/setup-bun@v2 with: bun-version: latest - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 22 - name: Install root dependencies run: bun install - name: Install desktop dependencies working-directory: desktop run: bun install - name: Install adapter dependencies working-directory: adapters run: bun install - name: Verify PR-quality gate before release packaging run: bun run verify signing-preflight: runs-on: ubuntu-latest steps: - name: Validate release signing and notarization secrets shell: bash env: CSC_LINK: ${{ secrets.MACOS_CERTIFICATE }} CSC_KEY_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} WIN_CSC_LINK: ${{ secrets.WINDOWS_CERTIFICATE }} WIN_CSC_KEY_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }} run: | missing=() [ -n "$CSC_LINK" ] || missing+=("MACOS_CERTIFICATE") [ -n "$CSC_KEY_PASSWORD" ] || missing+=("MACOS_CERTIFICATE_PASSWORD") [ -n "$APPLE_ID" ] || missing+=("APPLE_ID") [ -n "$APPLE_APP_SPECIFIC_PASSWORD" ] || missing+=("APPLE_APP_SPECIFIC_PASSWORD") [ -n "$APPLE_TEAM_ID" ] || missing+=("APPLE_TEAM_ID") [ -n "$WIN_CSC_LINK" ] || missing+=("WINDOWS_CERTIFICATE") [ -n "$WIN_CSC_KEY_PASSWORD" ] || missing+=("WINDOWS_CERTIFICATE_PASSWORD") if [ "${#missing[@]}" -gt 0 ]; then printf '::error::Missing required release signing/notarization secrets: %s\n' "${missing[*]}" exit 1 fi build: needs: - quality-preflight - signing-preflight strategy: fail-fast: false matrix: include: - platform: macos-latest target_triple: aarch64-apple-darwin builder_args: --mac dmg zip --arm64 label: macOS-ARM64 smoke_platform: macos - platform: macos-latest target_triple: x86_64-apple-darwin builder_args: --mac dmg zip --x64 label: macOS-x64 smoke_platform: macos - platform: ubuntu-22.04 target_triple: x86_64-unknown-linux-gnu builder_args: --linux AppImage deb --x64 label: Linux-x64 smoke_platform: linux - platform: ubuntu-22.04-arm target_triple: aarch64-unknown-linux-gnu builder_args: --linux AppImage deb --arm64 label: Linux-ARM64 smoke_platform: linux - platform: windows-latest target_triple: x86_64-pc-windows-msvc builder_args: --win nsis --x64 label: Windows-x64 smoke_platform: windows runs-on: ${{ matrix.platform }} name: Build (${{ matrix.label }}) steps: - name: Checkout uses: actions/checkout@v4 - name: Read version id: version shell: bash run: | VERSION=$(node -p "require('./desktop/package.json').version") echo "value=$VERSION" >> "$GITHUB_OUTPUT" - name: Validate tag matches version if: github.event_name == 'push' shell: bash run: | EXPECTED_TAG="v${{ steps.version.outputs.value }}" if [ "${GITHUB_REF_NAME}" != "$EXPECTED_TAG" ]; then echo "::error::Tag ${GITHUB_REF_NAME} does not match app version ${EXPECTED_TAG}" exit 1 fi - name: Install Linux dependencies if: contains(matrix.platform, 'ubuntu') run: | sudo apt-get update sudo apt-get install -y build-essential curl wget file libfuse2 - name: Setup Bun uses: oven-sh/setup-bun@v2 with: bun-version: latest - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 22 - name: Install root dependencies run: bun install - name: Install desktop dependencies working-directory: desktop run: bun install - name: Install adapter dependencies working-directory: adapters run: bun install - name: Build sidecars working-directory: desktop env: BUN_INSTALL_CACHE_DIR: ${{ runner.temp }}/bun-install-cache SIDECAR_TARGET_TRIPLE: ${{ matrix.target_triple }} run: bun run build:sidecars - name: Build renderer and Electron bundles working-directory: desktop run: | bun run build bun run build:electron - name: Build Electron release artifacts working-directory: desktop env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} CSC_LINK: ${{ secrets.MACOS_CERTIFICATE }} CSC_KEY_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} WIN_CSC_LINK: ${{ secrets.WINDOWS_CERTIFICATE }} WIN_CSC_KEY_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }} run: bunx electron-builder ${{ matrix.builder_args }} --publish never - name: Verify packaged app structure run: bun run test:package-smoke --platform ${{ matrix.smoke_platform }} --package-kind release --artifacts-dir desktop/build-artifacts/electron - name: Verify macOS launch policy if: matrix.smoke_platform == 'macos' run: bun run test:package-smoke --platform macos --package-kind release --artifacts-dir desktop/build-artifacts/electron --require-macos-gatekeeper - name: Validate matrix release asset set shell: bash working-directory: desktop/build-artifacts/electron env: APP_VERSION: ${{ steps.version.outputs.value }} run: | case "${{ matrix.label }}" in macOS-ARM64) expected=( "Claude-Code-Haha-${APP_VERSION}-mac-arm64.dmg" "Claude-Code-Haha-${APP_VERSION}-mac-arm64.dmg.blockmap" "Claude-Code-Haha-${APP_VERSION}-mac-arm64.zip" "Claude-Code-Haha-${APP_VERSION}-mac-arm64.zip.blockmap" "latest-mac.yml" ) ;; macOS-x64) expected=( "Claude-Code-Haha-${APP_VERSION}-mac-x64.dmg" "Claude-Code-Haha-${APP_VERSION}-mac-x64.dmg.blockmap" "Claude-Code-Haha-${APP_VERSION}-mac-x64.zip" "Claude-Code-Haha-${APP_VERSION}-mac-x64.zip.blockmap" "latest-mac.yml" ) ;; Linux-x64) expected=( "Claude-Code-Haha-${APP_VERSION}-linux-x64.AppImage" "Claude-Code-Haha-${APP_VERSION}-linux-x64.AppImage.blockmap" "Claude-Code-Haha-${APP_VERSION}-linux-x64.deb" "latest-linux.yml" ) ;; Linux-ARM64) expected=( "Claude-Code-Haha-${APP_VERSION}-linux-arm64.AppImage" "Claude-Code-Haha-${APP_VERSION}-linux-arm64.AppImage.blockmap" "Claude-Code-Haha-${APP_VERSION}-linux-arm64.deb" "latest-linux.yml" ) ;; Windows-x64) expected=( "Claude-Code-Haha-${APP_VERSION}-win-x64.exe" "Claude-Code-Haha-${APP_VERSION}-win-x64.exe.blockmap" "latest.yml" ) ;; *) echo "::error::No expected asset list for matrix label ${{ matrix.label }}" exit 1 ;; esac missing=() for file in "${expected[@]}"; do [ -f "$file" ] || missing+=("$file") done if [ "${#missing[@]}" -gt 0 ]; then printf '::error::Missing release assets for %s: %s\n' "${{ matrix.label }}" "${missing[*]}" exit 1 fi - name: Namespace update metadata assets shell: bash working-directory: desktop/build-artifacts/electron run: | shopt -s nullglob for file in latest*.yml; do mv "$file" "${file%.yml}-${{ matrix.label }}.yml" done - name: Upload update metadata for merge uses: actions/upload-artifact@v4 with: name: desktop-update-metadata-${{ matrix.label }} path: desktop/build-artifacts/electron/latest*.yml if-no-files-found: ignore - name: Upload release artifacts for final publish uses: actions/upload-artifact@v4 with: name: desktop-release-artifacts-${{ matrix.label }} path: | desktop/build-artifacts/electron/*.dmg desktop/build-artifacts/electron/*.zip desktop/build-artifacts/electron/*.exe desktop/build-artifacts/electron/*.AppImage desktop/build-artifacts/electron/*.deb desktop/build-artifacts/electron/*.blockmap desktop/build-artifacts/electron/*.yml if-no-files-found: error publish-release: needs: build runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Read version id: version shell: bash run: | VERSION=$(node -p "require('./desktop/package.json').version") echo "value=$VERSION" >> "$GITHUB_OUTPUT" - name: Validate tag matches version if: github.event_name == 'push' shell: bash run: | EXPECTED_TAG="v${{ steps.version.outputs.value }}" if [ "${GITHUB_REF_NAME}" != "$EXPECTED_TAG" ]; then echo "::error::Tag ${GITHUB_REF_NAME} does not match app version ${EXPECTED_TAG}" exit 1 fi - name: Load release notes id: release_notes shell: bash run: | NOTES_FILE="release-notes/v${{ steps.version.outputs.value }}.md" if [ ! -f "$NOTES_FILE" ]; then echo "::error::Missing release notes file: $NOTES_FILE" exit 1 fi { echo 'body<<__RELEASE_NOTES__' cat "$NOTES_FILE" echo echo '__RELEASE_NOTES__' } >> "$GITHUB_OUTPUT" - name: Setup Bun uses: oven-sh/setup-bun@v2 with: bun-version: latest - name: Install root dependencies run: bun install - name: Download release artifacts uses: actions/download-artifact@v4 with: pattern: desktop-release-artifacts-* path: artifacts/release-assets merge-multiple: true - name: Validate complete release asset set shell: bash env: APP_VERSION: ${{ steps.version.outputs.value }} run: | expected=( "Claude-Code-Haha-${APP_VERSION}-mac-arm64.dmg" "Claude-Code-Haha-${APP_VERSION}-mac-arm64.dmg.blockmap" "Claude-Code-Haha-${APP_VERSION}-mac-arm64.zip" "Claude-Code-Haha-${APP_VERSION}-mac-arm64.zip.blockmap" "Claude-Code-Haha-${APP_VERSION}-mac-x64.dmg" "Claude-Code-Haha-${APP_VERSION}-mac-x64.dmg.blockmap" "Claude-Code-Haha-${APP_VERSION}-mac-x64.zip" "Claude-Code-Haha-${APP_VERSION}-mac-x64.zip.blockmap" "Claude-Code-Haha-${APP_VERSION}-linux-x64.AppImage" "Claude-Code-Haha-${APP_VERSION}-linux-x64.AppImage.blockmap" "Claude-Code-Haha-${APP_VERSION}-linux-x64.deb" "Claude-Code-Haha-${APP_VERSION}-linux-arm64.AppImage" "Claude-Code-Haha-${APP_VERSION}-linux-arm64.AppImage.blockmap" "Claude-Code-Haha-${APP_VERSION}-linux-arm64.deb" "Claude-Code-Haha-${APP_VERSION}-win-x64.exe" "Claude-Code-Haha-${APP_VERSION}-win-x64.exe.blockmap" ) missing=() for file in "${expected[@]}"; do [ -f "artifacts/release-assets/$file" ] || missing+=("$file") done if [ "${#missing[@]}" -gt 0 ]; then printf '::error::Missing complete release assets: %s\n' "${missing[*]}" exit 1 fi - name: Download update metadata artifacts uses: actions/download-artifact@v4 with: pattern: desktop-update-metadata-* path: artifacts/update-metadata merge-multiple: true - name: Merge standard update metadata run: bun run scripts/release-update-metadata.ts --metadata-dir artifacts/update-metadata --out-dir artifacts/update-metadata-standard - name: Validate standard update metadata set shell: bash run: | expected=( "latest-mac.yml" "latest-linux.yml" "latest-linux-arm64.yml" "latest.yml" ) missing=() for file in "${expected[@]}"; do [ -f "artifacts/update-metadata-standard/$file" ] || missing+=("$file") done if [ "${#missing[@]}" -gt 0 ]; then printf '::error::Missing standard update metadata: %s\n' "${missing[*]}" exit 1 fi - name: Publish complete GitHub release uses: softprops/action-gh-release@v2 with: tag_name: v${{ steps.version.outputs.value }} name: Claude Code Haha v${{ steps.version.outputs.value }} body: ${{ steps.release_notes.outputs.body }} draft: ${{ github.event_name == 'workflow_dispatch' && inputs.draft || false }} prerelease: false fail_on_unmatched_files: true files: | artifacts/release-assets/**/*.dmg artifacts/release-assets/**/*.zip artifacts/release-assets/**/*.exe artifacts/release-assets/**/*.AppImage artifacts/release-assets/**/*.deb artifacts/release-assets/**/*.blockmap artifacts/update-metadata-standard/*.yml