Files
DiunaBI/.gitea/scripts/getLatestRunWithArtifacts.js
Michał Zieliński 6feb20c85e r
2025-09-18 12:35:25 +02:00

155 lines
5.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// .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 doesnt 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);
});