fix: finalize Electron migration boundaries

Complete the Electron replacement boundary before merging by removing the renderer-side Tauri host fallback, tightening H5/browser access so only desktop navigation is tokenless, and moving desktop release publication to a tag-driven GitHub Actions matrix with a single final publish job.

Constraint: H5/browser capability access must not gain tokenless access through localhost or retired Tauri origins

Constraint: Desktop release artifacts must be built by GitHub Actions from version tags, not treated as local build outputs

Rejected: Keep localhost browser origins trusted for convenience | local browser contexts can access loopback services and must use the H5 token path

Rejected: Publish from each matrix job | partial releases can be created before all platforms finish

Confidence: high

Scope-risk: broad

Directive: Do not reintroduce Tauri origins or localhost browser origins into the trusted desktop origin set without a reviewed security design

Tested: bun test src/server/__tests__/h5-access-policy.test.ts src/server/__tests__/h5-access-auth.test.ts src/server/__tests__/diagnostics-service.test.ts src/server/middleware/cors.test.ts

Tested: bun test scripts/pr/release-workflow.test.ts scripts/release-update-metadata.test.ts

Tested: bun run check:desktop

Tested: bun run check:native

Tested: git diff --check

Not-tested: bun run check:server is blocked by expired quarantine entries server:cron-scheduler, server:providers-real, server:tasks, server:e2e:business-flow, server:e2e:full-flow
This commit is contained in:
程序员阿江(Relakkes)
2026-06-02 22:42:53 +08:00
parent 16f4137954
commit 81845fbc49
36 changed files with 859 additions and 606 deletions
+141 -17
View File
@@ -2,8 +2,18 @@ import { describe, expect, test } from 'bun:test'
import { readFileSync } from 'node:fs'
describe('release desktop workflow', () => {
function readReleaseWorkflow() {
return readFileSync('.github/workflows/release-desktop.yml', 'utf8')
}
function extractJob(workflow: string, jobName: string) {
return workflow.match(
new RegExp(`${jobName}:[\\s\\S]*?(?:\\n {2}[a-zA-Z0-9_-]+:|$)`),
)?.[0]
}
test('build job waits for a PR-quality preflight before packaging', () => {
const workflow = readFileSync('.github/workflows/release-desktop.yml', 'utf8')
const workflow = readReleaseWorkflow()
expect(workflow).toContain('quality-preflight:')
expect(workflow).toContain('run: bun run verify')
@@ -38,24 +48,22 @@ describe('release desktop workflow', () => {
})
test('release workflow requires macOS Gatekeeper launch approval before upload', () => {
const workflow = readFileSync('.github/workflows/release-desktop.yml', 'utf8')
const workflow = readReleaseWorkflow()
const gatekeeperStep = workflow.match(
/- name: Verify macOS launch policy[\s\S]*?(?:\n\s{6}- name:|$)/,
)?.[0]
expect(gatekeeperStep).toContain("if: matrix.smoke_platform == 'macos'")
expect(gatekeeperStep).toContain('bun run test:package-smoke --platform macos --package-kind release --artifacts-dir desktop/build-artifacts/electron --require-macos-gatekeeper')
expect(workflow.indexOf('Verify macOS launch policy')).toBeLessThan(workflow.indexOf('Upload artifacts'))
expect(workflow.indexOf('Verify macOS launch policy')).toBeLessThan(workflow.indexOf('Upload release artifacts for final publish'))
})
test('release workflow fails globally before matrix fan-out when signing or notarization secrets are missing', () => {
const workflow = readFileSync('.github/workflows/release-desktop.yml', 'utf8')
const workflow = readReleaseWorkflow()
const signingJob = workflow.match(
/signing-preflight:[\s\S]*?(?:\n {2}[a-zA-Z0-9_-]+:|$)/,
)?.[0]
const buildJob = workflow.match(
/build:[\s\S]*?(?:\n {2}[a-zA-Z0-9_-]+:|$)/,
)?.[0]
const buildJob = extractJob(workflow, 'build')
expect(signingJob).toContain('Validate release signing and notarization secrets')
for (const secret of [
@@ -73,33 +81,149 @@ describe('release desktop workflow', () => {
expect(buildJob).toContain('- quality-preflight')
expect(buildJob).toContain('- signing-preflight')
expect(workflow.indexOf('signing-preflight:')).toBeLessThan(workflow.indexOf('build:'))
expect(workflow.indexOf('signing-preflight:')).toBeLessThan(workflow.indexOf('Upload artifacts'))
expect(workflow.indexOf('signing-preflight:')).toBeLessThan(workflow.indexOf('Upload release artifacts for final publish'))
})
test('release workflow avoids same-name updater metadata uploads from matrix builds', () => {
const workflow = readFileSync('.github/workflows/release-desktop.yml', 'utf8')
const workflow = readReleaseWorkflow()
const namespaceStep = workflow.match(
/- name: Namespace update metadata assets[\s\S]*?(?:\n\s{6}- name:|$)/,
)?.[0]
expect(namespaceStep).toContain('for file in latest*.yml')
expect(namespaceStep).toContain('"${file%.yml}-${{ matrix.label }}.yml"')
expect(workflow.indexOf('Namespace update metadata assets')).toBeLessThan(workflow.indexOf('Upload artifacts'))
expect(workflow.indexOf('Namespace update metadata assets')).toBeLessThan(workflow.indexOf('Upload release artifacts for final publish'))
})
test('release workflow republishes standard updater metadata after all matrix builds pass', () => {
const workflow = readFileSync('.github/workflows/release-desktop.yml', 'utf8')
const publishJob = workflow.match(
/publish-update-metadata:[\s\S]*?(?:\n {2}[a-zA-Z0-9_-]+:|$)/,
)?.[0]
test('release workflow uploads only Actions artifacts from matrix builds', () => {
const workflow = readReleaseWorkflow()
const buildJob = extractJob(workflow, 'build')
expect(buildJob).toContain('Validate matrix release asset set')
for (const label of ['macOS-ARM64', 'macOS-x64', 'Linux-x64', 'Linux-ARM64', 'Windows-x64']) {
expect(buildJob).toContain(`${label})`)
}
expect(buildJob).toContain('Upload release artifacts for final publish')
expect(buildJob).toContain('actions/upload-artifact@v4')
expect(buildJob).toContain('name: desktop-release-artifacts-${{ matrix.label }}')
expect(buildJob).not.toContain('softprops/action-gh-release@v2')
expect(buildJob).not.toContain('Load release notes')
})
test('release workflow publishes all release assets only after all matrix builds pass', () => {
const workflow = readReleaseWorkflow()
const publishJob = extractJob(workflow, 'publish-release')
expect(workflow).toContain('name: desktop-update-metadata-${{ matrix.label }}')
expect(workflow).toContain('name: desktop-release-artifacts-${{ matrix.label }}')
expect(publishJob).toContain('needs: build')
expect(publishJob).toContain('actions/download-artifact@v4')
expect(publishJob).toContain('pattern: desktop-release-artifacts-*')
expect(publishJob).toContain('pattern: desktop-update-metadata-*')
expect(publishJob).toContain('Validate complete release asset set')
expect(publishJob).toContain('bun run scripts/release-update-metadata.ts --metadata-dir artifacts/update-metadata --out-dir artifacts/update-metadata-standard')
expect(publishJob).toContain('files: artifacts/update-metadata-standard/*.yml')
expect(workflow.indexOf('publish-update-metadata:')).toBeGreaterThan(workflow.indexOf('build:'))
expect(publishJob).toContain('Validate standard update metadata set')
expect(publishJob).toContain('softprops/action-gh-release@v2')
expect(publishJob).toContain('artifacts/release-assets/**/*.dmg')
expect(publishJob).toContain('artifacts/release-assets/**/*.exe')
expect(publishJob).toContain('artifacts/release-assets/**/*.AppImage')
expect(publishJob).toContain('artifacts/update-metadata-standard/*.yml')
expect(publishJob).toContain('fail_on_unmatched_files: true')
expect(publishJob).toContain('Load release notes')
expect(workflow.indexOf('publish-release:')).toBeGreaterThan(workflow.indexOf('build:'))
})
test('release matrix asset basenames remain unique when final artifacts are flattened', () => {
const desktopPackage = JSON.parse(readFileSync('desktop/package.json', 'utf8')) as {
version: string
build: {
artifactName: string
}
}
const version = desktopPackage.version
expect(desktopPackage.build.artifactName).toBe('Claude-Code-Haha-${version}-${os}-${arch}.${ext}')
const expectedReleaseAssets = [
`Claude-Code-Haha-${version}-mac-arm64.dmg`,
`Claude-Code-Haha-${version}-mac-arm64.dmg.blockmap`,
`Claude-Code-Haha-${version}-mac-arm64.zip`,
`Claude-Code-Haha-${version}-mac-arm64.zip.blockmap`,
`Claude-Code-Haha-${version}-mac-x64.dmg`,
`Claude-Code-Haha-${version}-mac-x64.dmg.blockmap`,
`Claude-Code-Haha-${version}-mac-x64.zip`,
`Claude-Code-Haha-${version}-mac-x64.zip.blockmap`,
`Claude-Code-Haha-${version}-linux-x64.AppImage`,
`Claude-Code-Haha-${version}-linux-x64.AppImage.blockmap`,
`Claude-Code-Haha-${version}-linux-x64.deb`,
`Claude-Code-Haha-${version}-linux-arm64.AppImage`,
`Claude-Code-Haha-${version}-linux-arm64.AppImage.blockmap`,
`Claude-Code-Haha-${version}-linux-arm64.deb`,
`Claude-Code-Haha-${version}-win-x64.exe`,
`Claude-Code-Haha-${version}-win-x64.exe.blockmap`,
]
const namespacedMetadata = [
'latest-mac-macOS-ARM64.yml',
'latest-mac-macOS-x64.yml',
'latest-linux-Linux-x64.yml',
'latest-linux-Linux-ARM64.yml',
'latest-Windows-x64.yml',
]
const standardMetadata = [
'latest-mac.yml',
'latest-linux.yml',
'latest-linux-arm64.yml',
'latest.yml',
]
const flattenedNames = [
...expectedReleaseAssets,
...namespacedMetadata,
...standardMetadata,
]
expect(new Set(flattenedNames).size).toBe(flattenedNames.length)
expect(expectedReleaseAssets.filter((name) => name.endsWith('.dmg')).length).toBe(2)
expect(expectedReleaseAssets.filter((name) => name.endsWith('.zip')).length).toBe(2)
expect(expectedReleaseAssets.filter((name) => name.endsWith('.AppImage')).length).toBe(2)
expect(expectedReleaseAssets.filter((name) => name.endsWith('.deb')).length).toBe(2)
expect(expectedReleaseAssets.filter((name) => name.endsWith('.exe')).length).toBe(1)
for (const platform of ['mac', 'linux', 'win']) {
expect(expectedReleaseAssets.some((name) => name.includes(`-${platform}-`))).toBe(true)
}
expect(standardMetadata).toEqual([
'latest-mac.yml',
'latest-linux.yml',
'latest-linux-arm64.yml',
'latest.yml',
])
})
test('release workflow validates exact expected release assets and update metadata before publishing', () => {
const workflow = readReleaseWorkflow()
const buildJob = extractJob(workflow, 'build')
const publishJob = extractJob(workflow, 'publish-release')
const expectedFiles = [
'Claude-Code-Haha-${APP_VERSION}-mac-arm64.dmg',
'Claude-Code-Haha-${APP_VERSION}-mac-arm64.zip',
'Claude-Code-Haha-${APP_VERSION}-mac-x64.dmg',
'Claude-Code-Haha-${APP_VERSION}-mac-x64.zip',
'Claude-Code-Haha-${APP_VERSION}-linux-x64.AppImage',
'Claude-Code-Haha-${APP_VERSION}-linux-x64.deb',
'Claude-Code-Haha-${APP_VERSION}-linux-arm64.AppImage',
'Claude-Code-Haha-${APP_VERSION}-linux-arm64.deb',
'Claude-Code-Haha-${APP_VERSION}-win-x64.exe',
]
for (const file of expectedFiles) {
expect(buildJob).toContain(file)
expect(publishJob).toContain(file)
}
for (const metadata of ['latest-mac.yml', 'latest-linux.yml', 'latest-linux-arm64.yml', 'latest.yml']) {
expect(publishJob).toContain(`artifacts/update-metadata-standard/$file`)
expect(publishJob).toContain(metadata)
}
expect(buildJob).toContain('Missing release assets for %s')
expect(publishJob).toContain('Missing complete release assets')
expect(publishJob).toContain('Missing standard update metadata')
})
test('Electron Builder publish config does not rely on git remote autodetection', () => {