diff --git a/.gitea/scripts/getLatestRunWithArtifacts.js b/.gitea/scripts/getLatestRunWithArtifacts.js index 08fc2ff..de396d9 100644 --- a/.gitea/scripts/getLatestRunWithArtifacts.js +++ b/.gitea/scripts/getLatestRunWithArtifacts.js @@ -1,145 +1,173 @@ +// .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. -const https = require('https'); -const fs = require('fs'); -const path = require('path'); +/* 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" +*/ -// Konfiguracja z zmiennych środowiskowych -const config = { - baseUrl: process.env.GITEA_BASE_URL, - owner: process.env.OWNER, - repo: process.env.REPO, - token: process.env.GITEA_PAT, - requiredArtifacts: process.env.REQUIRED_ARTIFACTS?.split(',') || [], - scanLimit: parseInt(process.env.SCAN_LIMIT) || 50 -}; +const fs = require("fs"); +const path = require("path"); -// Walidacja konfiguracji -if (!config.baseUrl || !config.owner || !config.repo || !config.token) { - console.error('Błąd: Brak wymaganych zmiennych środowiskowych (GITEA_BASE_URL, OWNER, REPO, 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); + +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); } -console.log(`Szukam ostatniego udanego buildu dla ${config.owner}/${config.repo}`); -console.log(`Wymagane artefakty: ${config.requiredArtifacts.join(', ')}`); +const cacheDir = path.join(".gitea", ".cache"); +fs.mkdirSync(cacheDir, { recursive: true }); -// Funkcja do wykonywania zapytań HTTPS -function makeRequest(url, options = {}) { - return new Promise((resolve, reject) => { - const parsedUrl = new URL(url); - const requestOptions = { - hostname: parsedUrl.hostname, - port: parsedUrl.port || 443, - path: parsedUrl.pathname + parsedUrl.search, - method: 'GET', - headers: { - 'Authorization': `token ${config.token}`, - 'Accept': 'application/json', - 'User-Agent': 'Gitea-Workflow-Script' - }, - ...options - }; - - const req = https.request(requestOptions, (res) => { - let data = ''; - res.on('data', chunk => data += chunk); - res.on('end', () => { - if (res.statusCode >= 200 && res.statusCode < 300) { - try { - resolve(JSON.parse(data)); - } catch (e) { - resolve(data); - } - } else { - reject(new Error(`HTTP ${res.statusCode}: ${data}`)); - } - }); - }); - - req.on('error', reject); - req.end(); +async function http(url, accept = "application/json") { + const res = await fetch(url, { + headers: { Authorization: `token ${TOKEN}`, Accept: accept }, }); + return res; } -// Funkcja do pobierania listy workflow runs -async function getWorkflowRuns() { - const url = `${config.baseUrl}/api/v1/repos/${config.owner}/${config.repo}/actions/runs?limit=${config.scanLimit}`; - console.log(`Pobieranie workflow runs z: ${url}`); - - try { - const response = await makeRequest(url); - return response.workflow_runs || []; - } catch (error) { - console.error('Błąd podczas pobierania workflow runs:', error.message); - throw error; +async function apiJSON(url) { + const res = await http(url, "application/json"); + if (!res.ok) { + const t = await res.text().catch(() => ""); + const err = new Error(`API ${res.status} for ${url}\n${t}`); + err.status = res.status; + throw err; } + return res.json(); } -// Funkcja do pobierania artefaktów dla danego run -async function getRunArtifacts(runId) { - const url = `${config.baseUrl}/api/v1/repos/${config.owner}/${config.repo}/actions/runs/${runId}/artifacts`; - - try { - const response = await makeRequest(url); - return response.artifacts || []; - } catch (error) { - console.warn(`Nie można pobrać artefaktów dla run ${runId}:`, error.message); - return []; - } +function normalizeRunList(resp) { + if (Array.isArray(resp)) return resp; + return resp?.runs || resp?.workflow_runs || resp?.data || resp?.items || []; } -// Główna funkcja -async function findLatestSuccessfulRunWithArtifacts() { - try { - const runs = await getWorkflowRuns(); - console.log(`Znaleziono ${runs.length} workflow runs`); +function isSuccessfulRun(r) { + const s = String(r.status || "").toLowerCase(); + const c = String(r.conclusion || "").toLowerCase(); + return s === "success" || (s === "completed" && c === "success"); +} - for (const run of runs) { - console.log(`Sprawdzam run ${run.id} (${run.name}): status=${run.status}, conclusion=${run.conclusion}`); +/* -------- Run listing -------- */ +async function tryApiListRuns() { + const pageSize = Math.min(50, SCAN_LIMIT); + const out = []; + let page = 1; - // Sprawdzaj tylko udane buildy - if (run.status === 'completed' && run.conclusion === 'success') { - console.log(`Run ${run.id} zakończony sukcesem, sprawdzam artefakty...`); - - const artifacts = await getRunArtifacts(run.id); - const artifactNames = artifacts.map(a => a.name); - - console.log(`Artefakty w run ${run.id}:`, artifactNames); - - // Sprawdź czy wszystkie wymagane artefakty są dostępne - const hasAllRequiredArtifacts = config.requiredArtifacts.every(required => - artifactNames.includes(required) - ); - - if (hasAllRequiredArtifacts) { - console.log(`✅ Znaleziono odpowiedni run: ${run.id}`); - return run.id; - } else { - const missing = config.requiredArtifacts.filter(req => !artifactNames.includes(req)); - console.log(`❌ Run ${run.id} nie ma wszystkich wymaganych artefaktów. Brakuje: ${missing.join(', ')}`); - } - } + 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; + } + return out.slice(0, SCAN_LIMIT); +} - throw new Error('Nie znaleziono żadnego udanego buildu z wymaganymi artefaktami'); - } catch (error) { - console.error('Błąd:', error.message); +async function listRunsFromHtml() { + const url = `${BASE}/${OWNER}/${REPO}/actions`; + const res = await http(url, "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 })); +} + +/* -------- 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); + } +} + +/* -------- Main -------- */ +(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); } -} -// Wykonanie skryptu -findLatestSuccessfulRunWithArtifacts().then(runId => { - console.log(`Ostatni udany build z artefaktami: ${runId}`); + const candidates = runs + .filter(r => r && r.id != null) + .sort((a, b) => (b.id ?? 0) - (a.id ?? 0)); - // Utworzenie katalogu cache i zapisanie ID - const cacheDir = path.join('.gitea', '.cache'); - if (!fs.existsSync(cacheDir)) { - fs.mkdirSync(cacheDir, { recursive: true }); + console.log(`Scanning ${candidates.length} runs for artifacts: ${REQUIRED_ARTIFACTS.join(", ")}`); + + let pickedId = null; + + for (const r of candidates) { + const runId = r.id; + if (r.status && r.conclusion && !isSuccessfulRun(r)) { + console.log(`Run ${runId}: not successful`); + continue; + } + 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]}`); + } } - fs.writeFileSync(path.join(cacheDir, 'run_id'), runId.toString()); - console.log(`ID zapisane do .gitea/.cache/run_id`); -}).catch(error => { - console.error('Niepowodzenie:', error.message); + if (!pickedId) { + console.error("No run exposes all required artifacts."); + process.exit(1); + } + + 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)); process.exit(1); }); \ No newline at end of file