fix(desktop): preserve tag creation rejection diagnostics

This commit is contained in:
winewill committed 2026-10-28 11:08:27 +08:00
1 parent 7e3ee09872
commit 7cd00ebaaa
5 files changed
+55 -9

No files matched your search

+2 -2
View File
@@ -27,8 +27,8 @@
en: f7c12d343891e547
zh: da596ee1366c2046
/desktop-ci-publication/recovery-and-records:
en: 2e1e6b16d0d6bd41
zh: 6ff4ad5a8f1f57b4
en: b62a7484929f9941
zh: c9dd06c77098e6ef
/desktop-ci-publication/restore-a-channel:
en: d82e2ccf70cce397
zh: d93dcb77cc4b463e
+1 -1
View File
@@ -84,7 +84,7 @@ After resolving a failure, re-run the failed publishing job to keep its prepared
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.
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. A rejected tag creation retains its HTTP status and validated GitHub request ID; a follow-up lookup does not replace that failure with a missing-tag error. A repository-rule rejection directs the operator to the matching rulesets without logging the raw response.
Normal publication and manual restoration acquire the same local channel lock. A crash can leave a lock directory under the sibling `publish-locks/`; verify no writer is running before manually removing it. A runner label alone does not coordinate writers on other machines. Keep one publishing machine per channel and preserve its records when migrating.
+1 -1
View File
@@ -84,7 +84,7 @@ Environment 审批限制此工作流,不限制同一机器账户运行的任
同级 `upload-records/` 目录保留操作标识、逐行事件和最终结果。频道写入意图同时记录先前和目标 YAML。失败记录阶段、安全错误分类与诊断;COS 失败还标明 `GetObject` 或 `PutObject`,并在可用时记录经过校验的 HTTP 状态码和 request ID。访问拒绝提示会指出需检查的权限。不保存原始服务响应或凭据。审计初始化在文件完整后才可见;遗留的 `.initializing-*` 目录不参与已完成发布的扫描。写入和回读在继续操作前落盘;没有最终结果代表中断,不代表成功。Actions 还将操作记录保存为诊断 artifact,保留 30 天。这些本地记录不防篡改。COS 回读验证存储字节,不代表公开 CDN 行为;公开下载需单独验收。
频道更新完成但 tag 失败时,结果为 `published-tag-pending`,job 明确失败。重试时,已落盘的频道完成证据只允许恢复 tag,即使后来发布已推进频道,也不会重新发布旧 YAML。已有冲突 tag 需要管理员调查,不会自动移动。
频道更新完成但 tag 失败时,结果为 `published-tag-pending`,job 明确失败。重试时,已落盘的频道完成证据只允许恢复 tag,即使后来发布已推进频道,也不会重新发布旧 YAML。已有冲突 tag 需要管理员调查,不会自动移动。tag 创建被拒绝时保留其 HTTP 状态及经过校验的 GitHub request ID,后续查询不会用 tag 不存在的错误替换创建失败。仓库规则拒绝会提示操作人员检查匹配的 ruleset,不记录原始响应。
正常发布和手动恢复获取同一把本地频道锁。崩溃可能在同级 `publish-locks/` 下留下锁目录;手动删除前确认没有写入任务运行。runner 标签本身不能协调其他机器的写入。每个频道保持单一发布机器,迁移时保留记录。
+26 -3
View File
@@ -40,8 +40,31 @@ export async function publishDesktopTag(
body: JSON.stringify({ ref: `refs/tags/${tag}`, sha: commit }), redirect: 'error', signal: AbortSignal.timeout(30_000),
})
if (response.status === 201) { await matches(response); return 'created' }
// A parallel platform release may have created the same version tag after our first read.
if (response.status === 422) { await response.body?.cancel(); await matches(await get()); return 'present' }
const requestId = response.headers.get('x-github-request-id')
const identity = requestId !== null && /^[A-Fa-f0-9:]{1,128}$/u.test(requestId) ? `; GitHub request ${requestId}` : ''
const failure = `desktop tag: creation failed (HTTP ${response.status}${identity})`
// A 422 can reject repository rules as well as report a concurrent tag creation.
if (response.status === 422) {
let detail: unknown
try { detail = await response.json() } catch (error) {
// Non-JSON error responses still retain the original HTTP status and request ID.
detail = undefined
}
const rulesRejected = typeof detail === 'object' && detail !== null && 'message' in detail
&& typeof detail.message === 'string' && detail.message.startsWith('Repository rule violations found')
const guidance = rulesRejected ? 'repository rules rejected tag creation; inspect the matching tag rulesets'
: 'inspect repository rule insights and App permissions before retrying'
let reconciled: Response
try { reconciled = await get() } catch (cause) {
throw new Error(`${failure}; follow-up tag lookup failed; ${guidance}`, { cause })
}
if (!reconciled.ok) {
await reconciled.body?.cancel()
throw new Error(`${failure}; follow-up tag lookup returned HTTP ${reconciled.status}; ${guidance}`)
}
await matches(reconciled)
return 'present'
}
await response.body?.cancel()
throw new Error(`desktop tag: creation failed (HTTP ${response.status}); reconcile the tag before retrying`)
throw new Error(`${failure}; reconcile the tag before retrying`)
}
+25 -2
View File
@@ -3,13 +3,13 @@ import { publishDesktopTag } from '../scripts/desktop-publish-tag.ts'
const commit = 'a'.repeat(40)
const tag = (sha = commit) => ({ object: { type: 'commit', sha } })
function transport(replies: Array<{ status: number; body?: object }>) {
function transport(replies: Array<{ status: number; body?: object; headers?: Record<string, string> }>) {
const calls: Array<{ url: string; init: RequestInit | undefined }> = []
const request: typeof fetch = async (url, init) => {
calls.push({ url: typeof url === 'string' ? url : url instanceof URL ? url.href : url.url, init })
const reply = replies.shift()
if (!reply) throw new Error('unexpected HTTP request')
return new Response(JSON.stringify(reply.body ?? {}), { status: reply.status })
return new Response(JSON.stringify(reply.body ?? {}), { status: reply.status, headers: reply.headers ?? {} })
}
return { calls, request }
}
@@ -57,3 +57,26 @@ it('explains an existing annotated tag without replacing it', async () => {
await expect(publishDesktopTag('owner/repo', '1.2.3', commit, 'fixture-token', http.request)).rejects.toThrow(/not a lightweight commit tag/u)
expect(http.calls).toHaveLength(1)
})
it('keeps the creation status and safe rule diagnostic when no competing tag exists', async () => {
const http = transport([{ status: 404 }, { status: 422,
headers: { 'x-github-request-id': 'ABCD:1234' },
body: { message: 'Repository rule violations found for refs/tags/desktop-v1.2.3', errors: ['secret-value'] } }, { status: 404 }])
const failure = publishDesktopTag('owner/repo', '1.2.3', commit, 'fixture-token', http.request)
await expect(failure).rejects.toThrow('creation failed (HTTP 422; GitHub request ABCD:1234); follow-up tag lookup returned HTTP 404; repository rules rejected')
await expect(failure).rejects.not.toThrow('secret-value')
expect(http.calls.map(call => call.init?.method ?? 'GET')).toEqual(['GET', 'POST', 'GET'])
})
it('does not echo arbitrary GitHub response text or unrecognized request IDs', async () => {
const http = transport([{ status: 404 }, { status: 422,
headers: { 'x-github-request-id': 'fixture-token' }, body: { message: 'fixture-token' } }, { status: 403 }])
const failure = publishDesktopTag('owner/repo', '1.2.3', commit, 'fixture-token', http.request)
await expect(failure).rejects.toThrow('creation failed (HTTP 422); follow-up tag lookup returned HTTP 403')
await expect(failure).rejects.not.toThrow('fixture-token')
})
it('still rejects a competing tag on a different commit', async () => {
const http = transport([{ status: 404 }, { status: 422 }, { status: 200, body: tag('b'.repeat(40)) }])
await expect(publishDesktopTag('owner/repo', '1.2.3', commit, 'fixture-token', http.request)).rejects.toThrow('refusing to move')
})