name: Release Desktop on: workflow_call: inputs: build-type: required: true type: string app-version: required: true type: string git-short-hash: required: true type: string desktop_macos: description: 'Desktop - macOS' required: false default: true type: boolean desktop_windows: description: 'Desktop - Windows' required: false default: true type: boolean desktop_linux: description: 'Desktop - Linux' required: false default: true type: boolean require-windows-signing: description: 'Require all Windows signing steps to succeed before release' required: false default: false type: boolean permissions: actions: write contents: write security-events: write id-token: write attestations: write env: BUILD_TYPE: ${{ inputs.build-type }} RELEASE_VERSION: ${{ inputs.app-version }} DEBUG: 'affine:*,napi:*' APP_NAME: affine MACOSX_DEPLOYMENT_TARGET: '11.6' jobs: before-make: if: ${{ inputs.desktop_macos || inputs.desktop_windows || inputs.desktop_linux }} runs-on: ubuntu-latest environment: ${{ inputs.build-type }} steps: - uses: actions/checkout@v6 - name: Setup Version uses: ./.github/actions/setup-version with: app-version: ${{ inputs.app-version }} - name: Setup Node.js uses: ./.github/actions/setup-node - name: Setup @sentry/cli uses: ./.github/actions/setup-sentry - name: generate-assets run: yarn affine @affine/electron generate-assets env: SENTRY_ORG: ${{ secrets.SENTRY_ORG }} SENTRY_PROJECT: 'affine' SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_DSN: ${{ secrets.SENTRY_DSN }} SENTRY_RELEASE: ${{ inputs.app-version }} RELEASE_VERSION: ${{ inputs.app-version }} - name: Upload web artifact uses: actions/upload-artifact@v4 with: name: desktop-web path: packages/frontend/apps/electron/resources/web-static windows-signer-gate: if: ${{ inputs.desktop_windows }} runs-on: ubuntu-latest outputs: signer_available: ${{ steps.check.outputs.signer_available }} steps: - uses: actions/checkout@v6 - name: Check windows signer availability id: check run: node ./scripts/check-windows-signer.mjs env: AFFINE_SIGN_CLIENT_HASH: ${{ secrets.AFFINE_SIGN_CLIENT_HASH }} AFFINE_SIGNER_ADDR: ${{ secrets.AFFINE_SIGNER_ADDR }} AFFINE_SIGNER_TOKEN: ${{ secrets.AFFINE_SIGNER_TOKEN }} AFFINE_SIGNER_TS_AUTH_KEY: ${{ secrets.AFFINE_SIGNER_TS_AUTH_KEY }} BUILD_TYPE: ${{ inputs.build-type }} REQUIRE_SIGNER: ${{ inputs.require-windows-signing }} WINDOWS_SIGNER_PUBLIC_CERT_BASE64: ${{ secrets.WINDOWS_SIGNER_PUBLIC_CERT_BASE64 }} make-distribution-macos: if: ${{ inputs.desktop_macos }} strategy: fail-fast: false matrix: spec: - runner: macos-latest platform: darwin arch: x64 target: x86_64-apple-darwin - runner: macos-latest platform: darwin arch: arm64 target: aarch64-apple-darwin needs: before-make uses: ./.github/workflows/release-desktop-platform.yml secrets: inherit with: build_type: ${{ inputs.build-type }} app_version: ${{ inputs.app-version }} git_short_hash: ${{ inputs.git-short-hash }} runner: ${{ matrix.spec.runner }} platform: ${{ matrix.spec.platform }} arch: ${{ matrix.spec.arch }} target: ${{ matrix.spec.target }} apple_codesign: true make-distribution-linux: if: ${{ inputs.desktop_linux }} strategy: fail-fast: false matrix: spec: - runner: ubuntu-latest platform: linux arch: x64 target: x86_64-unknown-linux-gnu needs: before-make uses: ./.github/workflows/release-desktop-platform.yml secrets: inherit with: build_type: ${{ inputs.build-type }} app_version: ${{ inputs.app-version }} git_short_hash: ${{ inputs.git-short-hash }} runner: ${{ matrix.spec.runner }} platform: ${{ matrix.spec.platform }} arch: ${{ matrix.spec.arch }} target: ${{ matrix.spec.target }} install_linux_deps: true build-and-sign-windows: if: ${{ inputs.desktop_windows && needs.windows-signer-gate.outputs.signer_available == 'true' }} needs: - before-make - windows-signer-gate strategy: fail-fast: false matrix: spec: - platform: win32 arch: x64 target: x86_64-pc-windows-msvc - platform: win32 arch: arm64 target: aarch64-pc-windows-msvc runs-on: windows-latest env: AFFINE_SIGNER_ADDR: ${{ secrets.AFFINE_SIGNER_ADDR }} AFFINE_SIGNER_TOKEN: ${{ secrets.AFFINE_SIGNER_TOKEN }} APP_NAME: affine BUILD_TYPE: ${{ inputs.build-type }} DEBUG: 'affine:*,napi:*' RELEASE_VERSION: ${{ inputs.app-version }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_DSN: ${{ secrets.SENTRY_DSN }} SENTRY_ORG: ${{ secrets.SENTRY_ORG }} SENTRY_PROJECT: 'affine' SENTRY_RELEASE: ${{ inputs.app-version }} SKIP_GENERATE_ASSETS: 1 TS_RS_EXPERIMENT: this_is_unstable_software steps: - uses: actions/checkout@v6 - name: Setup Version uses: ./.github/actions/setup-version with: app-version: ${{ inputs.app-version }} - name: Setup Node.js timeout-minutes: 10 uses: ./.github/actions/setup-node with: extra-flags: workspaces focus @affine/electron @affine/monorepo @affine/nbstore @toeverything/infra enableScripts: true hard-link-nm: false nmHoistingLimits: workspaces env: npm_config_arch: ${{ matrix.spec.arch }} - name: Build AFFiNE native uses: ./.github/actions/build-rust with: target: ${{ matrix.spec.target }} package: '@affine/native' - name: Download web artifact uses: actions/download-artifact@v4 with: name: desktop-web path: packages/frontend/apps/electron/resources/web-static - name: Build Desktop Layers run: yarn affine @affine/electron build - name: Remove nbstore node_modules shell: bash run: | rm -rf packages/frontend/apps/electron/node_modules/@affine/nbstore/node_modules/@blocksuite/affine/node_modules rm -rf packages/frontend/apps/electron/node_modules/@affine/native/node_modules - name: Package windows app run: yarn affine @affine/electron package --platform=${{ matrix.spec.platform }} --arch=${{ matrix.spec.arch }} env: HOIST_NODE_MODULES: 1 NODE_OPTIONS: --max-old-space-size=14384 SKIP_WEB_BUILD: 1 - name: Connect Tailscale uses: tailscale/github-action@v4 with: authkey: ${{ secrets.AFFINE_SIGNER_TS_AUTH_KEY }} hostname: affine-signer-${{ github.run_id }}-${{ github.run_attempt }}-${{ matrix.spec.arch }} - name: Check signer connectivity shell: pwsh run: | $Parts = "$env:AFFINE_SIGNER_ADDR".Split(':') if ($Parts.Count -ne 2) { throw "AFFINE_SIGNER_ADDR must be host:port, got $env:AFFINE_SIGNER_ADDR" } $Result = Test-NetConnection -ComputerName $Parts[0] -Port ([int]$Parts[1]) if (!$Result.TcpTestSucceeded) { throw "Unable to connect to signer at $env:AFFINE_SIGNER_ADDR" } - name: Download remote signer client shell: pwsh run: | if (!$env:AFFINE_SIGN_CLIENT_HASH) { throw 'AFFINE_SIGN_CLIENT_HASH is required.' } Invoke-WebRequest -Uri "https://cdn.affine.pro/sign-client/$env:AFFINE_SIGN_CLIENT_HASH" -OutFile affine-sign-client.exe env: AFFINE_SIGN_CLIENT_HASH: ${{ secrets.AFFINE_SIGN_CLIENT_HASH }} - name: Prepare public signing certificate shell: pwsh run: | if (!$env:WINDOWS_SIGNER_PUBLIC_CERT_BASE64) { throw 'WINDOWS_SIGNER_PUBLIC_CERT_BASE64 is required.' } [IO.File]::WriteAllBytes('windows-signer-public.cer', [Convert]::FromBase64String($env:WINDOWS_SIGNER_PUBLIC_CERT_BASE64)) env: WINDOWS_SIGNER_PUBLIC_CERT_BASE64: ${{ secrets.WINDOWS_SIGNER_PUBLIC_CERT_BASE64 }} - name: Resolve signtool shell: pwsh run: | $Command = Get-Command signtool.exe -ErrorAction SilentlyContinue if ($Command) { "SIGNTOOL=$($Command.Source)" >> $env:GITHUB_ENV Write-Host "Using signtool: $($Command.Source)" exit 0 } $KitsRoot = "${env:ProgramFiles(x86)}\Windows Kits\10\bin" $Candidates = @() if (Test-Path -LiteralPath $KitsRoot) { $Candidates = Get-ChildItem -Path $KitsRoot -Recurse -Filter signtool.exe | Where-Object { $_.FullName -match '\\x64\\signtool\.exe$' } | Sort-Object FullName -Descending } if ($Candidates.Count -eq 0) { throw "Unable to find signtool.exe under PATH or $KitsRoot" } "SIGNTOOL=$($Candidates[0].FullName)" >> $env:GITHUB_ENV Write-Host "Using signtool: $($Candidates[0].FullName)" - name: Get packaged files to sign id: packaged_files_to_sign shell: pwsh run: | Set-Variable -Name FILES_TO_BE_SIGNED -Value ((Get-ChildItem -Path packages/frontend/apps/electron/out -Recurse -File | Where-Object { $_.Extension -in @(".exe", ".node", ".dll", ".msi") } | ForEach-Object { '"' + $_.FullName.Replace((Get-Location).Path + '\packages\frontend\apps\electron\out\', '') + '"' }) -join ' ') "FILES_TO_BE_SIGNED=$FILES_TO_BE_SIGNED" >> $env:GITHUB_OUTPUT echo $FILES_TO_BE_SIGNED - name: Sign packaged files shell: pwsh run: | ./affine-sign-client.exe ` --server "$env:AFFINE_SIGNER_ADDR" ` --token "$env:AFFINE_SIGNER_TOKEN" ` --workdir packages/frontend/apps/electron/out ` --files '${{ steps.packaged_files_to_sign.outputs.FILES_TO_BE_SIGNED }}' ` --cert windows-signer-public.cer ` --plain-tcp - name: Make squirrel.windows installer run: yarn affine @affine/electron make-squirrel --platform=${{ matrix.spec.platform }} --arch=${{ matrix.spec.arch }} - name: Make nsis.windows installer run: yarn affine @affine/electron make-nsis --platform=${{ matrix.spec.platform }} --arch=${{ matrix.spec.arch }} - name: Get installer files to sign id: installer_files_to_sign shell: pwsh run: | Set-Variable -Name FILES_TO_BE_SIGNED -Value ((Get-ChildItem -Path packages/frontend/apps/electron/out/${{ env.BUILD_TYPE }}/make -Recurse -File | Where-Object { $_.Extension -in @(".exe", ".node", ".dll", ".msi") } | ForEach-Object { '"' + $_.FullName.Replace((Get-Location).Path + '\packages\frontend\apps\electron\out\${{ env.BUILD_TYPE }}\make\', '') + '"' }) -join ' ') "FILES_TO_BE_SIGNED=$FILES_TO_BE_SIGNED" >> $env:GITHUB_OUTPUT echo $FILES_TO_BE_SIGNED - name: Sign installer files shell: pwsh run: | ./affine-sign-client.exe ` --server "$env:AFFINE_SIGNER_ADDR" ` --token "$env:AFFINE_SIGNER_TOKEN" ` --workdir packages/frontend/apps/electron/out/${{ env.BUILD_TYPE }}/make ` --files '${{ steps.installer_files_to_sign.outputs.FILES_TO_BE_SIGNED }}' ` --cert windows-signer-public.cer ` --plain-tcp - name: Save artifacts run: | mkdir -p builds mv packages/frontend/apps/electron/out/*/make/zip/win32/${{ matrix.spec.arch }}/AFFiNE*-win32-${{ matrix.spec.arch }}-*.zip ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-windows-${{ matrix.spec.arch }}.zip mv packages/frontend/apps/electron/out/*/make/squirrel.windows/${{ matrix.spec.arch }}/*.exe ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-windows-${{ matrix.spec.arch }}.exe mv packages/frontend/apps/electron/out/*/make/nsis.windows/${{ matrix.spec.arch }}/*.exe ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-windows-${{ matrix.spec.arch }}.nsis.exe - uses: actions/attest-build-provenance@v4 with: subject-path: | ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-windows-${{ matrix.spec.arch }}.zip ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-windows-${{ matrix.spec.arch }}.exe ./builds/affine-${{ env.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-windows-${{ matrix.spec.arch }}.nsis.exe - name: Upload Artifact uses: actions/upload-artifact@v4 with: name: affine-${{ matrix.spec.platform }}-${{ matrix.spec.arch }}-builds path: builds release: if: >- ${{ always() && (inputs.desktop_macos || inputs.desktop_linux || inputs.desktop_windows) && needs.before-make.result == 'success' && (!inputs.desktop_macos || needs.make-distribution-macos.result == 'success') && (!inputs.desktop_linux || needs.make-distribution-linux.result == 'success') && ( !inputs.desktop_windows || !inputs.require-windows-signing || needs.build-and-sign-windows.result == 'success' ) }} needs: [ before-make, make-distribution-macos, make-distribution-linux, build-and-sign-windows, ] runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Download Artifacts (macos-x64) if: ${{ inputs.desktop_macos }} uses: actions/download-artifact@v4 with: name: affine-darwin-x64-builds path: ./release - name: Download Artifacts (macos-arm64) if: ${{ inputs.desktop_macos }} uses: actions/download-artifact@v4 with: name: affine-darwin-arm64-builds path: ./release - name: Download Artifacts (windows-x64) if: ${{ inputs.desktop_windows && needs.build-and-sign-windows.result == 'success' }} uses: actions/download-artifact@v4 with: name: affine-win32-x64-builds path: ./release - name: Download Artifacts (windows-arm64) if: ${{ inputs.desktop_windows && needs.build-and-sign-windows.result == 'success' }} uses: actions/download-artifact@v4 with: name: affine-win32-arm64-builds path: ./release - name: Download Artifacts (linux-x64) if: ${{ inputs.desktop_linux }} uses: actions/download-artifact@v4 with: name: affine-linux-x64-builds path: ./release - uses: actions/setup-node@v6 with: node-version: 20 - name: Copy Selfhost Release Files run: | cp ./.docker/selfhost/compose.yml ./release/docker-compose.yml cp ./.docker/selfhost/.env.example ./release/.env.example cp ./.docker/selfhost/schema.json ./release/config.schema.json - name: Generate Release yml run: | node ./scripts/generate-release-yml.mjs env: RELEASE_VERSION: ${{ env.RELEASE_VERSION }} - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: name: ${{ env.RELEASE_VERSION }} draft: ${{ inputs.build-type == 'stable' }} prerelease: ${{ inputs.build-type != 'stable' }} tag_name: v${{ env.RELEASE_VERSION}} files: ./release/*