From 6feb20c85e1e15f73083eeaa4b317ba1ecca543c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Zieli=C5=84ski?= Date: Thu, 18 Sep 2025 12:35:25 +0200 Subject: [PATCH] r --- .gitea/scripts/getLatestRunWithArtifacts.js | 158 +++++++++----------- 1 file changed, 70 insertions(+), 88 deletions(-) diff --git a/.gitea/scripts/getLatestRunWithArtifacts.js b/.gitea/scripts/getLatestRunWithArtifacts.js index de396d9..ef3d488 100644 --- a/.gitea/scripts/getLatestRunWithArtifacts.js +++ b/.gitea/scripts/getLatestRunWithArtifacts.js @@ -1,24 +1,18 @@ // .gitea/scripts/getLatestRunWithArtifacts.js -// Finds the latest successful *workflow run* that exposes all required artifacts. -// Uses API if available; otherwise falls back to HTML scraping for run listing and artifacts. - -/* ENVIRONMENT: - - GITEA_BASE_URL e.g. https://code.example.com - - OWNER repo owner, e.g. mz - - REPO repo name, e.g. DiunaBI - - GITEA_PAT personal access token with repo read permissions - - SCAN_LIMIT optional; max number of runs to scan (default: 100) - - REQUIRED_ARTIFACTS comma-separated list, default "frontend,webapi" -*/ +// 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 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); @@ -27,67 +21,52 @@ 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); } +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, accept = "application/json") { - const res = await fetch(url, { - headers: { Authorization: `token ${TOKEN}`, Accept: accept }, +async function http(url, opts = {}) { + return fetch(url, { + ...opts, + headers: { Authorization: `token ${TOKEN}`, ...(opts.headers || {}) }, }); - return res; } async function apiJSON(url) { - const res = await http(url, "application/json"); + const res = await http(url); if (!res.ok) { - const t = await res.text().catch(() => ""); - const err = new Error(`API ${res.status} for ${url}\n${t}`); + 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 || []; } -function isSuccessfulRun(r) { - const s = String(r.status || "").toLowerCase(); - const c = String(r.conclusion || "").toLowerCase(); - return s === "success" || (s === "completed" && c === "success"); -} - -/* -------- Run listing -------- */ async function tryApiListRuns() { - const pageSize = Math.min(50, SCAN_LIMIT); - const out = []; - let page = 1; - - while (out.length < SCAN_LIMIT) { - const url = `${BASE}/api/v1/repos/${OWNER}/${REPO}/actions/runs?limit=${pageSize}&page=${page}`; - let resp; - try { - resp = await apiJSON(url); - } catch (e) { - if (e.status === 404) return null; // endpoint not available - throw e; - } - const chunk = normalizeRunList(resp); - if (!Array.isArray(chunk) || chunk.length === 0) break; - out.push(...chunk); - if (chunk.length < pageSize) break; - page += 1; + 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; } - return out.slice(0, SCAN_LIMIT); } async function listRunsFromHtml() { const url = `${BASE}/${OWNER}/${REPO}/actions`; - const res = await http(url, "text/html"); + 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}`); @@ -97,30 +76,17 @@ async function listRunsFromHtml() { .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 })); + return unique.slice(0, SCAN_LIMIT).map(id => ({ id, status: "success" })); // HTML doesn’t give status, assume ok } -/* -------- Artifact listing -------- */ -async function readArtifactNames(runId) { - const url = `${BASE}/api/v1/repos/${OWNER}/${REPO}/actions/runs/${runId}/artifacts`; - try { - const json = await apiJSON(url); - const list = Array.isArray(json?.artifacts) - ? json.artifacts - : (Array.isArray(json) ? json : []); - return list.map(a => a?.name).filter(Boolean); - } catch (e) { - // fallback: artifacts tab in HTML - const res = await http(`${BASE}/${OWNER}/${REPO}/actions/runs/${runId}?_tab=artifacts`, "text/html"); - if (!res.ok) return []; - const html = await res.text(); - return Array.from(html.matchAll(/\/actions\/artifacts\/\d+[^>]*>([^<]+) m[1].trim()) - .filter(Boolean); - } +// ---- 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; } -/* -------- Main -------- */ (async () => { let runs = await tryApiListRuns(); if (!runs) { @@ -133,40 +99,56 @@ async function readArtifactNames(runId) { process.exit(1); } + // newest first const candidates = runs .filter(r => r && r.id != null) - .sort((a, b) => (b.id ?? 0) - (a.id ?? 0)); + .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 pickedId = null; - + let picked = null; for (const r of candidates) { const runId = r.id; - if (r.status && r.conclusion && !isSuccessfulRun(r)) { - console.log(`Run ${runId}: not successful`); - continue; + 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; + } } - try { - const names = await readArtifactNames(runId); - const ok = REQUIRED_ARTIFACTS.every(req => names.includes(req)); - if (ok) { pickedId = runId; break; } - console.log(`Run ${runId}: lacks required artifacts (has: ${names.join(", ") || "none"})`); - } catch (e) { - console.log(`Run ${runId}: cannot read artifacts -> ${String(e.message).split("\n")[0]}`); + + if (allPresent) { + picked = { id: runId }; + console.log(`Picked run_id=${runId}`); + break; } } - if (!pickedId) { + if (!picked) { console.error("No run exposes all required artifacts."); process.exit(1); } - fs.writeFileSync(path.join(cacheDir, "run_id"), String(pickedId), "utf8"); + 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=${pickedId}\n`); + fs.appendFileSync(process.env.GITHUB_OUTPUT, `run_id=${runIdStr}\n`); + } else { + console.log(`::set-output name=run_id::${runIdStr}`); } - console.log(`Picked run_id=${pickedId}`); })().catch(err => { console.error(err.stack || err.message || String(err)); process.exit(1);