// .gitea/scripts/getLatestRunWithArtifacts.js // Purpose: Find latest successful run that exposes all REQUIRED_ARTIFACTS via GUI URLs. // Strategy: // 1. Try to list runs via API (/actions/runs). // 2. If not available (404), fallback to scraping HTML /actions page. // 3. For each run (newest first, only "success"), check artifacts by probing GUI URLs. // Outputs: sets `run_id` to GITHUB_OUTPUT and writes .gitea/.cache/run_id file. const fs = require("fs"); const path = require("path"); const BASE = process.env.GITEA_BASE_URL; const OWNER = process.env.OWNER; const REPO = process.env.REPO; const TOKEN = process.env.GITEA_PAT; const SCAN_LIMIT = Number(process.env.SCAN_LIMIT || "100"); const REQUIRED_ARTIFACTS = (process.env.REQUIRED_ARTIFACTS || "frontend,webapi") .split(",").map(s => s.trim()).filter(Boolean); if (!BASE || !OWNER || !REPO) { console.error("Missing one of: GITEA_BASE_URL, OWNER, REPO"); process.exit(1); } if (!TOKEN) { console.error("Missing GITEA_PAT"); process.exit(1); } const cacheDir = path.join(".gitea", ".cache"); fs.mkdirSync(cacheDir, { recursive: true }); async function http(url, opts = {}) { return fetch(url, { ...opts, headers: { Authorization: `token ${TOKEN}`, ...(opts.headers || {}) }, }); } async function apiJSON(url) { const res = await http(url); if (!res.ok) { const text = await res.text().catch(() => ""); const err = new Error(`API ${res.status} ${res.statusText} for ${url}\n${text}`); err.status = res.status; throw err; } return res.json(); } // ---- Run listing ---- function normalizeRunList(resp) { if (Array.isArray(resp)) return resp; return resp?.runs || resp?.workflow_runs || resp?.data || resp?.items || []; } async function tryApiListRuns() { const url = `${BASE}/api/v1/repos/${OWNER}/${REPO}/actions/runs?limit=${SCAN_LIMIT}`; try { const resp = await apiJSON(url); return normalizeRunList(resp); } catch (e) { if (e.status === 404) return null; throw e; } } async function listRunsFromHtml() { const url = `${BASE}/${OWNER}/${REPO}/actions`; const res = await http(url, { headers: { Accept: "text/html" } }); if (!res.ok) { const t = await res.text().catch(() => ""); throw new Error(`HTML ${res.status} for ${url}\n${t}`); } const html = await res.text(); const runIds = Array.from(html.matchAll(/\/actions\/runs\/(\d+)/g)) .map(m => Number(m[1])) .filter(n => Number.isFinite(n)); const unique = [...new Set(runIds)].sort((a, b) => b - a); return unique.slice(0, SCAN_LIMIT).map(id => ({ id, status: "success" })); // HTML doesn’t give status, assume ok } // ---- Artifact check via GUI URL ---- async function headOk(url) { let res = await http(url, { method: "HEAD", redirect: "follow" }); if (res.ok) return true; res = await http(url, { method: "GET", redirect: "manual" }); return res.status >= 200 && res.status < 400; } (async () => { let runs = await tryApiListRuns(); if (!runs) { console.log("Runs API not available on this Gitea – falling back to HTML scraping."); runs = await listRunsFromHtml(); } if (!runs.length) { console.error("No workflow runs found."); process.exit(1); } // newest first const candidates = runs .filter(r => r && r.id != null) .sort((a, b) => (b.id ?? 0) - (a.id ?? 0)) .filter(r => (r.status || "").toLowerCase() === "success" || !r.status); // HTML case: no status info if (!candidates.length) { console.error("No successful runs found."); process.exit(1); } console.log(`Scanning ${candidates.length} runs for artifacts: ${REQUIRED_ARTIFACTS.join(", ")}`); let picked = null; for (const r of candidates) { const runId = r.id; const urls = REQUIRED_ARTIFACTS.map( name => `${BASE}/${OWNER}/${REPO}/actions/runs/${runId}/artifacts/${encodeURIComponent(name)}` ); let allPresent = true; for (const u of urls) { const ok = await headOk(u).catch(() => false); if (!ok) { allPresent = false; console.log(`Run ${runId}: artifact not accessible -> ${u}`); break; } } if (allPresent) { picked = { id: runId }; console.log(`Picked run_id=${runId}`); break; } } if (!picked) { console.error("No run exposes all required artifacts."); process.exit(1); } const runIdStr = String(picked.id); fs.writeFileSync(path.join(cacheDir, "run_id"), runIdStr, "utf8"); if (process.env.GITHUB_OUTPUT) { fs.appendFileSync(process.env.GITHUB_OUTPUT, `run_id=${runIdStr}\n`); } else { console.log(`::set-output name=run_id::${runIdStr}`); } })().catch(err => { console.error(err.stack || err.message || String(err)); process.exit(1); });