release
This commit is contained in:
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user