Merge master and reconcile translation records

This commit is contained in:
Dudu-0223 committed 2026-10-23 18:19:07 +08:00
commit 00a2e76b87
127 files changed
+4167 -557

No files matched your search

+1 -1
View File
@@ -70,7 +70,7 @@ describe('repository build steps', () => {
}
})
it('keeps artifact steps in serial dependency order', () => {
it('finishes Host and Desktop readers before Client compilation rewrites shared outputs', () => {
expect(graph(buildGates(root, 'artifacts'))).toEqual(artifactGraph)
})
+3 -1
View File
@@ -166,10 +166,12 @@ function pnpmStep(id: LibraryStepId, label: string, args: readonly string[], nee
/**
* List the package-library build steps in Host, Desktop, then Client dependency order.
* Source-only Typert generation supplies declarations before any package compilation.
* Client compilation waits for Host and Desktop bundling because split compiler faces
* rewrite shared lib/types files consumed by those bundles.
* @param root - Repository root containing the compiler and bundle configurations.
* @param faces - `host` lists Typert generation, Host compilation, and bundling; `all` adds Desktop and Client.
* @param mode - Check package types by default; benchmark emission defers diagnostics to required full builds.
* @returns The ordered steps; each step needs its predecessor.
* @returns Steps with explicit compiler and bundle dependencies.
* @throws If an aggregate configuration is invalid.
*/
export function libraryBuildSteps(root: string, faces: 'host' | 'all', mode: CompilationMode = 'checked'): LibraryStep[] {
+29 -1
View File
@@ -9,7 +9,16 @@ const workflow = yaml.load(readFileSync(resolve(import.meta.dirname, '../.github
concurrency: { group: string; 'cancel-in-progress': boolean }
jobs: Record<string, {
'runs-on': string[]
steps: Array<{ id?: string; name?: string; if?: string; uses?: string; run?: string; with?: Record<string, unknown> }>
env: Record<string, string>
steps: Array<{
id?: string
name?: string
if?: string
uses?: string
run?: string
env?: Record<string, string>
with?: Record<string, unknown>
}>
}>
}
@@ -29,6 +38,7 @@ it('confines signed Mac packaging to manual dispatch and a dedicated Apple Silic
expect(uploads[0]?.if).toBe("always() && steps.checkout.outcome == 'success'")
expect(uploads[1]?.if).toBe('success()')
expect(uploads[1]?.with?.path).toBe('${{ steps.retained.outputs.directory }}/*')
expect(uploads[1]?.with?.['compression-level']).toBe(0)
})
it('defaults to notarization and labels the explicit debugging option', () => {
@@ -40,3 +50,21 @@ it('defaults to notarization and labels the explicit debugging option', () => {
expect(steps.filter(step => step.run?.includes('args+=(--skip-notarization)'))).toHaveLength(2)
expect(steps.find(step => step.name === 'Retain installers for the selected mode')?.with?.name).toContain('${{ steps.settings.outputs.mode }}')
})
it('retains build inputs outside checkout and run-specific pnpm installations', () => {
const env = workflow.jobs.package!.env
expect(JSON.stringify(env)).not.toContain('runner.')
const steps = workflow.jobs.package!.steps
const cache = steps.find(step => step.name === 'Configure persistent package caches')!
expect(cache.env?.DSH_DESKTOP_PACKAGE_CACHE_DIR).toBe('${{ runner.tool_cache }}/dsh-desktop-package-cache')
expect(cache.run).toContain('PNPM_CONFIG_STORE_DIR=$DSH_DESKTOP_PACKAGE_CACHE_DIR/workspace-pnpm-store')
expect(cache.run).toContain('DSH_DESKTOP_PACKAGE_CACHE_DIR=$DSH_DESKTOP_PACKAGE_CACHE_DIR')
expect(steps.indexOf(cache)).toBeLessThan(steps.findIndex(step => step.name === 'Install locked dependencies'))
})
it('offers ZIP compression choices with a fast default', () => {
expect(workflow.on.workflow_dispatch).toMatchObject({ inputs: { zip_compression_level: {
type: 'choice', options: ['3', '7'], default: '3', required: true,
} } })
expect(workflow.jobs.package!.env.PACKAGE_MACOS_ZIP_COMPRESSION_LEVEL).toBe('${{ inputs.zip_compression_level }}')
})
+66
View File
@@ -0,0 +1,66 @@
import { execFileSync } from 'node:child_process'
import { existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { expect, it } from 'vitest'
import { prepareWindowsBuildWorkspace, removeWindowsBuildWorkspace } from '../apps/desktop/scripts/windows-build-workspace.mjs'
it('clones the exact clean commit and removes only its run-owned checkout', () => {
const root = realpathSync(mkdtempSync(join(tmpdir(), 'package-workspace-')))
try {
const sourceDirectory = join(root, 'original')
mkdirSync(sourceDirectory)
const git = (...args: string[]) => execFileSync('git', args, { cwd: sourceDirectory, encoding: 'utf8', stdio: 'pipe' }).trim()
git('init')
writeFileSync(join(sourceDirectory, 'source.txt'), 'selected source')
writeFileSync(join(sourceDirectory, '.gitignore'), 'private.env\n')
git('add', '.')
git('-c', 'user.name=Fixture', '-c', '[email protected]', 'commit', '-m', 'fixture')
writeFileSync(join(sourceDirectory, 'private.env'), 'ignored fixture')
const options = { sourceDirectory, buildRoot: join(root, 'builds'), runId: '123', runAttempt: '1', sourceSha: git('rev-parse', 'HEAD') }
expect(() => prepareWindowsBuildWorkspace({ ...options, buildRoot: sourceDirectory })).toThrow('outside existing Git')
expect(() => prepareWindowsBuildWorkspace({ ...options, buildRoot: 'relative' })).toThrow('absolute')
expect(() => prepareWindowsBuildWorkspace({ ...options, runId: '../other' })).toThrow('identity')
expect(() => prepareWindowsBuildWorkspace({ ...options, sourceSha: '0'.repeat(40) })).toThrow('match')
writeFileSync(join(sourceDirectory, 'source.txt'), 'dirty')
expect(() => prepareWindowsBuildWorkspace(options)).toThrow('clean')
writeFileSync(join(sourceDirectory, 'source.txt'), 'selected source')
const output = join(root, 'actions-output')
execFileSync(process.execPath, [resolve(import.meta.dirname, '../apps/desktop/scripts/windows-build-workspace.mjs')], {
cwd: sourceDirectory,
env: {
...process.env,
GITHUB_WORKSPACE: sourceDirectory,
PACKAGE_BUILD_ROOT: options.buildRoot,
GITHUB_RUN_ID: options.runId,
GITHUB_RUN_ATTEMPT: options.runAttempt,
PACKAGE_SOURCE_SHA: options.sourceSha,
GITHUB_OUTPUT: output,
},
stdio: 'pipe',
})
const directory = join(options.buildRoot, 'windows-package-123-1', 'source')
const temporaryDirectory = join(options.buildRoot, 'windows-package-123-1', 'temp')
expect(readFileSync(output, 'utf8')).toBe(`directory=${directory}\ntemporaryDirectory=${temporaryDirectory}\n`)
writeFileSync(join(temporaryDirectory, 'temporary.txt'), 'owned temporary data')
expect(readFileSync(join(directory, 'source.txt'), 'utf8')).toBe('selected source')
expect(existsSync(join(directory, 'private.env'))).toBe(false)
expect(() => prepareWindowsBuildWorkspace(options)).toThrow()
expect(() => { removeWindowsBuildWorkspace({ ...options, sourceSha: '0'.repeat(40) }) }).toThrow('ownership')
expect(existsSync(directory)).toBe(true)
const external = join(root, 'external')
mkdirSync(external)
writeFileSync(join(external, 'keep.txt'), 'keep')
symlinkSync(external, join(directory, 'node_modules'), process.platform === 'win32' ? 'junction' : 'dir')
removeWindowsBuildWorkspace(options)
expect(existsSync(directory)).toBe(false)
expect(existsSync(temporaryDirectory)).toBe(false)
expect(readFileSync(join(external, 'keep.txt'), 'utf8')).toBe('keep')
expect(readFileSync(join(sourceDirectory, 'source.txt'), 'utf8')).toBe('selected source')
const linked = join(root, 'linked')
symlinkSync(external, linked, process.platform === 'win32' ? 'junction' : 'dir')
expect(() => prepareWindowsBuildWorkspace({ ...options, buildRoot: join(linked, 'builds') })).toThrow('links')
} finally {
rmSync(root, { recursive: true, force: true })
}
})
+184 -5
View File
@@ -1,13 +1,13 @@
import { execFileSync } from 'node:child_process'
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { resolve } from 'node:path'
import { parse, resolve } from 'node:path'
import yaml from 'js-yaml'
import { expect, it } from 'vitest'
const root = resolve(import.meta.dirname, '..')
const workflow = yaml.load(readFileSync(resolve(root, '.github/workflows/windows-package.yml'), 'utf8')) as {
on: Record<string, { inputs: Record<string, { required?: boolean; default?: string; description?: string }> }>
on: Record<string, { inputs: Record<string, { type?: string; required?: boolean; default?: string; description?: string }> }>
permissions: Record<string, string>
concurrency: { group: string; 'cancel-in-progress': boolean }
jobs: Record<string, {
@@ -19,6 +19,7 @@ const workflow = yaml.load(readFileSync(resolve(root, '.github/workflows/windows
uses?: string
run?: string
if?: string
'working-directory'?: string
env?: Record<string, string>
with?: Record<string, unknown>
}>
@@ -61,12 +62,17 @@ it('routes manual packaging to the dedicated runner with explicit preset and sig
expect(Object.keys(workflow.on)).toEqual(['workflow_dispatch'])
expect(workflow.jobs.package).not.toHaveProperty('name')
expect(job['runs-on']).toBe('dsh-win-package-trial')
expect(JSON.stringify(job.env)).not.toContain('runner.')
expect(job.steps.find(step => step.name === 'Configure persistent pnpm store')?.env?.DSH_DESKTOP_PACKAGE_CACHE_DIR)
.toBe('${{ runner.tool_cache }}/dsh-desktop-package-cache')
expect(workflow.concurrency).toEqual({ group: 'windows-desktop-package', 'cancel-in-progress': false })
expect(workflow.permissions).toEqual({ contents: 'read' })
expect(Object.keys(workflow.on.workflow_dispatch!.inputs).sort()).toEqual([
'build_version', 'preset', 'signing', 'source_ref', 'test_release_id', 'update_channel', 'version_mode',
'build_root', 'build_version', 'cache_namespace', 'compression_level', 'preset', 'signing', 'source_ref', 'test_release_id', 'update_channel', 'version_mode',
])
expect(workflow.on.workflow_dispatch!.inputs.signing!.default).toBe('signed')
expect(workflow.on.workflow_dispatch!.inputs.compression_level).toMatchObject({ type: 'choice', options: ['3', '7'], default: '3', required: true })
expect(job.env.PACKAGE_WINDOWS_COMPRESSION_LEVEL).toBe('${{ inputs.compression_level }}')
expect(workflow.on.workflow_dispatch!.inputs.build_version!.required).toBe(false)
expect(workflow.on.workflow_dispatch!.inputs.version_mode!.default).toBe('default')
expect(workflow.on.workflow_dispatch!.inputs.source_ref!.default).toBe('master')
@@ -101,9 +107,182 @@ it('retains failure diagnostics and uploads only the selected successful install
const uploads = job.steps.filter(step => step.uses?.startsWith('actions/upload-artifact@'))
expect(uploads).toHaveLength(3)
expect(uploads[0]!.if).toBe("always() && steps.checkout.outcome == 'success'")
expect(uploads[0]!.with).toMatchObject({ path: 'apps/desktop/.desktop-build/packaging-runs/', 'include-hidden-files': true })
expect(uploads[0]!.with).toMatchObject({
path: '${{ steps.build_workspace.outputs.directory || github.workspace }}/apps/desktop/.desktop-build/packaging-runs/',
'include-hidden-files': true,
})
for (const mode of ['unsigned', 'signed']) {
const upload = uploads.find(step => step.if === `success() && inputs.signing == '${mode}'`)!
expect(upload.with).toMatchObject({ path: '${{ steps.retained.outputs.directory }}/*', 'if-no-files-found': 'error' })
expect(upload.with).toMatchObject({ path: '${{ steps.retained.outputs.directory }}/*', 'if-no-files-found': 'error', 'compression-level': 0 })
}
})
it('routes all packaging consumers to the selected workspace and removes it only after successful uploads', () => {
expect(workflow.on.workflow_dispatch!.inputs.build_root).toMatchObject({ type: 'string', default: '', required: false })
expect(job.env.PACKAGE_BUILD_ROOT).toBe('${{ inputs.build_root }}')
const selected = job.steps.find(step => step.id === 'build_workspace')!
expect(selected.name).toBe('Select packaging workspace')
expect(selected.env).toEqual({ PACKAGE_SOURCE_SHA: '${{ steps.checkout.outputs.commit }}' })
expect(selected['working-directory']).toBeUndefined()
expect(selected.run).toContain('if (!$env:PACKAGE_BUILD_ROOT)')
expect(selected.run).toContain('"directory=$env:GITHUB_WORKSPACE" >> $env:GITHUB_OUTPUT')
expect(selected.run).toContain('} else {')
expect(selected.run).toContain('node "$env:PACKAGE_CI_HELPERS/windows-build-workspace.mjs"')
expect(selected.run).toContain('exit $LASTEXITCODE')
const index = job.steps.indexOf(selected)
expect(index).toBeGreaterThan(job.steps.findIndex(step => step.uses?.startsWith('actions/setup-node@')))
expect(index).toBeLessThan(job.steps.findIndex(step => step.name === 'Configure persistent pnpm store'))
const consumers = [
'Install locked dependencies',
'Validate preset and resolve build version',
'Check packaging configuration and toolchain',
'Build installer and verify packaged runtime',
'Retain complete package outside the checkout',
]
for (const name of consumers) {
const step = job.steps.find(entry => entry.name === name)!
expect(step['working-directory']).toBe('${{ steps.build_workspace.outputs.directory }}')
expect(job.steps.indexOf(step)).toBeGreaterThan(index)
}
expect(job.steps.filter(step => step['working-directory'] === '${{ steps.build_workspace.outputs.directory }}'))
.toHaveLength(consumers.length)
const store = job.steps.find(step => step.name === 'Configure persistent pnpm store')!
expect(store.run).toContain('[IO.Path]::GetPathRoot($env:GITHUB_WORKSPACE)')
expect(store['working-directory']).toBeUndefined()
const cleanup = job.steps.at(-1)!
expect(cleanup).toMatchObject({
name: 'Remove isolated packaging workspace',
if: "success() && inputs.build_root != ''",
env: { PACKAGE_SOURCE_SHA: '${{ steps.checkout.outputs.commit }}' },
run: 'node "$env:PACKAGE_CI_HELPERS/windows-build-workspace.mjs" --remove',
})
expect(cleanup['working-directory']).toBeUndefined()
for (const upload of job.steps.filter(step => step.uses?.startsWith('actions/upload-artifact@'))) {
expect(job.steps.indexOf(upload)).toBeLessThan(job.steps.indexOf(cleanup))
}
})
it('normalizes priority in the dependency and packaging shells before launching their children', () => {
const command = '& "$env:PACKAGE_CI_HELPERS/windows-packaging-priority.ps1" -BestEffort'
expect(job.steps.find(entry => entry.name === 'Validate selected source and adapter support')!.run)
.not.toContain('windows-packaging-priority.ps1')
for (const name of ['Install locked dependencies', 'Build installer and verify packaged runtime']) {
const step = job.steps.find(entry => entry.name === name)!
expect(step.run!.trimStart().startsWith(command + '\n')).toBe(true)
}
})
it('scopes the optional temporary directory to installer building without changing Actions or later steps', () => {
const build = job.steps.find(step => step.name === 'Build installer and verify packaged runtime')!
expect(build.env?.PACKAGE_BUILD_TEMP).toBe('${{ steps.build_workspace.outputs.temporaryDirectory }}')
expect(build.run).toContain([
'if ($env:PACKAGE_BUILD_TEMP) {',
' $env:TEMP = $env:PACKAGE_BUILD_TEMP',
' $env:TMP = $env:PACKAGE_BUILD_TEMP',
'}',
].join('\n'))
expect(build.run!.indexOf('if ($env:PACKAGE_BUILD_TEMP)')).toBeLessThan(build.run!.indexOf('$packageArgs ='))
expect(build.run).not.toContain('GITHUB_ENV')
expect(build.run).not.toContain('RUNNER_TEMP')
expect(job.env).not.toHaveProperty('TEMP')
expect(job.env).not.toHaveProperty('TMP')
expect(job.env).not.toHaveProperty('PACKAGE_BUILD_TEMP')
for (const step of job.steps.filter(step => step !== build)) {
expect(step.env ?? {}).not.toHaveProperty('PACKAGE_BUILD_TEMP')
expect(step.env ?? {}).not.toHaveProperty('TEMP')
expect(step.env ?? {}).not.toHaveProperty('TMP')
expect(step.run ?? '').not.toMatch(/\$env:(?:TEMP|TMP)\s*=/u)
}
const selected = job.steps.find(step => step.id === 'build_workspace')!
const defaultBranch = selected.run!.split('} else {')[0]!
expect(defaultBranch).toContain('"directory=$env:GITHUB_WORKSPACE" >> $env:GITHUB_OUTPUT')
expect(defaultBranch).not.toContain('temporaryDirectory')
})
it.skipIf(process.platform !== 'win32')('keeps the dependency store stable across run-specific pnpm homes', () => {
const directory = mkdtempSync(resolve(tmpdir(), 'windows-package-store-'))
try {
const workspace = resolve(directory, 'runner', 'repo')
const packageCache = resolve(directory, 'tool-cache', 'desktop')
const store = resolve(parse(directory).root, '.pnpm-store')
const step = job.steps.find(entry => entry.name === 'Configure persistent pnpm store')!
expect(job.steps.indexOf(step)).toBeLessThan(job.steps.findIndex(entry => entry.name === 'Install locked dependencies'))
const script = resolve(directory, 'store.ps1')
writeFileSync(script, "$ErrorActionPreference = 'Stop'\n" + step.run!)
const outputs = ['first', 'second'].map((run) => {
const environmentFile = resolve(directory, run + '.env')
execFileSync('pwsh', ['-NoProfile', '-File', script], {
env: { ...process.env, GITHUB_WORKSPACE: workspace,
PACKAGE_CACHE_NAMESPACE: '', DSH_DESKTOP_PACKAGE_CACHE_DIR: packageCache,
PNPM_HOME: resolve(directory, run, 'pnpm'), GITHUB_ENV: environmentFile },
stdio: 'pipe',
})
return readFileSync(environmentFile, 'utf8').trim()
})
expect(outputs.map(output => output.split(/\r?\n/u))).toEqual(Array.from({ length: 2 }, () => [
`DSH_DESKTOP_PACKAGE_CACHE_DIR=${packageCache}`, `PNPM_CONFIG_STORE_DIR=${store}`,
]))
const runNamespace = (namespace: string) => {
const output = resolve(directory, 'namespace.env')
rmSync(output, { force: true })
execFileSync('pwsh', ['-NoProfile', '-File', script], {
env: { ...process.env, GITHUB_WORKSPACE: workspace,
PACKAGE_CACHE_NAMESPACE: namespace, DSH_DESKTOP_PACKAGE_CACHE_DIR: packageCache, GITHUB_ENV: output },
stdio: 'pipe',
})
return readFileSync(output, 'utf8').trim().split(/\r?\n/u)
}
expect(runNamespace('perf-1')).toEqual([
`DSH_DESKTOP_PACKAGE_CACHE_DIR=${resolve(packageCache, 'perf-1')}`, `PNPM_CONFIG_STORE_DIR=${resolve(store, 'perf-1')}`,
])
for (const invalid of ['../escape', 'Upper', 'has space', 'con', 'perf\n', 'a'.repeat(65)]) {
expect(() => runNamespace(invalid)).toThrow()
}
} finally {
rmSync(directory, { recursive: true, force: true, maxRetries: 3 })
}
})
it('pins environment helpers to the workflow commit before selecting product source', () => {
const helper = job.steps.find(step => step.name === 'Checkout workflow helpers')!
const stage = job.steps.find(step => step.name === 'Stage workflow helpers')!
const source = job.steps.find(step => step.id === 'checkout')!
expect(helper.with).toMatchObject({ ref: '${{ github.workflow_sha }}', 'persist-credentials': false })
expect(job.steps.indexOf(helper)).toBeLessThan(job.steps.indexOf(stage))
expect(job.steps.indexOf(stage)).toBeLessThan(job.steps.indexOf(source))
expect(stage.run).toContain('RUNNER_TEMP')
expect(stage.run).toContain('windows-build-workspace.mjs')
expect(stage.run).toContain('windows-packaging-priority.ps1')
for (const step of job.steps.filter(step => step.name !== 'Stage workflow helpers')) {
expect(step.run ?? '').not.toContain('./apps/desktop/scripts/windows-packaging-priority.ps1')
expect(step.run ?? '').not.toContain('node apps/desktop/scripts/windows-build-workspace.mjs')
}
})
it.skipIf(process.platform !== 'win32')('stages helpers that remain usable when product source lacks them', () => {
const directory = mkdtempSync(resolve(tmpdir(), 'workflow-helpers-'))
try {
const source = resolve(directory, 'source')
const scripts = resolve(source, 'apps/desktop/scripts')
const temporary = resolve(directory, 'temporary')
mkdirSync(scripts, { recursive: true })
mkdirSync(temporary)
for (const name of ['windows-packaging-priority.ps1', 'windows-build-workspace.mjs']) {
writeFileSync(resolve(scripts, name), readFileSync(resolve(root, 'apps/desktop/scripts', name)))
}
const stage = resolve(directory, 'stage.ps1')
writeFileSync(stage, job.steps.find(step => step.name === 'Stage workflow helpers')!.run!)
const env = { ...process.env, RUNNER_TEMP: temporary, GITHUB_RUN_ID: '123', GITHUB_RUN_ATTEMPT: '1', GITHUB_ENV: resolve(directory, 'env') }
execFileSync('pwsh', ['-NoProfile', '-File', stage], { cwd: source, env, stdio: 'pipe' })
rmSync(scripts, { recursive: true })
const helper = resolve(temporary, 'packaging-helpers-123-1/windows-packaging-priority.ps1')
expect(execFileSync('pwsh', ['-NoProfile', '-File', helper, '-InspectOnly'], { cwd: source, stdio: 'pipe' }).toString())
.toContain('packaging-process-priority')
expect(existsSync(resolve(temporary, 'packaging-helpers-123-1/windows-build-workspace.mjs'))).toBe(true)
expect(() => execFileSync('pwsh', ['-NoProfile', '-File', stage], {
cwd: source, env: { ...env, GITHUB_RUN_ATTEMPT: '2' }, stdio: 'pipe',
})).toThrow()
} finally {
rmSync(directory, { recursive: true, force: true, maxRetries: 3 })
}
})