fix(desktop): publish to administratively unversioned COS buckets
This commit is contained in:
1 parent
af66022ace
commit
b8f8abc5d6
10 files changed
+97
-20
No files matched your search
@@ -15,14 +15,14 @@
|
||||
en: 1b6a4d7146879eec
|
||||
zh: f2520fc412cd2d2a
|
||||
/desktop-ci-publication/machine-setup:
|
||||
en: 990ff7e56dd6005a
|
||||
zh: 18f9dc23c520dc8c
|
||||
en: f80388c2b1ffef4c
|
||||
zh: f242ab2316bf38a1
|
||||
/desktop-ci-publication/package-and-publish:
|
||||
en: 0a26537845e89dda
|
||||
zh: 307378bb65e24650
|
||||
/desktop-ci-publication/recovery-and-records:
|
||||
en: e92bfad06162443e
|
||||
zh: 6c3e3d8ece673217
|
||||
en: 2e1e6b16d0d6bd41
|
||||
zh: 6ff4ad5a8f1f57b4
|
||||
/desktop-ci-publication/restore-a-channel:
|
||||
en: d82e2ccf70cce397
|
||||
zh: d93dcb77cc4b463e
|
||||
|
||||
@@ -23,7 +23,7 @@ Publish a completed package from its original machine without downloading its in
|
||||
|
||||
Keep `DSH_PACKAGING_CONFIG_DIR` pointed at the existing machine packaging directory. Completed attempts are copied to its sibling `releases/<run-id>/<attempt>/`; `DSH_DESKTOP_RELEASES_DIR` overrides that root with an absolute path. The inventory includes installers, update archives, blockmaps, completion metadata and a credential-free manifest. Debug packages are retained but cannot publish. Copies are checked before the attempt directory becomes visible, and an existing attempt cannot be replaced. GitHub retains the same file inventory for 14 days. Local files remain until an operator removes them; do not delete an attempt while it is being verified or published.
|
||||
|
||||
Create `publishing/test.json` and, when authorized, `publishing/production.json` below the configuration directory. Each declares `bucket`, HTTPS `origin` without a path, and an absolute `credentialsFile`. The credential file contains only `secretId` and `secretKey`. macOS also requires `macOS.signingIdentity` and `macOS.teamId`; Windows requires `windows.certificateThumbprint`. Production requires the exact origin `https://download.deepseek.com` and `githubApp.clientId` plus an absolute `githubApp.privateKeyFile`. These files stay outside Git. The uploader supports permanent COS credentials, not STS session credentials. Bucket versioning must be disabled for immutable binary writes.
|
||||
Create `publishing/test.json` and, when authorized, `publishing/production.json` below the configuration directory. Each declares `bucket`, HTTPS `origin` without a path, and an absolute `credentialsFile`. The credential file contains only `secretId` and `secretKey`. macOS also requires `macOS.signingIdentity` and `macOS.teamId`; Windows requires `windows.certificateThumbprint`. Production requires the exact origin `https://download.deepseek.com` and `githubApp.clientId` plus an absolute `githubApp.privateKeyFile`. These files stay outside Git. The uploader supports permanent COS credentials, not STS session credentials. Administrators must keep test and production buckets unversioned (neither enabled nor suspended). Publication assumes this deployment prerequisite and does not call `GetBucketVersioning` or require its permission. Immutable binary PUTs retain `x-cos-forbid-overwrite`; their protection depends on administrators maintaining the bucket configuration.
|
||||
|
||||
Set the repository variable `DESKTOP_PUBLISH_RUNNERS` to a JSON mapping of registered runner names to unique labels and targets, for example `{"mini":{"label":"dsh-mini-publish","target":"mac-arm64"}}`. Assign that label to exactly that runner. Windows uses target `win-x64`. The routing job checks the original successful job, and the publisher checks its own runner name against the recorded name. Machines need no shared filesystem or LAN connection to each other.
|
||||
|
||||
@@ -49,7 +49,7 @@ Before uploading, the publisher hashes all retained files, checks version and de
|
||||
|
||||
After resolving a failure, re-run the failed publishing job to keep its prepared package attempt, or start a new publication with the explicit original package run and attempt. Re-running all jobs with a blank attempt resolves the latest package attempt again and requires a new approval. Do not rebuild solely to retry an upload. Missing objects are uploaded, identical objects are skipped, and conflicting same-name bytes stop the operation. Unknown write outcomes are reconciled by reading remote bytes. A newer channel version prevents a stale upload from replacing it. Each package updates only its embedded channel: nightly writes `nightly.yml` / `nightly-mac.yml`, latest writes `latest.yml` / `latest-mac.yml`. Latest binaries use a `latest/` subdirectory under the target binary prefix, separate from existing nightly objects. Publishing the same source/version to both channels requires two packages; an existing release tag is reused only for the same source commit.
|
||||
|
||||
The sibling `upload-records/` directory retains an operation identity, newline-delimited events and terminal result. Feed-write intents retain both previous and requested YAML. Failures record their stage, safe error classification and diagnostic; raw provider responses are excluded. Audit initialization becomes visible only after its files are complete; abandoned `.initializing-*` directories are not scanned for completed publication. Writes and readback are recorded before proceeding; missing terminal results mean interruption, not success. Actions also keeps its operation record as a diagnostic artifact for 30 days. These local records are not tamper-proof. COS readback verifies storage bytes, not public CDN behavior; public download acceptance is separate.
|
||||
The sibling `upload-records/` directory retains an operation identity, newline-delimited events and terminal result. Feed-write intents retain both previous and requested YAML. Failures record their stage, safe error classification and diagnostic; COS failures also identify `GetObject` or `PutObject`, plus a validated HTTP status and request ID when available. Access-denied reports name the permission to inspect. Raw provider responses and credentials are excluded. Audit initialization becomes visible only after its files are complete; abandoned `.initializing-*` directories are not scanned for completed publication. Writes and readback are recorded before proceeding; missing terminal results mean interruption, not success. Actions also keeps its operation record as a diagnostic artifact for 30 days. These local records are not tamper-proof. COS readback verifies storage bytes, not public CDN behavior; public download acceptance is separate.
|
||||
|
||||
If the feed completed but tagging failed, the result is `published-tag-pending` and the job fails visibly. On retry, durable feed-completion evidence permits only tag recovery, even if a later release has advanced the channel. It never re-publishes old YAML. An existing conflicting tag requires administrator investigation and is never moved automatically.
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ description: "在专用 macOS 和 Windows runner 上发布留存的 Desktop 安
|
||||
|
||||
保持 `DSH_PACKAGING_CONFIG_DIR` 指向现有机器打包目录。完成的构建复制到其同级 `releases/<run-id>/<attempt>/`;`DSH_DESKTOP_RELEASES_DIR` 可用绝对路径覆盖该根目录。文件清单包含安装包、更新归档、blockmap、完成记录和不含凭据的 manifest。调试包会留存,但不能发布。复制内容通过校验后才开放 attempt 目录,已有 attempt 不能被替换。GitHub 保留相同文件清单 14 天。本地文件保留到操作员删除;校验或发布期间不要删除对应 attempt。
|
||||
|
||||
在配置目录下创建 `publishing/test.json`,获得授权后再创建 `publishing/production.json`。每份配置声明 `bucket`、不带路径的 HTTPS `origin` 和绝对路径 `credentialsFile`。凭据文件只包含 `secretId` 和 `secretKey`。macOS 还需 `macOS.signingIdentity` 和 `macOS.teamId`;Windows 需要 `windows.certificateThumbprint`。production 必须使用 origin `https://download.deepseek.com`,并提供 `githubApp.clientId` 和绝对路径 `githubApp.privateKeyFile`。这些文件不进入 Git。上传器支持长期 COS 凭据,不支持 STS 会话凭据。不可覆盖的二进制写入要求 bucket 版本控制关闭。
|
||||
在配置目录下创建 `publishing/test.json`,获得授权后再创建 `publishing/production.json`。每份配置声明 `bucket`、不带路径的 HTTPS `origin` 和绝对路径 `credentialsFile`。凭据文件只包含 `secretId` 和 `secretKey`。macOS 还需 `macOS.signingIdentity` 和 `macOS.teamId`;Windows 需要 `windows.certificateThumbprint`。production 必须使用 origin `https://download.deepseek.com`,并提供 `githubApp.clientId` 和绝对路径 `githubApp.privateKeyFile`。这些文件不进入 Git。上传器支持长期 COS 凭据,不支持 STS 会话凭据。管理员必须维持 test 和 production bucket 未启用版本控制(不能是已启用或已暂停状态)。发布流程以此为部署前提,不调用 `GetBucketVersioning`,也不要求该权限。二进制 PUT 仍携带 `x-cos-forbid-overwrite`;防覆盖保证依赖管理员维持桶配置。
|
||||
|
||||
将仓库变量 `DESKTOP_PUBLISH_RUNNERS` 配置为已注册 runner 名称到唯一标签和目标的 JSON 映射,例如 `{"mini":{"label":"dsh-mini-publish","target":"mac-arm64"}}`。该标签只能分配给这台 runner。Windows 使用目标 `win-x64`。路由 job 检查原构建成功的 job,发布器再检查自身 runner 名称与记录一致。机器之间不需要共享文件系统或局域网连接。
|
||||
|
||||
@@ -49,7 +49,7 @@ Environment 审批限制此工作流,不限制同一机器账户运行的任
|
||||
|
||||
解决故障后重跑失败的发布 job,以保持准备阶段选定的打包尝试;也可显式填写原打包运行和尝试,发起新的发布。尝试留空时重跑所有 job 会重新解析最新打包尝试,并需要重新审批。不要仅为重试上传而重新打包。缺失对象会上传,相同对象跳过,同名但字节不同的对象会停止操作。写入结果不明时通过读取远端字节核对。频道已有更高版本时,旧上传不能替换它。每个包只更新自身内置通道:nightly 写入 `nightly.yml` / `nightly-mac.yml`,latest 写入 `latest.yml` / `latest-mac.yml`。latest 二进制位于目标二进制前缀下的 `latest/` 子目录,与已有 nightly 对象分离。同一源码和版本发布到两个通道需要分别打包;已有发布 tag 仅在源码提交相同时复用。
|
||||
|
||||
同级 `upload-records/` 目录保留操作标识、逐行事件和最终结果。频道写入意图同时记录先前和目标 YAML。失败记录阶段、安全错误分类与诊断,不保存原始服务响应。审计初始化在文件完整后才可见;遗留的 `.initializing-*` 目录不参与已完成发布的扫描。写入和回读在继续操作前落盘;没有最终结果代表中断,不代表成功。Actions 还将操作记录保存为诊断 artifact,保留 30 天。这些本地记录不防篡改。COS 回读验证存储字节,不代表公开 CDN 行为;公开下载需单独验收。
|
||||
同级 `upload-records/` 目录保留操作标识、逐行事件和最终结果。频道写入意图同时记录先前和目标 YAML。失败记录阶段、安全错误分类与诊断;COS 失败还标明 `GetObject` 或 `PutObject`,并在可用时记录经过校验的 HTTP 状态码和 request ID。访问拒绝提示会指出需检查的权限。不保存原始服务响应或凭据。审计初始化在文件完整后才可见;遗留的 `.initializing-*` 目录不参与已完成发布的扫描。写入和回读在继续操作前落盘;没有最终结果代表中断,不代表成功。Actions 还将操作记录保存为诊断 artifact,保留 30 天。这些本地记录不防篡改。COS 回读验证存储字节,不代表公开 CDN 行为;公开下载需单独验收。
|
||||
|
||||
频道更新完成但 tag 失败时,结果为 `published-tag-pending`,job 明确失败。重试时,已落盘的频道完成证据只允许恢复 tag,即使后来发布已推进频道,也不会重新发布旧 YAML。已有冲突 tag 需要管理员调查,不会自动移动。
|
||||
|
||||
|
||||
@@ -5,13 +5,40 @@ import { DesktopPublishError } from './desktop-publish.ts'
|
||||
const publicCodes = new Set(['AccessDenied', 'InternalError', 'NoSuchBucket', 'BadDigest', 'SignatureDoesNotMatch',
|
||||
'RequestTimeout', 'TimeoutError', 'AbortError', 'ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND', 'ENOSPC', 'EACCES', 'EPERM', 'ENOENT'])
|
||||
|
||||
/** COS operation failure retaining its cause for safe diagnostics; a failed write may have an unknown remote outcome. */
|
||||
export class DesktopCosOperationError extends Error {
|
||||
constructor(readonly operation: 'GetObject' | 'PutObject', cause: unknown) {
|
||||
const failure = desktopPublicationFailure(cause, operation)
|
||||
const message = failure.code === 'AccessDenied'
|
||||
? `AccessDenied. Check name/cos:${operation} permission and explicit deny rules for the selected bucket/object. Publication stopped; reconcile remote state before retrying.`
|
||||
: failure.message
|
||||
super(`COS ${operation}: ${message}`, { cause })
|
||||
this.name = 'DesktopCosOperationError'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep actionable local validation details without recording SDK response bodies or credentials.
|
||||
* @param error Failure from validation, filesystem, subprocess or remote transport.
|
||||
* @param phase Operation stage known by the entrypoint.
|
||||
* @returns Credential-free diagnostic suitable for stderr and an audit event.
|
||||
*/
|
||||
export function desktopPublicationFailure(error: unknown, phase: string): { phase: string; code: string; message: string } {
|
||||
export function desktopPublicationFailure(error: unknown, phase: string): {
|
||||
phase: string
|
||||
code: string
|
||||
message: string
|
||||
operation?: 'GetObject' | 'PutObject'
|
||||
requestId?: string
|
||||
httpStatus?: number
|
||||
} {
|
||||
if (error instanceof DesktopCosOperationError) {
|
||||
const cause = typeof error.cause === 'object' && error.cause !== null ? error.cause : {}
|
||||
const requestId = 'RequestId' in cause ? cause.RequestId : undefined
|
||||
const status = 'statusCode' in cause ? cause.statusCode : undefined
|
||||
return { ...desktopPublicationFailure(error.cause, phase), operation: error.operation, message: error.message,
|
||||
...(typeof requestId === 'string' && /^[\w+/=.-]{1,256}$/u.test(requestId) ? { requestId } : {}),
|
||||
...(typeof status === 'number' && Number.isInteger(status) && status >= 100 && status <= 599 ? { httpStatus: status } : {}) }
|
||||
}
|
||||
if (error instanceof DesktopPublishError) return { phase, code: error.code, message: error.message }
|
||||
if (error instanceof ZodError) return { phase, code: 'invalid-data',
|
||||
message: `Invalid fields: ${error.issues.map(issue => `${issue.path.join('.') || '(root)'} (${issue.code})`).join(', ')}` }
|
||||
|
||||
@@ -7,6 +7,7 @@ import { createDesktopCos, DESKTOP_COS_REGION, type DesktopCosCredentials } from
|
||||
import { cosOperation } from './cos-operation.ts'
|
||||
import type { DesktopUploadPlan } from './desktop-upload-plan.ts'
|
||||
import { desktopArtifactDigest, type DesktopPublishTransport } from './desktop-publish.ts'
|
||||
import { DesktopCosOperationError } from './desktop-publication-failure.ts'
|
||||
|
||||
function missing(error: unknown): boolean {
|
||||
return typeof error === 'object' && error !== null && 'code' in error && error.code === 'NoSuchKey'
|
||||
@@ -14,6 +15,7 @@ function missing(error: unknown): boolean {
|
||||
|
||||
/**
|
||||
* Create a bounded COS transport restricted to the validated plan's object keys.
|
||||
* Administrators must keep bucket versioning absent; publication does not query bucket configuration.
|
||||
* @param plan Destination and exact object inventory; arbitrary keys are rejected.
|
||||
* @param credentials Credentials for this deployment only.
|
||||
* @param createClient Client factory; absent uses the shared production client, tests supply a loopback client.
|
||||
@@ -51,7 +53,7 @@ export function desktopPublishCos(
|
||||
return { size, sha512: sha512.digest('base64'), ...(channel ? { contents: Buffer.concat(chunks).toString('utf8') } : {}) }
|
||||
} catch (error) {
|
||||
if (missing(error)) return null
|
||||
throw error
|
||||
throw new DesktopCosOperationError('GetObject', error)
|
||||
} finally { output.destroy() }
|
||||
},
|
||||
async put(artifact, digest, forbidOverwrite) {
|
||||
@@ -62,13 +64,6 @@ export function desktopPublishCos(
|
||||
const actual = await desktopArtifactDigest(artifact)
|
||||
if (actual.size !== digest.size || actual.sha512 !== digest.sha512) throw new Error('desktop publish: local input changed')
|
||||
const cos = client()
|
||||
// COS overwrite prevention requires versioning to be absent, not enabled or suspended.
|
||||
if (forbidOverwrite) {
|
||||
const state = await cosOperation(cos, 30_000, () => cos.getBucketVersioning({ Bucket: plan.bucket, Region: DESKTOP_COS_REGION }))
|
||||
if (state.statusCode !== 200 || state.VersioningConfiguration.Status !== undefined) {
|
||||
throw new Error('desktop publish: immutable objects require bucket versioning to be disabled')
|
||||
}
|
||||
}
|
||||
const md5 = createHash('md5')
|
||||
const bytes = artifact.contents === undefined ? createReadStream(artifact.path) : [Buffer.from(artifact.contents)]
|
||||
for await (const chunk of bytes) md5.update(chunk)
|
||||
@@ -78,6 +73,8 @@ export function desktopPublishCos(
|
||||
...address, Body: body, ContentLength: digest.size, ContentType: artifact.contentType,
|
||||
Headers: { 'Content-MD5': md5.digest('base64'), ...(forbidOverwrite ? { 'x-cos-forbid-overwrite': 'true' } : {}) },
|
||||
}))
|
||||
} catch (error) {
|
||||
throw new DesktopCosOperationError('PutObject', error)
|
||||
} finally { body.destroy() }
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { spawnSync } from 'node:child_process'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { expect, it } from 'vitest'
|
||||
import { z } from 'zod'
|
||||
import { desktopPublicationFailure } from '../scripts/desktop-publication-failure.ts'
|
||||
import { DesktopCosOperationError, desktopPublicationFailure } from '../scripts/desktop-publication-failure.ts'
|
||||
import { DesktopPublishError } from '../scripts/desktop-publish.ts'
|
||||
|
||||
it('preserves actionable validation and filesystem diagnostics', () => {
|
||||
@@ -34,3 +34,13 @@ it('prints rollback usage through the real CLI before any audit or remote operat
|
||||
expect(diagnostic.code).toBe('validation')
|
||||
expect(diagnostic.message).toContain('Usage: rollback-desktop-channel.ts')
|
||||
})
|
||||
|
||||
it('omits malformed provider receipt fields and never copies raw errors into COS diagnostics', () => {
|
||||
const error = new DesktopCosOperationError('PutObject', { code: 'AccessDenied', statusCode: 'fixture-secret',
|
||||
RequestId: 'Authorization: fixture-secret', body: 'fixture-secret', message: 'fixture-secret' })
|
||||
const failure = desktopPublicationFailure(error, 'publication')
|
||||
expect(failure).toMatchObject({ code: 'AccessDenied', operation: 'PutObject' })
|
||||
expect(failure).not.toHaveProperty('requestId')
|
||||
expect(failure).not.toHaveProperty('httpStatus')
|
||||
expect(JSON.stringify(failure)).not.toContain('fixture-secret')
|
||||
})
|
||||
@@ -4,11 +4,12 @@ import { desktopPublishCos } from '../scripts/desktop-publish-cos.ts'
|
||||
import { desktopArtifactDigest } from '../scripts/desktop-publish.ts'
|
||||
import type { DesktopUploadPlan } from '../scripts/desktop-upload-plan.ts'
|
||||
import { answer, cosError, startCosLoopback } from './cos-loopback.ts'
|
||||
import { desktopPublicationFailure } from '../scripts/desktop-publication-failure.ts'
|
||||
|
||||
it('uses overwrite prevention, streams verified bytes, and hashes remote readback through the SDK', async () => {
|
||||
const objects = new Map<string, Buffer>()
|
||||
const server = await startCosLoopback((request, response) => {
|
||||
if (request.path.includes('versioning')) { answer(response, 200, '<VersioningConfiguration/>'); return }
|
||||
if (request.path.includes('versioning')) { answer(response, 403, cosError('AccessDenied')); return }
|
||||
if (request.method === 'PUT') {
|
||||
expect(request.headers['x-cos-forbid-overwrite']).toBe('true')
|
||||
objects.set(request.path, request.body)
|
||||
@@ -36,6 +37,8 @@ it('uses overwrite prevention, streams verified bytes, and hashes remote readbac
|
||||
expect(await transport.read(artifact.key, false)).toEqual(digest)
|
||||
await expect(transport.read('elsewhere/file.exe', false)).rejects.toThrow(/outside/u)
|
||||
expect(server.requests.filter(request => request.method === 'PUT')).toHaveLength(1)
|
||||
expect(server.requests.map(request => request.method)).toEqual(['GET', 'PUT', 'GET'])
|
||||
expect(server.requests.every(request => !request.path.includes('versioning'))).toBe(true)
|
||||
} finally { await server.close() }
|
||||
})
|
||||
|
||||
@@ -54,3 +57,27 @@ it('propagates an oversized channel stream error through the SDK and closes the
|
||||
expect(server.requests).toHaveLength(1)
|
||||
} finally { await server.close() }
|
||||
})
|
||||
|
||||
it.each(['GetObject', 'PutObject'] as const)('reports %s denial with a safe COS receipt', async (operation) => {
|
||||
const server = await startCosLoopback((_request, response) => {
|
||||
answer(response, 403, cosError('AccessDenied'), { 'content-type': 'application/xml', 'x-cos-request-id': 'fixture-request' })
|
||||
})
|
||||
try {
|
||||
const artifact = { path: '', filename: 'file.exe', key: 'bin/file.exe', contents: 'fixture bytes', contentType: 'application/octet-stream', channelMetadata: false }
|
||||
const plan: DesktopUploadPlan = { environment: 'test', target: 'win-x64', version: '1.0.0', bucket: 'fixture-1250000000',
|
||||
publicUrl: 'https://example.com/', secretIdEnvName: 'ID', secretKeyEnvName: 'KEY', artifacts: [artifact] }
|
||||
const transport = desktopPublishCos(plan, { secretId: 'fixture-id', secretKey: 'fixture-secret' }, (credentials) => {
|
||||
const client = createDesktopCos(credentials)
|
||||
server.redirect(client)
|
||||
return client
|
||||
})
|
||||
const digest = await desktopArtifactDigest(artifact)
|
||||
const pending = operation === 'GetObject' ? transport.read(artifact.key, false) : transport.put(artifact, digest, true)
|
||||
const failure = await pending.then(() => { throw new Error('Expected COS denial') }, (error: unknown) => desktopPublicationFailure(error, 'publication'))
|
||||
expect(failure).toMatchObject({ operation, code: 'AccessDenied', httpStatus: 403, requestId: 'fixture-request' })
|
||||
expect(JSON.stringify(failure)).not.toContain('fixture-secret')
|
||||
await expect(JSON.stringify(failure, null, 2) + '\n').toMatchFileSnapshot(`./expected/cos-${operation}-denied.json`)
|
||||
expect(server.requests).toHaveLength(1)
|
||||
expect(server.requests[0]!.method).toBe(operation === 'GetObject' ? 'GET' : 'PUT')
|
||||
} finally { await server.close() }
|
||||
})
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"phase": "publication",
|
||||
"code": "AccessDenied",
|
||||
"message": "COS GetObject: AccessDenied. Check name/cos:GetObject permission and explicit deny rules for the selected bucket/object. Publication stopped; reconcile remote state before retrying.",
|
||||
"operation": "GetObject",
|
||||
"requestId": "fixture-request",
|
||||
"httpStatus": 403
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"phase": "publication",
|
||||
"code": "AccessDenied",
|
||||
"message": "COS PutObject: AccessDenied. Check name/cos:PutObject permission and explicit deny rules for the selected bucket/object. Publication stopped; reconcile remote state before retrying.",
|
||||
"operation": "PutObject",
|
||||
"requestId": "fixture-request",
|
||||
"httpStatus": 403
|
||||
}
|
||||
@@ -34,7 +34,7 @@ it('confines signed Mac packaging to manual dispatch and a dedicated Apple Silic
|
||||
it('defaults to notarization and labels the explicit debugging option', () => {
|
||||
expect(workflow.on.workflow_dispatch).toMatchObject({ inputs: { skip_notarization: {
|
||||
type: 'boolean', default: false,
|
||||
description: '跳过公证和 stapling:仅限内部调试,不可用于发布或自动更新',
|
||||
description: '跳过公证和 stapling:仅限内部调试,禁止发布到 test / production 或用于自动更新;需要发布请勿勾选',
|
||||
} } })
|
||||
const steps = workflow.jobs.package!.steps
|
||||
expect(steps.filter(step => step.run?.includes('args+=(--skip-notarization)'))).toHaveLength(2)
|
||||
|
||||
Reference in new issue
Block a user