release
This commit is contained in:
@@ -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(', ')}`);
|
||||
|
||||
// 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();
|
||||
});
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
// 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 [];
|
||||
}
|
||||
}
|
||||
|
||||
// Główna funkcja
|
||||
async function findLatestSuccessfulRunWithArtifacts() {
|
||||
try {
|
||||
const runs = await getWorkflowRuns();
|
||||
console.log(`Znaleziono ${runs.length} workflow runs`);
|
||||
|
||||
for (const run of runs) {
|
||||
console.log(`Sprawdzam run ${run.id} (${run.name}): status=${run.status}, conclusion=${run.conclusion}`);
|
||||
|
||||
// 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(', ')}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Nie znaleziono żadnego udanego buildu z wymaganymi artefaktami');
|
||||
} catch (error) {
|
||||
console.error('Błąd:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Wykonanie skryptu
|
||||
findLatestSuccessfulRunWithArtifacts().then(runId => {
|
||||
console.log(`Ostatni udany build z artefaktami: ${runId}`);
|
||||
|
||||
// Utworzenie katalogu cache i zapisanie ID
|
||||
const cacheDir = path.join('.gitea', '.cache');
|
||||
if (!fs.existsSync(cacheDir)) {
|
||||
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 },
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
||||
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);
|
||||
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();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
return out.slice(0, SCAN_LIMIT);
|
||||
}
|
||||
|
||||
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+[^>]*>([^<]+)</g))
|
||||
.map(m => 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);
|
||||
}
|
||||
|
||||
const candidates = runs
|
||||
.filter(r => r && r.id != null)
|
||||
.sort((a, b) => (b.id ?? 0) - (a.id ?? 0));
|
||||
|
||||
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]}`);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user