diff --git a/.github/workflows/release-desktop-app.yml b/.github/workflows/release-desktop-app.yml index 92a52bcbf3..e326a64c65 100644 --- a/.github/workflows/release-desktop-app.yml +++ b/.github/workflows/release-desktop-app.yml @@ -77,34 +77,23 @@ jobs: make-distribution: environment: production strategy: - # all combinations: macos-latest x64, macos-latest arm64, windows-latest x64, ubuntu-latest x64 + # all combinations: macos-latest x64, macos-latest arm64, ubuntu-latest x64 + # For windows, we need a separate approach matrix: spec: - - { - os: macos-latest, - platform: darwin, - arch: x64, - target: x86_64-apple-darwin, - } - - { - os: macos-latest, - platform: darwin, - arch: arm64, - target: aarch64-apple-darwin, - } - - { - os: ubuntu-latest, - platform: linux, - arch: x64, - target: x86_64-unknown-linux-gnu, - } - - { - os: windows-latest, - platform: win32, - arch: x64, - target: x86_64-pc-windows-msvc, - } - runs-on: ${{ matrix.spec.os }} + - runner: macos-latest + platform: darwin + arch: x64 + target: x86_64-apple-darwin + - runner: macos-latest + platform: darwin + arch: arm64 + target: aarch64-apple-darwin + - runner: ubuntu-latest + platform: linux + arch: x64 + target: x86_64-unknown-linux-gnu + runs-on: ${{ matrix.spec.runner }} needs: before-make env: APPLE_ID: ${{ secrets.APPLE_ID }} @@ -151,15 +140,6 @@ jobs: mkdir -p builds mv apps/electron/out/*/make/*.dmg ./builds/affine-${{ env.BUILD_TYPE }}-macos-${{ matrix.spec.arch }}.dmg mv apps/electron/out/*/make/zip/darwin/${{ matrix.spec.arch }}/*.zip ./builds/affine-${{ env.BUILD_TYPE }}-macos-${{ matrix.spec.arch }}.zip - - name: Save artifacts (windows) - if: ${{ matrix.spec.platform == 'win32' }} - run: | - mkdir -p builds - mv apps/electron/out/*/make/zip/win32/x64/AFFiNE*-win32-x64-*.zip ./builds/affine-${{ env.BUILD_TYPE }}-windows-x64.zip - mv apps/electron/out/*/make/squirrel.windows/x64/*.exe ./builds/affine-${{ env.BUILD_TYPE }}-windows-x64.exe - mv apps/electron/out/*/make/squirrel.windows/x64/*.msi ./builds/affine-${{ env.BUILD_TYPE }}-windows-x64.msi - mv apps/electron/out/*/make/squirrel.windows/x64/*.nupkg ./builds/affine-${{ env.BUILD_TYPE }}-windows-x64.nupkg - - name: Save artifacts (linux) if: ${{ matrix.spec.platform == 'linux' }} run: | @@ -173,8 +153,166 @@ jobs: name: affine-${{ matrix.spec.platform }}-${{ matrix.spec.arch }}-builds path: builds + package-distribution-windows: + environment: production + strategy: + # all combinations: macos-latest x64, macos-latest arm64, ubuntu-latest x64 + # For windows, we need a separate approach + matrix: + spec: + - runner: windows-latest + platform: win32 + arch: x64 + target: x86_64-pc-windows-msvc + runs-on: ${{ matrix.spec.runner }} + needs: before-make + outputs: + FILES_TO_BE_SIGNED: ${{ steps.get_files_to_be_signed.outputs.FILES_TO_BE_SIGNED }} + env: + SKIP_GENERATE_ASSETS: 1 + steps: + - uses: actions/checkout@v3 + - name: Setup Node.js + timeout-minutes: 10 + uses: ./.github/actions/setup-node + - name: Setup Maker + timeout-minutes: 10 + uses: ./.github/actions/setup-maker + - name: Build AFFiNE native + uses: ./.github/actions/build-rust + with: + target: ${{ matrix.spec.target }} + nx_token: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }} + - uses: actions/download-artifact@v3 + with: + name: core + path: apps/electron/resources/web-static + + - name: Build Plugins + run: yarn run build:plugins + + - name: Build Desktop Layers + run: yarn workspace @affine/electron build + + - name: package + run: yarn workspace @affine/electron package --platform=${{ matrix.spec.platform }} --arch=${{ matrix.spec.arch }} + + - name: get all files to be signed + id: get_files_to_be_signed + run: | + Set-Variable -Name FILES_TO_BE_SIGNED -Value ((Get-ChildItem -Path apps/electron/out -Recurse -File | Where-Object { $_.Extension -in @(".exe", ".node", ".dll", ".msi") } | ForEach-Object { '"' + $_.FullName.Replace((Get-Location).Path + '\apps\electron\out\', '') + '"' }) -join ' ') + "FILES_TO_BE_SIGNED=$FILES_TO_BE_SIGNED" >> $env:GITHUB_OUTPUT + echo $FILES_TO_BE_SIGNED + + - name: Zip artifacts for faster upload + run: Compress-Archive -CompressionLevel Fastest -Path apps/electron/out/* -DestinationPath archive.zip + + - name: Save packaged artifacts for signing + uses: actions/upload-artifact@v3 + with: + name: packaged-${{ matrix.spec.platform }}-${{ matrix.spec.arch }} + path: | + archive.zip + !**/*.map + + sign-packaged-artifacts-windows: + needs: package-distribution-windows + uses: ./.github/workflows/windows-signer.yml + with: + files: ${{ needs.package-distribution-windows.outputs.FILES_TO_BE_SIGNED }} + artifact-name: packaged-win32-x64 + + make-windows-installer: + environment: production + needs: sign-packaged-artifacts-windows + strategy: + # all combinations: macos-latest x64, macos-latest arm64, ubuntu-latest x64 + # For windows, we need a separate approach + matrix: + spec: + - runner: windows-latest + platform: win32 + arch: x64 + target: x86_64-pc-windows-msvc + runs-on: ${{ matrix.spec.runner }} + outputs: + FILES_TO_BE_SIGNED: ${{ steps.get_files_to_be_signed.outputs.FILES_TO_BE_SIGNED }} + steps: + - uses: actions/checkout@v3 + - name: Setup Node.js + timeout-minutes: 10 + uses: ./.github/actions/setup-node + - name: Download and overwrite packaged artifacts + uses: actions/download-artifact@v3 + with: + name: signed-packaged-${{ matrix.spec.platform }}-${{ matrix.spec.arch }} + path: . + - name: unzip file + run: Expand-Archive -Path signed.zip -DestinationPath apps/electron/out + + - name: Make squirrel.windows installer + run: yarn workspace @affine/electron make-squirrel --platform=${{ matrix.spec.platform }} --arch=${{ matrix.spec.arch }} + + - name: Zip artifacts for faster upload + run: Compress-Archive -CompressionLevel Fastest -Path apps/electron/out/${{ env.BUILD_TYPE }}/make/* -DestinationPath archive.zip + + - name: get all files to be signed + id: get_files_to_be_signed + run: | + Set-Variable -Name FILES_TO_BE_SIGNED -Value ((Get-ChildItem -Path apps/electron/out/${{ env.BUILD_TYPE }}/make -Recurse -File | Where-Object { $_.Extension -in @(".exe", ".node", ".dll", ".msi") } | ForEach-Object { '"' + $_.FullName.Replace((Get-Location).Path + '\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: Save installer for signing + uses: actions/upload-artifact@v3 + with: + name: installer-${{ matrix.spec.platform }}-${{ matrix.spec.arch }} + path: archive.zip + + sign-installer-artifacts-windows: + needs: make-windows-installer + uses: ./.github/workflows/windows-signer.yml + with: + files: ${{ needs.make-windows-installer.outputs.FILES_TO_BE_SIGNED }} + artifact-name: installer-win32-x64 + + finalize-installer-windows: + environment: production + needs: sign-installer-artifacts-windows + strategy: + # all combinations: macos-latest x64, macos-latest arm64, ubuntu-latest x64 + # For windows, we need a separate approach + matrix: + spec: + - runner: windows-latest + platform: win32 + arch: x64 + target: x86_64-pc-windows-msvc + runs-on: ${{ matrix.spec.runner }} + steps: + - name: Download and overwrite installer artifacts + uses: actions/download-artifact@v3 + with: + name: signed-installer-${{ matrix.spec.platform }}-${{ matrix.spec.arch }} + path: . + - name: unzip file + run: Expand-Archive -Path signed.zip -DestinationPath apps/electron/out/${{ env.BUILD_TYPE }}/make + + - name: Save artifacts + run: | + mkdir -p builds + mv apps/electron/out/*/make/zip/win32/x64/AFFiNE*-win32-x64-*.zip ./builds/affine-${{ env.BUILD_TYPE }}-windows-x64.zip + mv apps/electron/out/*/make/squirrel.windows/x64/*.exe ./builds/affine-${{ env.BUILD_TYPE }}-windows-x64.exe + mv apps/electron/out/*/make/squirrel.windows/x64/*.msi ./builds/affine-${{ env.BUILD_TYPE }}-windows-x64.msi + + - name: Upload Artifact + uses: actions/upload-artifact@v3 + with: + name: affine-${{ matrix.spec.platform }}-${{ matrix.spec.arch }}-builds + path: builds + release: - needs: [before-make, make-distribution] + needs: [before-make, make-distribution, finalize-installer-windows] runs-on: ubuntu-latest steps: @@ -222,7 +360,6 @@ jobs: ./*.zip ./*.dmg ./*.exe - ./*.nupkg ./RELEASES ./*.AppImage ./*.apk diff --git a/.github/workflows/windows-signer.yml b/.github/workflows/windows-signer.yml new file mode 100644 index 0000000000..36229ff390 --- /dev/null +++ b/.github/workflows/windows-signer.yml @@ -0,0 +1,42 @@ +name: Windows Signer +on: + workflow_call: + inputs: + artifact-name: + required: true + type: string + files: + required: true + type: string +jobs: + sign: + runs-on: [self-hosted, win-signer] + env: + ARCHIVE_DIR: ${{ github.run_id }}-${{ github.run_attempt }}-${{ inputs.artifact-name }} + steps: + - uses: actions/download-artifact@v3 + with: + name: ${{ inputs.artifact-name }} + path: ${{ env.ARCHIVE_DIR }} + - name: unzip file + shell: cmd + # 7za is pre-installed on the signer machine + run: | + cd ${{ env.ARCHIVE_DIR }} + md out + 7za x archive.zip -y -oout + - name: sign + shell: cmd + run: | + cd ${{ env.ARCHIVE_DIR }}/out + signtool sign /tr http://timestamp.sectigo.com /td sha256 /fd sha256 /a ${{ inputs.files }} + - name: zip file + shell: cmd + run: | + cd ${{ env.ARCHIVE_DIR }} + 7za a signed.zip .\out\* + - name: upload + uses: actions/upload-artifact@v3 + with: + name: signed-${{ inputs.artifact-name }} + path: ${{ env.ARCHIVE_DIR }}/signed.zip diff --git a/apps/electron/forge.config.js b/apps/electron/forge.config.js index a2b028efe0..ed2c9b38c7 100644 --- a/apps/electron/forge.config.js +++ b/apps/electron/forge.config.js @@ -1,37 +1,20 @@ /* eslint-disable @typescript-eslint/no-var-requires */ -const { z } = require('zod'); - const { utils: { fromBuildIdentifier }, } = require('@electron-forge/core'); const path = require('node:path'); -const ReleaseTypeSchema = z.enum(['stable', 'beta', 'canary', 'internal']); - -const envBuildType = (process.env.BUILD_TYPE || 'canary').trim().toLowerCase(); -const buildType = ReleaseTypeSchema.parse(envBuildType); -const stableBuild = buildType === 'stable'; -const productName = !stableBuild ? `AFFiNE-${buildType}` : 'AFFiNE'; -const icoPath = !stableBuild - ? `./resources/icons/icon_${buildType}.ico` - : './resources/icons/icon.ico'; -const icnsPath = !stableBuild - ? `./resources/icons/icon_${buildType}.icns` - : './resources/icons/icon.icns'; - -const arch = - process.argv.indexOf('--arch') > 0 - ? process.argv[process.argv.indexOf('--arch') + 1] - : process.arch; - -const platform = - process.argv.indexOf('--platform') > 0 - ? process.argv[process.argv.indexOf('--platform') + 1] - : process.platform; - -const windowsIconUrl = `https://cdn.affine.pro/app-icons/icon_${buildType}.ico`; +const { + arch, + buildType, + icnsPath, + icoPath, + platform, + productName, + iconUrl, +} = require('./scripts/make-env'); const makers = [ !process.env.SKIP_BUNDLE && @@ -84,7 +67,7 @@ const makers = [ config: { name: productName, setupIcon: icoPath, - iconUrl: windowsIconUrl, + iconUrl: iconUrl, loadingGif: './resources/icons/affine_installing.gif', }, }, diff --git a/apps/electron/package.json b/apps/electron/package.json index 0c4f493598..bcba33ff33 100644 --- a/apps/electron/package.json +++ b/apps/electron/package.json @@ -17,7 +17,8 @@ "generate-assets": "zx scripts/generate-assets.mjs", "package": "electron-forge package", "make": "electron-forge make", - "test": "DEBUG=pw:browser yarn -T run playwright test -c ./playwright.config.ts" + "test": "DEBUG=pw:browser yarn -T run playwright test -c ./playwright.config.ts", + "make-squirrel": "yarn ts-node-esm -T scripts/make-squirrel.mts" }, "config": { "forge": "./forge.config.js" diff --git a/apps/electron/scripts/macos-arm64-output-check.mts b/apps/electron/scripts/macos-arm64-output-check.mts index 550204c60b..86c7843cba 100644 --- a/apps/electron/scripts/macos-arm64-output-check.mts +++ b/apps/electron/scripts/macos-arm64-output-check.mts @@ -1,5 +1,5 @@ -import { fileURLToPath } from 'node:url'; import { readdir } from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; const outputRoot = fileURLToPath( new URL( diff --git a/apps/electron/scripts/make-env.js b/apps/electron/scripts/make-env.js new file mode 100644 index 0000000000..c5e834f1e7 --- /dev/null +++ b/apps/electron/scripts/make-env.js @@ -0,0 +1,49 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ + +const { z } = require('zod'); + +const path = require('node:path'); + +const ReleaseTypeSchema = z.enum(['stable', 'beta', 'canary', 'internal']); + +const ROOT = path.resolve(__dirname, '..'); + +const envBuildType = (process.env.BUILD_TYPE || 'canary').trim().toLowerCase(); +const buildType = ReleaseTypeSchema.parse(envBuildType); +const stableBuild = buildType === 'stable'; +const productName = !stableBuild ? `AFFiNE-${buildType}` : 'AFFiNE'; +const icoPath = path.join( + ROOT, + !stableBuild + ? `./resources/icons/icon_${buildType}.ico` + : './resources/icons/icon.ico' +); +const icnsPath = path.join( + ROOT, + !stableBuild + ? `./resources/icons/icon_${buildType}.icns` + : './resources/icons/icon.icns' +); + +const iconUrl = `https://cdn.affine.pro/app-icons/icon_${buildType}.ico`; +const arch = + process.argv.indexOf('--arch') > 0 + ? process.argv[process.argv.indexOf('--arch') + 1] + : process.arch; + +const platform = + process.argv.indexOf('--platform') > 0 + ? process.argv[process.argv.indexOf('--platform') + 1] + : process.platform; + +module.exports = { + ROOT, + buildType, + productName, + icoPath, + icnsPath, + iconUrl, + arch, + platform, + stableBuild, +}; diff --git a/apps/electron/scripts/make-squirrel.mts b/apps/electron/scripts/make-squirrel.mts new file mode 100644 index 0000000000..bc2d9e4370 --- /dev/null +++ b/apps/electron/scripts/make-squirrel.mts @@ -0,0 +1,82 @@ +import type { Options as ElectronWinstallerOptions } from 'electron-winstaller'; +import { convertVersion, createWindowsInstaller } from 'electron-winstaller'; +import fs from 'fs-extra'; +import path from 'path'; + +import { + arch, + buildType, + iconUrl, + platform, + productName, + ROOT, +} from './make-env'; + +async function ensureDirectory(dir: string) { + if (await fs.pathExists(dir)) { + await fs.remove(dir); + } + return fs.mkdirs(dir); +} + +// taking from https://github.com/electron/forge/blob/main/packages/maker/squirrel/src/MakerSquirrel.ts +// it was for forge's maker, but can be used standalone as well +async function make() { + const appName = productName; + const makeDir = path.resolve(ROOT, 'out', buildType, 'make'); + const outPath = path.resolve(makeDir, `squirrel.windows/${arch}`); + const appDirectory = path.resolve( + ROOT, + 'out', + buildType, + `${appName}-${platform}-${arch}` + ); + await ensureDirectory(outPath); + + const packageJSON = await fs.readJson(path.resolve(ROOT, 'package.json')); + + const winstallerConfig: ElectronWinstallerOptions = { + name: appName, + title: appName, + noMsi: true, + exe: `${appName}.exe`, + setupExe: `${appName}-${packageJSON.version} Setup.exe`, + version: packageJSON.version, + appDirectory: appDirectory, + outputDirectory: outPath, + iconUrl: iconUrl, + loadingGif: path.resolve(ROOT, './resources/icons/affine_installing.gif'), + }; + + await createWindowsInstaller(winstallerConfig); + const nupkgVersion = convertVersion(packageJSON.version); + const artifacts = [ + path.resolve(outPath, 'RELEASES'), + path.resolve(outPath, winstallerConfig.setupExe || `${appName}Setup.exe`), + path.resolve( + outPath, + `${winstallerConfig.name}-${nupkgVersion}-full.nupkg` + ), + ]; + const deltaPath = path.resolve( + outPath, + `${winstallerConfig.name}-${nupkgVersion}-delta.nupkg` + ); + if ( + (winstallerConfig.remoteReleases && !winstallerConfig.noDelta) || + (await fs.pathExists(deltaPath)) + ) { + artifacts.push(deltaPath); + } + const msiPath = path.resolve( + outPath, + winstallerConfig.setupMsi || `${appName}Setup.msi` + ); + if (!winstallerConfig.noMsi && (await fs.pathExists(msiPath))) { + artifacts.push(msiPath); + } + console.log('making squirrel.windows done:', artifacts); + return artifacts; +} + +make(); diff --git a/apps/electron/tsconfig.node.json b/apps/electron/tsconfig.node.json index 94614e8be5..5213fbb9c7 100644 --- a/apps/electron/tsconfig.node.json +++ b/apps/electron/tsconfig.node.json @@ -8,7 +8,8 @@ "moduleResolution": "Node", "allowSyntheticDefaultImports": true, "noEmit": false, - "outDir": "./lib/scripts" + "outDir": "./lib/scripts", + "allowJs": true }, "include": ["./scripts", "esbuild.main.config.ts", "esbuild.plugin.config.ts"] }