name: Desktop Release on: push: tags: - 'v*' - 'desktop-v*' - 'desktop-macos-v*' - 'desktop-linux-v*' - 'desktop-windows-v*' workflow_dispatch: inputs: tag: description: 'Existing tag to build (e.g. v0.1.0)' required: true type: string platform: description: 'Optional desktop platform to build.' required: false default: 'all' type: choice options: - all - macos - linux - windows concurrency: group: desktop-release-${{ github.ref }} cancel-in-progress: false env: SOURCE_TAG: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || github.ref_name }} DESKTOP_WORKSPACE: '@getpaseo/desktop' DESKTOP_PACKAGE_PATH: 'packages/desktop' jobs: create-release: if: ${{ (github.event_name == 'push' && !startsWith(github.ref_name, 'desktop-macos-v') && !startsWith(github.ref_name, 'desktop-linux-v') && !startsWith(github.ref_name, 'desktop-windows-v')) || (github.event_name == 'workflow_dispatch' && github.event.inputs.platform == 'all') }} permissions: contents: write runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: sparse-checkout: scripts ref: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || github.ref }} - name: Resolve release metadata shell: bash run: node scripts/emit-release-env.mjs --source-tag "$SOURCE_TAG" >> "$GITHUB_ENV" - name: Create GitHub release if: env.IS_SMOKE_TAG != 'true' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | if gh release view "$RELEASE_TAG" --repo "${{ github.repository }}" > /dev/null 2>&1; then echo "Release $RELEASE_TAG already exists, skipping creation" else prerelease_flag="" if [[ "$IS_PRERELEASE" == "true" ]]; then prerelease_flag="--prerelease" fi gh release create "$RELEASE_TAG" \ --repo "${{ github.repository }}" \ --title "Paseo $RELEASE_TAG" \ --notes "" \ $prerelease_flag || { echo "Release creation raced with another workflow; continuing." } fi publish-macos: needs: [create-release] if: ${{ always() && (needs.create-release.result == 'success' || needs.create-release.result == 'skipped') && ((github.event_name == 'workflow_dispatch' && (github.event.inputs.platform == 'all' || github.event.inputs.platform == 'macos')) || (github.event_name == 'push' && (startsWith(github.ref_name, 'v') || startsWith(github.ref_name, 'desktop-v') || startsWith(github.ref_name, 'desktop-macos-v')))) }} strategy: fail-fast: false matrix: include: - runner: macos-14 electron_arch: arm64 - runner: macos-15-intel electron_arch: x64 permissions: contents: write packages: read runs-on: ${{ matrix.runner }} steps: - uses: actions/checkout@v4 with: fetch-depth: 0 ref: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || github.ref }} - name: Resolve release metadata shell: bash run: node scripts/emit-release-env.mjs --source-tag "$SOURCE_TAG" >> "$GITHUB_ENV" - name: Setup Node uses: actions/setup-node@v4 with: node-version: '22' cache: 'npm' cache-dependency-path: package-lock.json registry-url: 'https://npm.pkg.github.com' scope: '@boudra' - name: Install JS dependencies run: npm ci env: NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Set desktop package version from tag shell: bash run: | node <<'NODE' const fs = require('node:fs'); const path = require('node:path'); const version = process.env.DESKTOP_VERSION; if (!version) throw new Error('DESKTOP_VERSION env var is missing'); const packageJsonPath = path.join(process.env.DESKTOP_PACKAGE_PATH, 'package.json'); const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); packageJson.version = version; fs.writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`); NODE - name: Build web app for desktop run: npm run build:web --workspace=@getpaseo/app - name: Build desktop release shell: bash env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} EP_GH_IGNORE_TIME: true CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }} CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_PASSWORD }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} run: | set -euo pipefail publish_mode="never" publish_args=() if [[ "$IS_SMOKE_TAG" != "true" ]]; then publish_mode="always" publish_args+=("-c.publish.releaseType=$RELEASE_TYPE") publish_args+=("-c.publish.channel=$RELEASE_CHANNEL") fi npm run build --workspace="$DESKTOP_WORKSPACE" -- --publish "$publish_mode" --mac --${{ matrix.electron_arch }} "${publish_args[@]}" - name: Upload manifest artifact if: env.IS_SMOKE_TAG != 'true' uses: actions/upload-artifact@v4 with: name: mac-manifest-${{ matrix.electron_arch }} path: ${{ env.DESKTOP_PACKAGE_PATH }}/release/${{ env.RELEASE_CHANNEL }}-mac.yml retention-days: 1 finalize-mac-manifest: needs: [publish-macos] if: ${{ needs.publish-macos.result == 'success' }} permissions: contents: write runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: sparse-checkout: scripts ref: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || github.ref }} - name: Resolve release tag shell: bash run: node scripts/emit-release-env.mjs --source-tag "$SOURCE_TAG" >> "$GITHUB_ENV" - name: Download manifest artifacts if: env.IS_SMOKE_TAG != 'true' uses: actions/download-artifact@v4 with: pattern: mac-manifest-* - name: Merge manifests if: env.IS_SMOKE_TAG != 'true' shell: bash run: | set -euo pipefail node <<'NODE' const fs = require('node:fs'); // Simple YAML parser for electron-builder's latest-mac.yml format function parseManifest(text) { const lines = text.split('\n'); const result = { files: [] }; let currentFile = null; for (const line of lines) { if (line.startsWith('version:')) result.version = line.split(': ')[1].trim(); else if (line.startsWith('path:')) result.path = line.split(': ')[1].trim(); else if (line.startsWith('sha512:') && !currentFile) result.sha512 = line.split(': ')[1].trim(); else if (line.startsWith('releaseDate:')) result.releaseDate = line.split(': ')[1].trim().replace(/'/g, ''); else if (line.trim().startsWith('- url:')) { currentFile = { url: line.trim().replace('- url: ', '') }; result.files.push(currentFile); } else if (line.trim().startsWith('sha512:') && currentFile) { currentFile.sha512 = line.trim().split(': ')[1].trim(); } else if (line.trim().startsWith('size:') && currentFile) { currentFile.size = parseInt(line.trim().split(': ')[1].trim(), 10); currentFile = null; } } return result; } function toYaml(manifest) { let out = `version: ${manifest.version}\n`; out += `files:\n`; for (const f of manifest.files) { out += ` - url: ${f.url}\n`; out += ` sha512: ${f.sha512}\n`; out += ` size: ${f.size}\n`; } out += `path: ${manifest.path}\n`; out += `sha512: ${manifest.sha512}\n`; out += `releaseDate: '${manifest.releaseDate}'\n`; return out; } const manifestName = `${process.env.RELEASE_CHANNEL}-mac.yml`; const arm64Text = fs.readFileSync(`mac-manifest-arm64/${manifestName}`, 'utf8'); const x64Text = fs.readFileSync(`mac-manifest-x64/${manifestName}`, 'utf8'); const arm64 = parseManifest(arm64Text); const x64 = parseManifest(x64Text); // Merge: all files from both, default path points to arm64 zip const merged = { version: arm64.version, files: [...arm64.files, ...x64.files], path: arm64.path, sha512: arm64.sha512, releaseDate: arm64.releaseDate || x64.releaseDate, }; const output = toYaml(merged); fs.writeFileSync(manifestName, output); console.log('Merged manifest:\n' + output); NODE - name: Upload merged manifest to release if: env.IS_SMOKE_TAG != 'true' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: gh release upload "$RELEASE_TAG" "$RELEASE_CHANNEL-mac.yml" --clobber --repo "${{ github.repository }}" publish-linux: needs: [create-release] if: ${{ always() && (needs.create-release.result == 'success' || needs.create-release.result == 'skipped') && ((github.event_name == 'workflow_dispatch' && (github.event.inputs.platform == 'all' || github.event.inputs.platform == 'linux')) || (github.event_name == 'push' && (startsWith(github.ref_name, 'v') || startsWith(github.ref_name, 'desktop-v') || startsWith(github.ref_name, 'desktop-linux-v')))) }} permissions: contents: write packages: read runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 with: fetch-depth: 0 ref: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || github.ref }} - name: Resolve release metadata shell: bash run: node scripts/emit-release-env.mjs --source-tag "$SOURCE_TAG" >> "$GITHUB_ENV" - name: Setup Node uses: actions/setup-node@v4 with: node-version: '22' cache: 'npm' cache-dependency-path: package-lock.json registry-url: 'https://npm.pkg.github.com' scope: '@boudra' - name: Install JS dependencies run: npm ci env: NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Set desktop package version from tag shell: bash run: | node <<'NODE' const fs = require('node:fs'); const path = require('node:path'); const version = process.env.DESKTOP_VERSION; if (!version) throw new Error('DESKTOP_VERSION env var is missing'); const packageJsonPath = path.join(process.env.DESKTOP_PACKAGE_PATH, 'package.json'); const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); packageJson.version = version; fs.writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`); NODE - name: Build web app for desktop run: npm run build:web --workspace=@getpaseo/app - name: Build desktop release shell: bash env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} EP_GH_IGNORE_TIME: true run: | set -euo pipefail publish_mode="never" publish_args=() if [[ "$IS_SMOKE_TAG" != "true" ]]; then publish_mode="always" publish_args+=("-c.publish.releaseType=$RELEASE_TYPE") publish_args+=("-c.publish.channel=$RELEASE_CHANNEL") fi npm run build --workspace="$DESKTOP_WORKSPACE" -- --publish "$publish_mode" --linux --x64 "${publish_args[@]}" publish-windows: needs: [create-release] if: ${{ always() && (needs.create-release.result == 'success' || needs.create-release.result == 'skipped') && ((github.event_name == 'workflow_dispatch' && (github.event.inputs.platform == 'all' || github.event.inputs.platform == 'windows')) || (github.event_name == 'push' && (startsWith(github.ref_name, 'v') || startsWith(github.ref_name, 'desktop-v') || startsWith(github.ref_name, 'desktop-windows-v')))) }} permissions: contents: write packages: read runs-on: windows-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 ref: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || github.ref }} - name: Resolve release metadata shell: bash run: node scripts/emit-release-env.mjs --source-tag "$SOURCE_TAG" >> "$GITHUB_ENV" - name: Setup Node uses: actions/setup-node@v4 with: node-version: '22' cache: 'npm' cache-dependency-path: package-lock.json registry-url: 'https://npm.pkg.github.com' scope: '@boudra' - name: Install JS dependencies run: npm ci env: NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Set desktop package version from tag shell: bash run: | node <<'NODE' const fs = require('node:fs'); const path = require('node:path'); const version = process.env.DESKTOP_VERSION; if (!version) throw new Error('DESKTOP_VERSION env var is missing'); const packageJsonPath = path.join(process.env.DESKTOP_PACKAGE_PATH, 'package.json'); const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); packageJson.version = version; fs.writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`); NODE - name: Build workspace dependencies run: npm run build:workspace-deps --workspace=@getpaseo/app - name: Build web app for desktop shell: pwsh run: | $patchPath = (Get-Item "$env:GITHUB_WORKSPACE/scripts/metro-config-windows-loader-patch.cjs").FullName $env:NODE_OPTIONS = "--require=$patchPath" npx expo export --platform web working-directory: packages/app - name: Build desktop release shell: bash env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} EP_GH_IGNORE_TIME: true run: | set -euo pipefail publish_mode="never" publish_args=() if [[ "$IS_SMOKE_TAG" != "true" ]]; then publish_mode="always" publish_args+=("-c.publish.releaseType=$RELEASE_TYPE") publish_args+=("-c.publish.channel=$RELEASE_CHANNEL") fi npm run build --workspace="$DESKTOP_WORKSPACE" -- --publish "$publish_mode" --win --x64 "${publish_args[@]}"