mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-10-26 16:55:08 +00:00
fix(desktop): preflight local publication config before install
This commit is contained in:
1 parent
cf0a621494
commit
012a25f8df
7 files changed
+104
-4
No files matched your search
@@ -74,6 +74,9 @@ jobs:
|
||||
- uses: actions/setup-node@v7.0.0
|
||||
with:
|
||||
node-version: '24'
|
||||
- name: Check local publication configuration before dependency installation
|
||||
working-directory: desktop-publisher
|
||||
run: node apps/desktop/scripts/preflight-desktop-publish-local.mjs
|
||||
- run: pnpm install --frozen-lockfile
|
||||
working-directory: desktop-publisher
|
||||
- name: Resolve retained package and approval environment
|
||||
@@ -115,6 +118,9 @@ jobs:
|
||||
- uses: actions/setup-node@v7.0.0
|
||||
with:
|
||||
node-version: '24'
|
||||
- name: Check local publication configuration before dependency installation
|
||||
working-directory: desktop-publisher
|
||||
run: node apps/desktop/scripts/preflight-desktop-publish-local.mjs
|
||||
- run: pnpm install --frozen-lockfile
|
||||
working-directory: desktop-publisher
|
||||
- name: Verify local package without upload credentials
|
||||
|
||||
@@ -24,8 +24,8 @@
|
||||
en: f80388c2b1ffef4c
|
||||
zh: f242ab2316bf38a1
|
||||
/desktop-ci-publication/package-and-publish:
|
||||
en: 0a26537845e89dda
|
||||
zh: 307378bb65e24650
|
||||
en: f7c12d343891e547
|
||||
zh: da596ee1366c2046
|
||||
/desktop-ci-publication/recovery-and-records:
|
||||
en: 2e1e6b16d0d6bd41
|
||||
zh: 6ff4ad5a8f1f57b4
|
||||
|
||||
@@ -72,7 +72,7 @@ Select the platform's `Package Desktop` workflow, preset, source, channel and ve
|
||||
|
||||
Packaging only retains files. After success, open **Publish Desktop · COS → Run workflow** on `master`, enter `package_run_id` from the packaging run URL, and optionally enter `package_run_attempt`. Blank selects the latest attempt, which must have succeeded; it never falls back to an older success. An explicit attempt selects that exact successful build. Windows must be signed; macOS must finish notarization and stapling. GitHub job evidence selects the original runner; its local manifest supplies the preset, version, source and channel before approval. No installer or request artifact is downloaded. Retained packages with the old `publishRequested=false` flag remain eligible; missing or changed local files stop publication.
|
||||
|
||||
Before uploading, the publisher hashes all retained files, checks version and destination metadata, and rechecks the platform signatures. macOS verifies the DMG and extracts the ZIP to verify the app's ticket; Windows checks Authenticode, the configured signer thumbprint and the timestamp. Preparation writes a Chinese approval summary with separate version, channel, platform and deployment fields, the retained test release ID, full installer/feed URLs, a link to the original packaging attempt, source ref/SHA, platform verification results and proposed tag. Folded details include the COS bucket and file sizes/SHA-256 of the upload bytes, including rewritten YAML. Public-repository summaries expose the test prefix and download URLs; the prefix is not authentication. Both publishing jobs wait for **Review deployments → Approve and deploy**, then recheck the approved manifest and upload plan digest. Changed bytes or destinations reject. Approval cannot edit a version; reject and rebuild instead. Both deployments upload missing binaries, verify remote bytes, then update YAML. There is no files-only mode in this workflow. Production finally creates `desktop-v<version>` at the recorded packaging commit; an existing tag must name the same commit.
|
||||
Before installing dependencies on the original runner, preparation and the approved publish job check that the selected `publishing/<preset>.json` is readable JSON. Missing configuration reports its path; restore it and retry publication with the same package. Full configuration and signature validation still run afterward. Before uploading, the publisher hashes all retained files, checks version and destination metadata, and rechecks the platform signatures. macOS verifies the DMG and extracts the ZIP to verify the app's ticket; Windows checks Authenticode, the configured signer thumbprint and the timestamp. Preparation writes a Chinese approval summary with separate version, channel, platform and deployment fields, the retained test release ID, full installer/feed URLs, a link to the original packaging attempt, source ref/SHA, platform verification results and proposed tag. Folded details include the COS bucket and file sizes/SHA-256 of the upload bytes, including rewritten YAML. Public-repository summaries expose the test prefix and download URLs; the prefix is not authentication. Both publishing jobs wait for **Review deployments → Approve and deploy**, then recheck the approved manifest and upload plan digest. Changed bytes or destinations reject. Approval cannot edit a version; reject and rebuild instead. Both deployments upload missing binaries, verify remote bytes, then update YAML. There is no files-only mode in this workflow. Production finally creates `desktop-v<version>` at the recorded packaging commit; an existing tag must name the same commit.
|
||||
|
||||
<a id="recovery-and-records"></a>
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ Environment 审批限制此工作流,不限制同一机器账户运行的任
|
||||
|
||||
打包只保留文件。成功后,在 `master` 上打开 **Publish Desktop · COS → Run workflow**,填写打包运行 URL 中的 `package_run_id`,并可填写 `package_run_attempt`。留空选择最新尝试,且该尝试必须成功,不会回退到较早的成功尝试;显式填写则选择对应的成功构建。Windows 必须完成签名,macOS 必须完成公证和 stapling。GitHub job 证据确定原 runner,本机清单在审批前提供 preset、版本、源码和通道。不会下载安装包或请求 artifact。旧清单的 `publishRequested=false` 不影响发布资格;本机文件缺失或变化会停止发布。
|
||||
|
||||
上传前,发布器校验全部留存文件的哈希、版本及目标信息,再复核平台签名。macOS 校验 DMG,并解压 ZIP 校验 app 票据;Windows 校验 Authenticode、配置的签名证书指纹及时间戳。准备阶段生成中文审批摘要,分别展示版本、通道、平台和环境,以及留存的 test release ID、安装包与升级 YAML 的完整 URL、原打包尝试链接、源码 ref/SHA、平台校验结果及拟创建的 tag。折叠详情包含 COS 存储桶,以及实际上传字节的大小和 SHA-256(包括改写后的 YAML)。公开仓库的摘要会公开测试前缀和下载 URL;前缀不提供访问鉴权。两种发布 job 均等待 **Review deployments → Approve and deploy**,随后重新核对已批准的 manifest 与上传计划摘要。文件或目标变化会被拒绝。审批不能修改版本;应拒绝并重新打包。两种环境都上传缺失文件、校验远端字节,再更新 YAML。此工作流没有只上传文件的模式。production 最后为记录的打包提交创建 `desktop-v<version>`;已有 tag 必须指向同一提交。
|
||||
在原 runner 安装依赖前,准备阶段和审批后的发布 job 都会检查所选 `publishing/<preset>.json` 是否为可读取的 JSON。配置缺失时会报告路径;补齐后可使用同一个包重试发布。之后仍会执行完整配置与签名校验。上传前,发布器校验全部留存文件的哈希、版本及目标信息,再复核平台签名。macOS 校验 DMG,并解压 ZIP 校验 app 票据;Windows 校验 Authenticode、配置的签名证书指纹及时间戳。准备阶段生成中文审批摘要,分别展示版本、通道、平台和环境,以及留存的 test release ID、安装包与升级 YAML 的完整 URL、原打包尝试链接、源码 ref/SHA、平台校验结果及拟创建的 tag。折叠详情包含 COS 存储桶,以及实际上传字节的大小和 SHA-256(包括改写后的 YAML)。公开仓库的摘要会公开测试前缀和下载 URL;前缀不提供访问鉴权。两种发布 job 均等待 **Review deployments → Approve and deploy**,随后重新核对已批准的 manifest 与上传计划摘要。文件或目标变化会被拒绝。审批不能修改版本;应拒绝并重新打包。两种环境都上传缺失文件、校验远端字节,再更新 YAML。此工作流没有只上传文件的模式。production 最后为记录的打包提交创建 `desktop-v<version>`;已有 tag 必须指向同一提交。
|
||||
|
||||
<a id="recovery-and-records"></a>
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Check the selected machine configuration without authorizing publication or reading secrets.
|
||||
* @param env Selected package or approved route and machine directories.
|
||||
* @returns Resolves when the selected configuration is a readable JSON object; full validation remains in the publisher.
|
||||
*/
|
||||
export function preflightDesktopPublishLocal(env: NodeJS.ProcessEnv): Promise<void>
|
||||
@@ -0,0 +1,43 @@
|
||||
/** Reject missing local publication configuration before dependency installation; never open credential files. */
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { dirname, isAbsolute, join, resolve } from 'node:path'
|
||||
|
||||
async function readJson(path, subject) {
|
||||
try {
|
||||
return JSON.parse(await readFile(path, 'utf8'))
|
||||
} catch {
|
||||
throw new Error(`desktop publish: ${subject} is missing, unreadable or invalid JSON: ${path}. Restore this file on the original runner, then retry publication; repackaging is not required.`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the selected machine configuration without authorizing publication or reading secrets.
|
||||
* @param {NodeJS.ProcessEnv} env Selected package or approved route and machine directories.
|
||||
* @returns {Promise<void>} Resolves when the selected configuration is a readable JSON object; full validation remains in the publisher.
|
||||
*/
|
||||
export async function preflightDesktopPublishLocal(env) {
|
||||
const root = env.DSH_PACKAGING_CONFIG_DIR
|
||||
if (!root || !isAbsolute(root)) throw new Error('desktop publish: set DSH_PACKAGING_CONFIG_DIR to the absolute machine packaging configuration directory')
|
||||
let preset
|
||||
if (env.DESKTOP_PUBLISH_ROUTE) {
|
||||
preset = JSON.parse(env.DESKTOP_PUBLISH_ROUTE).preset
|
||||
} else {
|
||||
const selection = JSON.parse(env.DESKTOP_PACKAGE_SELECTION || '{}')
|
||||
const run = selection?.run
|
||||
if (!Number.isSafeInteger(run?.id) || run.id <= 0 || !Number.isSafeInteger(run?.run_attempt) || run.run_attempt <= 0) {
|
||||
throw new Error('desktop publish: selected package must identify a positive run ID and attempt')
|
||||
}
|
||||
const releases = env.DSH_DESKTOP_RELEASES_DIR || join(dirname(root), 'releases')
|
||||
if (!isAbsolute(releases)) throw new Error('desktop publish: DSH_DESKTOP_RELEASES_DIR must be absolute')
|
||||
const manifest = await readJson(join(releases, String(run.id), String(run.run_attempt), 'manifest.json'), 'retained package manifest')
|
||||
preset = manifest?.source?.preset
|
||||
}
|
||||
if (preset !== 'test' && preset !== 'production') throw new Error('desktop publish: selected package preset must be test or production')
|
||||
const path = join(root, 'publishing', `${preset}.json`)
|
||||
const config = await readJson(path, 'publication configuration')
|
||||
if (!config || typeof config !== 'object' || Array.isArray(config)) throw new Error(`desktop publish: publication configuration must be a JSON object: ${path}`)
|
||||
}
|
||||
|
||||
if (process.argv[1] !== undefined && resolve(process.argv[1]) === import.meta.filename) {
|
||||
await preflightDesktopPublishLocal(process.env)
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { expect, it } from 'vitest'
|
||||
import { preflightDesktopPublishLocal } from '../scripts/preflight-desktop-publish-local.mjs'
|
||||
|
||||
it('checks the retained preset before approval and the approved preset before upload without opening credentials', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'publish-preflight-'))
|
||||
try {
|
||||
const configRoot = join(root, 'packaging')
|
||||
const releases = join(root, 'custom-releases')
|
||||
const manifest = join(releases, '42', '2')
|
||||
await mkdir(manifest, { recursive: true })
|
||||
await writeFile(join(manifest, 'manifest.json'), JSON.stringify({ source: { preset: 'test' } }))
|
||||
const env = { DSH_PACKAGING_CONFIG_DIR: configRoot, DSH_DESKTOP_RELEASES_DIR: releases,
|
||||
DESKTOP_PACKAGE_SELECTION: JSON.stringify({ run: { id: 42, run_attempt: 2 } }) }
|
||||
await expect(preflightDesktopPublishLocal(env)).rejects.toThrow(`publication configuration is missing, unreadable or invalid JSON: ${join(configRoot, 'publishing', 'test.json')}`)
|
||||
await mkdir(join(configRoot, 'publishing'), { recursive: true })
|
||||
await writeFile(join(configRoot, 'publishing', 'test.json'), JSON.stringify({ credentialsFile: join(root, 'absent-secret.json') }))
|
||||
await expect(preflightDesktopPublishLocal(env)).resolves.toBeUndefined()
|
||||
const approved = { ...env, DESKTOP_PUBLISH_ROUTE: JSON.stringify({ preset: 'production' }) }
|
||||
await expect(preflightDesktopPublishLocal(approved)).rejects.toThrow('production.json')
|
||||
await writeFile(join(configRoot, 'publishing', 'production.json'), '{secret-invalid-json')
|
||||
await expect(preflightDesktopPublishLocal(approved)).rejects.toThrow('invalid JSON')
|
||||
await writeFile(join(configRoot, 'publishing', 'production.json'), '[]')
|
||||
await expect(preflightDesktopPublishLocal(approved)).rejects.toThrow('must be a JSON object')
|
||||
await writeFile(join(configRoot, 'publishing', 'production.json'), '{}')
|
||||
await expect(preflightDesktopPublishLocal(approved)).resolves.toBeUndefined()
|
||||
await expect(preflightDesktopPublishLocal({ ...approved, DESKTOP_PUBLISH_ROUTE: '{"preset":"../test"}' })).rejects.toThrow('preset must be')
|
||||
await expect(preflightDesktopPublishLocal({ ...env, DESKTOP_PACKAGE_SELECTION: '{"run":{"id":"../42","run_attempt":2}}' })).rejects.toThrow('positive run ID')
|
||||
await expect(preflightDesktopPublishLocal({ ...env, DSH_DESKTOP_RELEASES_DIR: '' })).rejects.toThrow('retained package manifest')
|
||||
await expect(preflightDesktopPublishLocal({ ...env, DSH_PACKAGING_CONFIG_DIR: 'relative' })).rejects.toThrow('absolute machine')
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('runs the local preflight before dependency installation in both self-hosted jobs', async () => {
|
||||
const workflow = await readFile('.github/workflows/desktop-publish.yml', 'utf8')
|
||||
for (const job of ['prepare', 'publish']) {
|
||||
const section = workflow.split(`\n ${job}:`)[1]!.split(/\n \w+:/u)[0]!
|
||||
expect(section.indexOf('node apps/desktop/scripts/preflight-desktop-publish-local.mjs')).toBeGreaterThan(0)
|
||||
expect(section.indexOf('node apps/desktop/scripts/preflight-desktop-publish-local.mjs')).toBeLessThan(section.indexOf('pnpm install --frozen-lockfile'))
|
||||
}
|
||||
})
|
||||
Reference in new issue
Block a user