GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Deploy website to GitHub Pages / Deploy static website (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Desktop 0.12.7 omitted OpenCode's offline node_modules because the resource copier excludes node_modules at its source root. Copy from the parent and verify every prepared configuration file in the final package. Bundle DSH session projection with the unpacked Host instead of leaving an ASAR-only external import, and reject external DeepSeek imports in shipped chunks. Run packaged OpenCode plugin import and the real DSH UtilityProcess handshake in Windows x64 CI before removing unpacked output. Add regressions using the actual electron-builder copier and Vite externalization configuration. Prepare approved Desktop 0.12.8 notes and change macOS defaults and CI to DMG only. Update aggregation, website downloads and update readers for 12 installers and 20 assets. Current readers retain historical DMG/ZIP support; the user accepts that older macOS GitHub readers and all older mirror readers need manual upgrades. Agent remains 0.11.21, with no new Agent or LoongArch tag. Validation: 3608 tests passed, 0 failed, 61 skipped across 339 files; typecheck, lint, release-note validation, website static checks and 18 website tests passed. No local production build or packaged-app launch was performed at the user's request; actual packaged startup validation runs in CI. Release note: 修复安装后 OpenCode 离线依赖缺失和 DeepSeek Harness Host 启动退出;macOS 改为仅提供 DMG,受影响旧版用户需从下载页手动升级。
266 lines
7.4 KiB
TypeScript
266 lines
7.4 KiB
TypeScript
import {
|
|
createHash
|
|
} from 'node:crypto'
|
|
import {
|
|
mkdtempSync,
|
|
mkdirSync,
|
|
readFileSync,
|
|
readdirSync,
|
|
rmSync,
|
|
writeFileSync
|
|
} from 'node:fs'
|
|
import { createRequire } from 'node:module'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { describe, expect, it } from 'vitest'
|
|
|
|
interface TargetDefinition {
|
|
platform: 'windows' | 'macos' | 'linux'
|
|
arch: 'x64' | 'arm64'
|
|
formats: string[]
|
|
}
|
|
|
|
interface AggregateModule {
|
|
aggregateRelease: (
|
|
inputDirectory: string,
|
|
outputDirectory: string
|
|
) => Promise<{
|
|
version: string
|
|
targets: Array<{
|
|
platform: string
|
|
arch: string
|
|
manifest: string
|
|
}>
|
|
}>
|
|
assertSafeName: (name: string, description: string) => void
|
|
targetDefinitions: TargetDefinition[]
|
|
}
|
|
|
|
const require = createRequire(import.meta.url)
|
|
const aggregate = require(
|
|
'../build/aggregate-release.cjs'
|
|
) as AggregateModule
|
|
const packageVersion = (
|
|
require('../package.json') as { version: string }
|
|
).version
|
|
|
|
function sha256(value: string): string {
|
|
return createHash('sha256').update(value).digest('hex')
|
|
}
|
|
|
|
function artifactName(
|
|
target: TargetDefinition,
|
|
format: string
|
|
): string {
|
|
const base =
|
|
`GoodBuddy-${packageVersion}-${target.platform}-${target.arch}`
|
|
if (format === 'nsis') {
|
|
return `${base}-setup.exe`
|
|
}
|
|
if (format === 'portable') {
|
|
return `${base}-portable.zip`
|
|
}
|
|
const extension = format === 'zip' ? 'zip' : format
|
|
return `${base}.${extension}`
|
|
}
|
|
|
|
function createDownloadedArtifacts(parent: string): string {
|
|
const input = join(parent, 'downloads')
|
|
mkdirSync(input)
|
|
for (const target of aggregate.targetDefinitions) {
|
|
const key = `${target.platform}-${target.arch}`
|
|
const directory = join(input, `goodbuddy-${key}`)
|
|
mkdirSync(directory)
|
|
const files = target.formats.map((format) => {
|
|
const name = artifactName(target, format)
|
|
const content = `${key}:${format}`
|
|
writeFileSync(join(directory, name), content)
|
|
return {
|
|
name,
|
|
size: Buffer.byteLength(content),
|
|
sha256: sha256(content)
|
|
}
|
|
})
|
|
const debugContent = `${key}:debug`
|
|
writeFileSync(
|
|
join(directory, 'builder-debug.yml'),
|
|
debugContent
|
|
)
|
|
files.push({
|
|
name: 'builder-debug.yml',
|
|
size: Buffer.byteLength(debugContent),
|
|
sha256: sha256(debugContent)
|
|
})
|
|
const metadataName = `metadata-${key}.json`
|
|
const metadataContent = `${key}:metadata`
|
|
writeFileSync(
|
|
join(directory, metadataName),
|
|
metadataContent
|
|
)
|
|
files.push({
|
|
name: metadataName,
|
|
size: Buffer.byteLength(metadataContent),
|
|
sha256: sha256(metadataContent)
|
|
})
|
|
if (target.platform === 'windows') {
|
|
const setupName = artifactName(target, 'nsis')
|
|
const blockmapName = `${setupName}.blockmap`
|
|
const blockmapContent = `${key}:blockmap`
|
|
writeFileSync(
|
|
join(directory, blockmapName),
|
|
blockmapContent
|
|
)
|
|
files.push({
|
|
name: blockmapName,
|
|
size: Buffer.byteLength(blockmapContent),
|
|
sha256: sha256(blockmapContent)
|
|
})
|
|
}
|
|
writeFileSync(
|
|
join(directory, 'release-manifest.json'),
|
|
`${JSON.stringify({
|
|
formatVersion: 1,
|
|
productName: 'GoodBuddy',
|
|
version: packageVersion,
|
|
platform: target.platform,
|
|
arch: target.arch,
|
|
formats: target.formats,
|
|
files
|
|
}, null, 2)}\n`
|
|
)
|
|
}
|
|
return input
|
|
}
|
|
|
|
describe('release asset aggregation', () => {
|
|
it('strictly verifies six targets and writes isolated upload assets', async () => {
|
|
const parent = mkdtempSync(
|
|
join(tmpdir(), 'goodbuddy-release-aggregate-')
|
|
)
|
|
try {
|
|
const input = createDownloadedArtifacts(parent)
|
|
const output = join(parent, 'upload')
|
|
const manifest = await aggregate.aggregateRelease(input, output)
|
|
|
|
expect(manifest.version).toBe(packageVersion)
|
|
expect(manifest.targets).toHaveLength(6)
|
|
expect(
|
|
manifest.targets.map((target) => target.manifest)
|
|
).toEqual([
|
|
'release-manifest-windows-x64.json',
|
|
'release-manifest-windows-arm64.json',
|
|
'release-manifest-macos-x64.json',
|
|
'release-manifest-macos-arm64.json',
|
|
'release-manifest-linux-x64.json',
|
|
'release-manifest-linux-arm64.json'
|
|
])
|
|
|
|
const outputNames = readdirSync(output)
|
|
expect(outputNames).toHaveLength(20)
|
|
expect(outputNames).toContain('release-manifest.json')
|
|
expect(outputNames).toContain('SHA256SUMS')
|
|
expect(outputNames).not.toContain('builder-debug.yml')
|
|
expect(
|
|
outputNames.some((name) => name.startsWith('metadata-'))
|
|
).toBe(false)
|
|
expect(
|
|
outputNames.some((name) => name.endsWith('.blockmap'))
|
|
).toBe(false)
|
|
const windowsManifest = JSON.parse(
|
|
readFileSync(
|
|
join(output, 'release-manifest-windows-x64.json'),
|
|
'utf8'
|
|
)
|
|
) as { files: Array<{ name: string }> }
|
|
expect(windowsManifest.files.map((file) => file.name)).toEqual([
|
|
artifactName(aggregate.targetDefinitions[0]!, 'nsis'),
|
|
artifactName(aggregate.targetDefinitions[0]!, 'portable')
|
|
])
|
|
const sums = readFileSync(
|
|
join(output, 'SHA256SUMS'),
|
|
'utf8'
|
|
)
|
|
expect(sums.trim().split('\n')).toHaveLength(19)
|
|
expect(sums).toContain(
|
|
'release-manifest-windows-x64.json'
|
|
)
|
|
expect(sums).not.toMatch(/\sSHA256SUMS(?:\r?\n|$)/u)
|
|
} finally {
|
|
rmSync(parent, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
it('rejects package hash mismatches', async () => {
|
|
const parent = mkdtempSync(
|
|
join(tmpdir(), 'goodbuddy-release-hash-')
|
|
)
|
|
try {
|
|
const input = createDownloadedArtifacts(parent)
|
|
const file = join(
|
|
input,
|
|
'goodbuddy-windows-x64',
|
|
artifactName(aggregate.targetDefinitions[0]!, 'nsis')
|
|
)
|
|
writeFileSync(file, 'tampered')
|
|
|
|
await expect(
|
|
aggregate.aggregateRelease(input, join(parent, 'upload'))
|
|
).rejects.toThrow('完整性校验失败')
|
|
} finally {
|
|
rmSync(parent, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
it('verifies auxiliary files even though they are not published', async () => {
|
|
const parent = mkdtempSync(
|
|
join(tmpdir(), 'goodbuddy-release-auxiliary-')
|
|
)
|
|
try {
|
|
const input = createDownloadedArtifacts(parent)
|
|
writeFileSync(
|
|
join(
|
|
input,
|
|
'goodbuddy-windows-x64',
|
|
'builder-debug.yml'
|
|
),
|
|
'tampered'
|
|
)
|
|
|
|
await expect(
|
|
aggregate.aggregateRelease(input, join(parent, 'upload'))
|
|
).rejects.toThrow('完整性校验失败')
|
|
} finally {
|
|
rmSync(parent, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
it.each([
|
|
'../escape.exe',
|
|
'..\\escape.exe',
|
|
'/tmp/escape.exe',
|
|
'nested/file.exe'
|
|
])('rejects path traversal in file name %s', (name) => {
|
|
expect(() =>
|
|
aggregate.assertSafeName(name, '测试文件名')
|
|
).toThrow('不安全路径')
|
|
})
|
|
|
|
it('rejects undeclared files', async () => {
|
|
const parent = mkdtempSync(
|
|
join(tmpdir(), 'goodbuddy-release-extra-')
|
|
)
|
|
try {
|
|
const input = createDownloadedArtifacts(parent)
|
|
writeFileSync(
|
|
join(input, 'goodbuddy-linux-x64', 'unknown.rpm'),
|
|
'unknown'
|
|
)
|
|
await expect(
|
|
aggregate.aggregateRelease(input, join(parent, 'upload'))
|
|
).rejects.toThrow('未声明的文件')
|
|
} finally {
|
|
rmSync(parent, { recursive: true, force: true })
|
|
}
|
|
})
|
|
})
|