fix(desktop): preserve tag creation rejection diagnostics
This commit is contained in:
1 parent
7e3ee09872
commit
7cd00ebaaa
5 files changed
+55
-9
No files matched your search
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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 标签本身不能协调其他机器的写入。每个频道保持单一发布机器,迁移时保留记录。
|
||||
|
||||
|
||||
@@ -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`)
|
||||
}
|
||||
@@ -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')
|
||||
})
|
||||
Reference in new issue
Block a user