name: Package Desktop · Windows · x64 on: workflow_dispatch: inputs: source_ref: description: '打包源码:test 支持仓库内分支或 dsh-v* tag;production 仅支持 master 或 dsh-v* tag;源码须包含 CI 适配脚本' type: string default: master required: true preset: description: '打包环境:选择机器上的 test 或 production 配置;同时决定后续发布环境' type: choice options: [test, production] default: test required: true update_channel: description: '升级通道:nightly 或 latest(须为稳定版本);自动更新不会切换通道' type: choice options: [nightly, latest] default: nightly required: true version_mode: description: '版本模式:default 为 test 自动编号、production 跟随产品;product 保留原版本,auto 自动编号,manual 手填' type: choice options: [default, product, auto, manual] default: default required: true build_version: description: '手动版本号:仅 manual 时填写完整版本;须为产品原版本或受支持的预发布后缀' type: string default: '' required: false test_release_id: description: '测试 release ID:32 位小写十六进制随机前缀(非鉴权);留空使用机器默认值,连续升级测试须保持相同值' type: string default: '' required: false compression_level: description: '安装器压缩等级:3 打包较快,7 压缩更强;覆盖本轮机器配置' type: choice options: ['3', '7'] default: '3' required: true signing: description: '签名方式:unsigned 仅限内部调试,禁止发布到 test / production 或用于自动更新;需要发布请选择 signed(EV 硬件令牌)' type: choice options: [signed, unsigned] default: signed required: true cache_namespace: description: '缓存测量命名空间:留空使用共享缓存;新名称用于冷缓存,再次使用同名测热缓存,不清除现有缓存' type: string default: '' required: false permissions: contents: read concurrency: group: windows-desktop-package cancel-in-progress: false jobs: package: runs-on: dsh-win-package-trial timeout-minutes: 120 defaults: run: shell: pwsh env: PACKAGE_SOURCE_REF: ${{ inputs.source_ref }} PACKAGE_VERSION_MODE: ${{ inputs.version_mode }} DSH_DESKTOP_UPDATE_CHANNEL: ${{ inputs.update_channel }} DSH_TELEMETRY_DISABLED: '1' DSH_PACKAGING_PRESET: ${{ inputs.preset }} PACKAGE_TEST_RELEASE_ID: ${{ inputs.test_release_id }} PACKAGE_SIGNING: ${{ inputs.signing }} PACKAGE_CACHE_NAMESPACE: ${{ inputs.cache_namespace }} PACKAGE_WINDOWS_COMPRESSION_LEVEL: ${{ inputs.compression_level }} steps: - name: Reset packaging checkout working-directory: ${{ runner.temp }} run: | $workspace = [IO.Path]::GetFullPath($env:GITHUB_WORKSPACE) $parent = [IO.Path]::GetFullPath($env:RUNNER_WORKSPACE) if ([IO.Path]::GetDirectoryName($workspace) -ne $parent) { throw 'Checkout must be a direct child of RUNNER_WORKSPACE' } for ($item = [IO.DirectoryInfo]::new($workspace); $null -ne $item; $item = $item.Parent) { if ($item.Exists -and ($item.Attributes -band [IO.FileAttributes]::ReparsePoint)) { throw 'Checkout ancestors cannot be links' } } node -e "const fs = require('node:fs'); fs.rmSync(process.argv[1], { recursive: true, force: true, maxRetries: 3 }); fs.mkdirSync(process.argv[1])" $workspace if ($LASTEXITCODE -ne 0) { throw 'Checkout cleanup failed' } - name: Checkout workflow helpers uses: actions/checkout@v7.0.1 with: persist-credentials: false ref: ${{ github.workflow_sha }} sparse-checkout: apps/desktop/scripts - name: Stage workflow helpers run: | $helpers = Join-Path $env:RUNNER_TEMP "packaging-helpers-$env:GITHUB_RUN_ID-$env:GITHUB_RUN_ATTEMPT" New-Item -ItemType Directory -Path $helpers -ErrorAction Stop | Out-Null foreach ($name in @('windows-packaging-priority.ps1')) { Copy-Item -LiteralPath (Join-Path 'apps/desktop/scripts' $name) -Destination $helpers -ErrorAction Stop } "PACKAGE_CI_HELPERS=$helpers" >> $env:GITHUB_ENV - uses: actions/checkout@v7.0.1 id: checkout with: persist-credentials: false clean: true ref: ${{ inputs.source_ref }} - name: Validate selected source and adapter support run: >- node -e "const fs=require('node:fs'); const ref=process.env.PACKAGE_SOURCE_REF; const cp=require('node:child_process'); const preset=process.env.DSH_PACKAGING_PRESET; if(!['test','production'].includes(preset)) throw Error('Unknown packaging preset'); const tag=/^dsh-v[0-9][0-9A-Za-z.-]*$/.test(ref); if(preset==='production' && ref!=='master' && !tag) throw Error('Production source must be master or dsh-v*'); if(tag) { const v=JSON.parse(fs.readFileSync('package.json','utf8')).version; if(ref!=='dsh-v'+v) throw Error('Source tag must match product version'); const tagged=cp.execFileSync('git',['rev-parse','--verify','refs/tags/'+ref+'^{commit}'],{encoding:'utf8'}).trim(); const head=cp.execFileSync('git',['rev-parse','HEAD'],{encoding:'utf8'}).trim(); if(tagged!==head) throw Error('Checkout does not match source tag'); } else { const branch=cp.execFileSync('git',['rev-parse','--abbrev-ref','HEAD'],{encoding:'utf8'}).trim(); if(branch==='HEAD' || branch!==ref || ref.startsWith('refs/')) throw Error('Select a branch in this repository, not a SHA or PR ref'); } if(!fs.readFileSync('apps/desktop/scripts/desktop-ci-version.mjs','utf8').includes('PACKAGE_VERSION_MODE')) throw Error('Source lacks Desktop channel/version CI support; select a newer release');" - uses: pnpm/action-setup@v5.0.0 with: dest: ${{ runner.temp }}/setup-pnpm-${{ github.run_id }}-${{ github.run_attempt }} - uses: actions/setup-node@v7.0.0 with: node-version: '24' - name: Configure persistent pnpm store # PNPM_HOME belongs to this run; keep downloaded packages outside its temporary directory. env: DSH_DESKTOP_PACKAGE_CACHE_DIR: ${{ runner.tool_cache }}/dsh-desktop-package-cache run: | $storeRoot = Join-Path ([IO.Path]::GetPathRoot($env:GITHUB_WORKSPACE)) '.pnpm-store' $namespace = $env:PACKAGE_CACHE_NAMESPACE if ($namespace) { if ($namespace -cnotmatch '^[a-z0-9][a-z0-9-]{0,63}\z') { throw 'Cache namespace must contain 1-64 lowercase letters, digits or hyphens' } if ($namespace -match '^(con|prn|aux|nul|com[1-9]|lpt[1-9])$') { throw 'Cache namespace cannot be a Windows device name' } $storeRoot = Join-Path $storeRoot $namespace $env:DSH_DESKTOP_PACKAGE_CACHE_DIR = Join-Path $env:DSH_DESKTOP_PACKAGE_CACHE_DIR $namespace } "DSH_DESKTOP_PACKAGE_CACHE_DIR=$env:DSH_DESKTOP_PACKAGE_CACHE_DIR" >> $env:GITHUB_ENV "PNPM_CONFIG_STORE_DIR=$storeRoot" >> $env:GITHUB_ENV - name: Install locked dependencies run: | & "$env:PACKAGE_CI_HELPERS/windows-packaging-priority.ps1" -BestEffort node -e "const os=require('node:os'); console.log(JSON.stringify({node:process.version,availableParallelism:os.availableParallelism(),logicalCpus:os.cpus().length,totalMemoryBytes:os.totalmem(),freeMemoryBytes:os.freemem()}))" if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } pnpm store path if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } pnpm install --frozen-lockfile exit $LASTEXITCODE - name: Validate preset and resolve build version id: settings env: PACKAGE_BUILD_VERSION: ${{ inputs.build_version }} run: node apps/desktop/scripts/prepare-windows-ci.mjs - name: Check packaging configuration and toolchain env: PACKAGE_VERSION: ${{ steps.settings.outputs.version }} run: | $packageArgs = @('run', 'package:desktop:win:x64', '--check', '--build-version', $env:PACKAGE_VERSION) if ($env:PACKAGE_SIGNING -eq 'unsigned') { $packageArgs += '--unsigned' } pnpm @packageArgs exit $LASTEXITCODE - name: Build installer and verify packaged runtime env: PACKAGE_VERSION: ${{ steps.settings.outputs.version }} run: | & "$env:PACKAGE_CI_HELPERS/windows-packaging-priority.ps1" -BestEffort $packageArgs = @('run', 'package:desktop:win:x64', '--build-version', $env:PACKAGE_VERSION) if ($env:PACKAGE_SIGNING -eq 'unsigned') { $packageArgs += '--unsigned' } pnpm @packageArgs exit $LASTEXITCODE - name: Retain complete package outside the checkout id: retained env: PACKAGE_SOURCE_SHA: ${{ steps.checkout.outputs.commit }} PACKAGE_VERSION: ${{ steps.settings.outputs.version }} run: node --import tsx apps/desktop/scripts/retain-desktop-ci.ts - name: Retain packaging diagnostics if: always() && steps.checkout.outcome == 'success' uses: actions/upload-artifact@v7.0.1 with: name: windows-package-logs-${{ github.run_id }}-${{ github.run_attempt }} include-hidden-files: true path: ${{ github.workspace }}/apps/desktop/.desktop-build/packaging-runs/ if-no-files-found: warn retention-days: 14 - name: Retain verified unsigned installer if: success() && inputs.signing == 'unsigned' uses: actions/upload-artifact@v7.0.1 with: name: windows-x64-unsigned-${{ steps.settings.outputs.version }}-${{ github.run_id }}-${{ github.run_attempt }} compression-level: 0 include-hidden-files: true path: ${{ steps.retained.outputs.directory }}/* if-no-files-found: error retention-days: 14 - name: Retain verified signed installer if: success() && inputs.signing == 'signed' uses: actions/upload-artifact@v7.0.1 with: name: windows-x64-signed-${{ steps.settings.outputs.version }}-${{ github.run_id }}-${{ github.run_attempt }} compression-level: 0 include-hidden-files: true path: ${{ steps.retained.outputs.directory }}/* if-no-files-found: error retention-days: 14