From a4249728be12c7169e3e1177a90cf293c2866234 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Zieli=C5=84ski?= Date: Thu, 18 Sep 2025 12:05:10 +0200 Subject: [PATCH] release --- .gitea/scripts/getLatestRunWithArtifacts.js | 163 +++++++++++++++----- 1 file changed, 122 insertions(+), 41 deletions(-) diff --git a/.gitea/scripts/getLatestRunWithArtifacts.js b/.gitea/scripts/getLatestRunWithArtifacts.js index 9dc448d..6523eac 100644 --- a/.gitea/scripts/getLatestRunWithArtifacts.js +++ b/.gitea/scripts/getLatestRunWithArtifacts.js @@ -1,82 +1,162 @@ // .gitea/scripts/getLatestRunWithArtifacts.js -// Robust finder: uses only Gitea API (no GUI URLs). +// Robust finder for the latest successful *workflow run* that exposes required artifacts. +// Works against Gitea's Actions API only (no scraping of UI URLs). + +/* ENVIRONMENT: + - GITEA_BASE_URL e.g. https://code.example.com + - OWNER repository owner/user, e.g. mz + - REPO repository name, e.g. DiunaBI + - GITEA_PAT personal access token with repo read permissions + - SCAN_LIMIT optional; max number of runs to scan across pages (default: 100) + - REQUIRED_ARTIFACTS comma-separated list, e.g. "frontend,webapi" (default: as left) + - FILTER_BRANCH optional; if set, only consider runs from this branch (e.g. "main") + - INCLUDE_TAGS optional; "true" to allow refs/tags (default: true) +*/ const fs = require("fs"); const path = require("path"); -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 +// ---- Config & defaults ------------------------------------------------------ +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); + .split(",") + .map(s => s.trim()) + .filter(Boolean); + +const FILTER_BRANCH = (process.env.FILTER_BRANCH || "").trim(); // e.g. "main" +const INCLUDE_TAGS = String(process.env.INCLUDE_TAGS ?? "true").toLowerCase() !== "false"; if (!BASE || !OWNER || !REPO) { - console.error("Missing one of: GITEA_BASE_URL, OWNER, REPO"); process.exit(1); + console.error("Missing one of: GITEA_BASE_URL, OWNER, REPO"); + process.exit(1); } if (!TOKEN) { - console.error("Missing GITEA_PAT"); process.exit(1); + console.error("Missing GITEA_PAT"); + process.exit(1); } +// Output dir for passing values between steps const cacheDir = path.join(".gitea", ".cache"); fs.mkdirSync(cacheDir, { recursive: true }); +// ---- Small HTTP helper ------------------------------------------------------ async function apiJSON(url) { const res = await fetch(url, { headers: { Authorization: `token ${TOKEN}` } }); if (!res.ok) { - const t = await res.text().catch(()=>""); + const t = await res.text().catch(() => ""); throw new Error(`API ${res.status} for ${url}\n${t}`); } return res.json(); } +// Normalize different shapes of list responses across Gitea versions +function normalizeRunList(resp) { + if (Array.isArray(resp)) return resp; + return resp?.runs || resp?.workflow_runs || resp?.data || resp?.items || []; +} + +// Success predicate at the RUN level +function isSuccessfulRun(r) { + const status = String(r.status || "").toLowerCase(); // "completed", "success" (varies) + const concl = String(r.conclusion || "").toLowerCase(); // "success" (may be empty) + return status === "success" || (status === "completed" && concl === "success"); +} + +// Optional ref filter +function passesRefFilter(run) { + // Gitea usually provides head_branch plus ref. We accept: + // - FILTER_BRANCH (if set) + // - tags (refs/tags/*) when INCLUDE_TAGS=true + const headBranch = run.head_branch || ""; + const ref = run.ref || ""; + if (FILTER_BRANCH && headBranch && headBranch !== FILTER_BRANCH) { + // If FILTER_BRANCH is set, allow tags too if INCLUDE_TAGS=true + if (!(INCLUDE_TAGS && ref.startsWith("refs/tags/"))) return false; + } + if (!FILTER_BRANCH && !INCLUDE_TAGS) { + // No branch required and tags disabled → accept anything on branches only + if (ref.startsWith("refs/tags/")) return false; + } + return true; +} + +// Fetch paginated runs up to SCAN_LIMIT +async function fetchRunsPaginated() { + const pageSize = Math.min(50, SCAN_LIMIT); // be nice to the API + const out = []; + let page = 1; + + while (out.length < SCAN_LIMIT) { + const url = `${BASE}/api/v1/repos/${OWNER}/${REPO}/actions/runs?limit=${pageSize}&page=${page}`; + const resp = await apiJSON(url); + const chunk = normalizeRunList(resp); + if (!Array.isArray(chunk) || chunk.length === 0) break; + out.push(...chunk); + if (chunk.length < pageSize) break; // last page + page += 1; + } + + // Trim to SCAN_LIMIT if we over-fetched + return out.slice(0, SCAN_LIMIT); +} + +// Read artifact names for a given run id +async function readArtifactNames(runId) { + const url = `${BASE}/api/v1/repos/${OWNER}/${REPO}/actions/runs/${runId}/artifacts`; + const resp = await apiJSON(url); + const list = Array.isArray(resp?.artifacts) ? resp.artifacts : (Array.isArray(resp) ? resp : []); + return list.map(a => a?.name).filter(Boolean); +} + +// ---- Main ------------------------------------------------------------------- (async () => { - // 1) Pobierz listę tasków (runs) - const listUrl = `${BASE}/api/v1/repos/${OWNER}/${REPO}/actions/tasks?limit=${SCAN_LIMIT}`; - const resp = await apiJSON(listUrl); - - // Gitea 1.24 może zwracać różne kształty – normalizujemy: - const raw = Array.isArray(resp) - ? resp - : (resp.workflow_runs || resp.tasks || resp.data || []); - if (!Array.isArray(raw) || raw.length === 0) { - console.error("No runs returned by API."); process.exit(1); + const runs = await fetchRunsPaginated(); + if (!runs.length) { + console.error("No workflow runs returned by API."); + process.exit(1); } - // 2) Filtrujemy „successful” - const isSuccess = (r) => { - const s = (r.status || "").toLowerCase(); // "success" / "completed" - const c = (r.conclusion || "").toLowerCase(); // "success" (czasem brak) - return s === "success" || (s === "completed" && c === "success"); - }; - - const candidates = raw - .filter(r => r && (r.id != null)) + // Most recent first by id (Gitea ids are monotonic; if needed, also compare created_at) + const candidates = runs + .filter(r => r && r.id != null) .sort((a, b) => (b.id ?? 0) - (a.id ?? 0)) - .filter(isSuccess); + .filter(isSuccessfulRun) + .filter(passesRefFilter); - if (candidates.length === 0) { - console.error("No successful runs found."); process.exit(1); + if (!candidates.length) { + console.error("No successful workflow runs found (after filtering)."); + process.exit(1); } - console.log(`Scanning ${candidates.length} successful runs for artifacts: ${REQUIRED_ARTIFACTS.join(", ")}`); + console.log( + `Scanning ${candidates.length} successful runs for artifacts: ${REQUIRED_ARTIFACTS.join(", ")}` + ); - // 3) Sprawdź artefakty przez API let pickedId = null; + for (const r of candidates) { const runId = r.id; - const artsUrl = `${BASE}/api/v1/repos/${OWNER}/${REPO}/actions/runs/${runId}/artifacts`; try { - const arts = await apiJSON(artsUrl); - const names = (Array.isArray(arts?.artifacts) ? arts.artifacts : arts || []) - .map(a => a?.name) - .filter(Boolean); + 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"})`); + 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 via API -> ${e.message.split("\n")[0]}`); + // If artifacts endpoint fails (permissions, transient), keep scanning + console.log( + `Run ${runId}: cannot read artifacts via API -> ${String(e.message).split("\n")[0]}` + ); } } @@ -85,11 +165,12 @@ async function apiJSON(url) { process.exit(1); } - // 4) Zapisz outputy + // Write outputs for downstream steps fs.writeFileSync(path.join(cacheDir, "run_id"), String(pickedId), "utf8"); if (process.env.GITHUB_OUTPUT) { fs.appendFileSync(process.env.GITHUB_OUTPUT, `run_id=${pickedId}\n`); } + console.log(`Picked run_id=${pickedId}`); })().catch(err => { console.error(err.stack || err.message || String(err));