// .gitea/scripts/getLatestRunWithArtifacts.js // Purpose: Find latest successful run that exposes all REQUIRED_ARTIFACTS via GUI URLs. // Outputs: sets `run_id` to GITHUB_OUTPUT and writes .gitea/.cache/run_id file. const fs = require("fs"); const path = require("path"); (async () => { // --- Config from environment --- const BASE = process.env.GITEA_BASE_URL; // e.g. https://code.bim-it.pl const OWNER = process.env.OWNER; // e.g. mz const REPO = process.env.REPO; // e.g. DiunaBI const TOKEN = process.env.GITEA_PAT; // 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); } // Ensure cache dir exists const cacheDir = path.join(".gitea", ".cache"); fs.mkdirSync(cacheDir, { recursive: true }); // Helpers const api = async (url) => { const res = await fetch(url, { headers: { Authorization: `token ${TOKEN}` } }); if (!res.ok) { const text = await res.text().catch(() => ""); throw new Error(`API ${res.status} ${res.statusText} for ${url}\n${text}`); } return res.json(); }; const headOk = async (url) => { // Try HEAD first; some instances may require GET for redirects let res = await fetch(url, { method: "HEAD", redirect: "follow", headers: { Authorization: `token ${TOKEN}` } }); if (res.ok) return true; // Fallback to GET (no download) just to test availability res = await fetch(url, { method: "GET", redirect: "manual", headers: { Authorization: `token ${TOKEN}` } }); // Accept 200 OK, or 3xx redirect to a signed download URL return res.status >= 200 && res.status < 400; }; // 1) Get recent workflow runs (a.k.a. tasks) via REST const listUrl = `${BASE}/api/v1/repos/${OWNER}/${REPO}/actions/tasks?limit=${SCAN_LIMIT}`; const resp = await api(listUrl); // 2) Build candidate list: only status == "success", newest first by id const runs = Array.isArray(resp.workflow_runs) ? resp.workflow_runs : []; const candidates = runs .filter(r => r && r.status === "success") .sort((a, b) => (b.id ?? 0) - (a.id ?? 0)); if (!candidates.length) { console.error("No successful runs found."); process.exit(1); } console.log(`Scanning ${candidates.length} successful runs for artifacts: ${REQUIRED_ARTIFACTS.join(", ")}`); // 3) Find the first run that exposes all required artifacts via GUI URLs 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. Consider increasing SCAN_LIMIT or verify artifact names."); process.exit(1); } // 4) Write outputs const runIdStr = String(picked.id); // Write to cache (handy for debugging) fs.writeFileSync(path.join(cacheDir, "run_id"), runIdStr, "utf8"); // Export as GitHub-style output (supported by Gitea runners) const outFile = process.env.GITHUB_OUTPUT; if (outFile) { fs.appendFileSync(outFile, `run_id=${runIdStr}\n`); } else { // Fallback: also print for visibility console.log(`::set-output name=run_id::${runIdStr}`); } })().catch(err => { console.error(err.stack || err.message || String(err)); process.exit(1); });