/** * 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 { 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 { 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) } }