106 lines
3.4 KiB
TypeScript
106 lines
3.4 KiB
TypeScript
/**
|
|
* R3 Isolated Worktree & Gitea Evidence Upload Helper (`scripts/gitea-skill-workflow.ts`).
|
|
*
|
|
* Provides programmatic helpers for R3 skill workers to upload headless Chromium
|
|
* screenshots (`POST /api/v1/repos/troytt/diablo2-web/issues/{index}/assets`),
|
|
* post 1.13c numerical & runtime verification comments, and close Gitea issues.
|
|
*/
|
|
import { readFileSync } from 'node:fs'
|
|
import { basename } from 'node:path'
|
|
|
|
const GITEA_BASE_URL = 'https://git.projectdiablo2.cn/api/v1/repos/troytt/diablo2-web'
|
|
const GITEA_TOKEN = '94c84cc3bd2eab804346cb889650c843d0cda244'
|
|
|
|
export interface GiteaAssetUploadResult {
|
|
readonly id: number
|
|
readonly name: string
|
|
readonly uuid: string
|
|
readonly browser_download_url: string
|
|
}
|
|
|
|
const sleep = (ms: number) => new Promise(res => setTimeout(res, ms))
|
|
|
|
export async function uploadPngToGiteaIssue(
|
|
issueNumber: number,
|
|
pngPath: string,
|
|
): Promise<GiteaAssetUploadResult> {
|
|
const bytes = readFileSync(pngPath)
|
|
const fileName = basename(pngPath)
|
|
for (let attempt = 1; attempt <= 6; attempt += 1) {
|
|
try {
|
|
const form = new FormData()
|
|
form.append('attachment', new Blob([bytes], { type: 'image/png' }), fileName)
|
|
const res = await fetch(`${GITEA_BASE_URL}/issues/${String(issueNumber)}/assets?name=${encodeURIComponent(fileName)}`, {
|
|
method: 'POST',
|
|
headers: {
|
|
Authorization: `token ${GITEA_TOKEN}`,
|
|
},
|
|
body: form,
|
|
signal: AbortSignal.timeout(10000),
|
|
})
|
|
if (res.ok) {
|
|
return (await res.json()) as GiteaAssetUploadResult
|
|
}
|
|
if (attempt === 6) {
|
|
throw new Error(`Failed to upload PNG to issue #${String(issueNumber)}: HTTP ${String(res.status)}`)
|
|
}
|
|
} catch (err) {
|
|
if (attempt === 6) throw err
|
|
}
|
|
await sleep(250 * attempt)
|
|
}
|
|
throw new Error('Unreachable')
|
|
}
|
|
|
|
export async function postGiteaIssueComment(
|
|
issueNumber: number,
|
|
markdownBody: string,
|
|
): Promise<{ id: number; body: string }> {
|
|
for (let attempt = 1; attempt <= 6; attempt += 1) {
|
|
try {
|
|
const res = await fetch(`${GITEA_BASE_URL}/issues/${String(issueNumber)}/comments`, {
|
|
method: 'POST',
|
|
headers: {
|
|
Authorization: `token ${GITEA_TOKEN}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ body: markdownBody }),
|
|
signal: AbortSignal.timeout(10000),
|
|
})
|
|
if (res.ok) {
|
|
return (await res.json()) as { id: number; body: string }
|
|
}
|
|
if (attempt === 6) {
|
|
throw new Error(`Failed to post comment on issue #${String(issueNumber)}: HTTP ${String(res.status)}`)
|
|
}
|
|
} catch (err) {
|
|
if (attempt === 6) throw err
|
|
}
|
|
await sleep(250 * attempt)
|
|
}
|
|
throw new Error('Unreachable')
|
|
}
|
|
|
|
export async function closeGiteaIssue(issueNumber: number): Promise<void> {
|
|
for (let attempt = 1; attempt <= 6; attempt += 1) {
|
|
try {
|
|
const res = await fetch(`${GITEA_BASE_URL}/issues/${String(issueNumber)}`, {
|
|
method: 'PATCH',
|
|
headers: {
|
|
Authorization: `token ${GITEA_TOKEN}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ state: 'closed' }),
|
|
signal: AbortSignal.timeout(10000),
|
|
})
|
|
if (res.ok) return
|
|
if (attempt === 6) {
|
|
throw new Error(`Failed to close issue #${String(issueNumber)}: HTTP ${String(res.status)}`)
|
|
}
|
|
} catch (err) {
|
|
if (attempt === 6) throw err
|
|
}
|
|
await sleep(250 * attempt)
|
|
}
|
|
}
|