@@ -1,125 +1,96 @@
// .gitea/scripts/getLatestRunWithArtifacts.js
// Purpose: Find latest successful run that exposes all REQUIRED_ARTIFACTS via GUI URLs.
// Outputs: sets `run_id` to GITHUB_OUTPUT and writes .gitea/.cache/run_id file.
// Robust finder: uses only Gitea API (no GUI URLs) .
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
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 apiJSON ( url ) {
const res = await fetch ( url , { headers : { Authorization : ` token ${ TOKEN } ` } } ) ;
if ( ! res . ok ) {
const t = await res . text ( ) . catch ( ( ) => "" ) ;
throw new Error ( ` API ${ res . status } for ${ url } \n ${ t } ` ) ;
}
return res . json ( ) ;
}
( async ( ) => {
// --- Config from environment ---
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
const TOKEN = process . env . GITEA _PAT ; // 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 ) ;
}
// Ensure cache dir exists
const cacheDir = path . join ( ".gitea" , ".cache" ) ;
fs . mkdirSync ( cacheDir , { recursive : true } ) ;
// Helpers
const api = async ( url ) => {
const res = await fetch ( url , {
headers : { Authorization : ` token ${ TOKEN } ` }
} ) ;
if ( ! res . ok ) {
const text = await res . text ( ) . catch ( ( ) => "" ) ;
throw new Error ( ` API ${ res . status } ${ res . statusText } for ${ url } \n ${ text } ` ) ;
}
return res . json ( ) ;
} ;
const headOk = async ( url ) => {
// Try HEAD first; some instances may require GET for redirects
let res = await fetch ( url , {
method : "HEAD" ,
redirect : "follow" ,
headers : { Authorization : ` token ${ TOKEN } ` }
} ) ;
if ( res . ok ) return true ;
// Fallback to GET (no download) just to test availability
res = await fetch ( url , {
method : "GET" ,
redirect : "manual" ,
headers : { Authorization : ` token ${ TOKEN } ` }
} ) ;
// Accept 200 OK, or 3xx redirect to a signed download URL
return res . status >= 200 && res . status < 400 ;
} ;
// 1) Get recent workflow runs (a.k.a. tasks) via REST
// 1) Pobierz listę tasków (runs)
const listUrl = ` ${ BASE } /api/v1/repos/ ${ OWNER } / ${ REPO } /actions/tasks?limit= ${ SCAN _LIMIT } ` ;
const resp = await api ( listUrl ) ;
const resp = await apiJSON ( listUrl ) ;
// 2) Build candidate list: only status == "success", newest first by id
const runs = Array . isArray ( resp . workflow _runs ) ? resp . workflow _runs : [ ] ;
const candidates = runs
. filter ( r => r && r . status === "success" )
. sort ( ( a , b ) => ( b . id ? ? 0 ) - ( a . id ? ? 0 ) ) ;
// 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 ) ;
}
if ( ! candidates . length ) {
console . error ( "No successful runs found." ) ;
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 ) )
. sort ( ( a , b ) => ( b . id ? ? 0 ) - ( a . id ? ? 0 ) )
. filter ( isSuccess ) ;
if ( candidates . length === 0 ) {
console . error ( "No successful runs found." ) ; process . exit ( 1 ) ;
}
console . log ( ` Scanning ${ candidates . length } successful runs for artifacts: ${ REQUIRED _ARTIFACTS . join ( ", " ) } ` ) ;
// 3) Find the first run that exposes all required arti facts via GUI URLs
let picked = null ;
// 3) Sprawdź arte fakty przez API
let pickedId = 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 = tru e;
fo r ( 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 ;
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 ? . nam e )
. filte r ( Boolean ) ;
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 via API -> ${ e . message . split ( "\n" ) [ 0 ] } ` ) ;
}
}
if ( ! picked ) {
console . error ( "No run exposes all required artifacts. Consider increasing SCAN_LIMIT or verify artifact names ." ) ;
if ( ! pickedId ) {
console . error ( "No run exposes all required artifacts via API ." ) ;
process . exit ( 1 ) ;
}
// 4) Write outputs
const runIdStr = String ( picked. id ) ;
// Write to cache (handy for debugging)
fs . write FileSync( path . join ( cacheDir , " run_id" ) , runIdStr , "utf8" ) ;
// Export as GitHub-style output (supported by Gitea runners)
const outFile = process . env . GITHUB _OUTPUT ;
if ( outFile ) {
fs . appendFileSync ( outFile , ` run_id= ${ runIdStr } \n ` ) ;
} else {
// Fallback: also print for visibility
console . log ( ` ::set-output name=run_id:: ${ runIdStr } ` ) ;
// 4) Zapisz outputy
fs . writeFileSync ( path . join ( cacheDir , "run_id" ) , String ( pickedId ) , "utf8" ) ;
if ( process . env . GITHUB _OUTPUT ) {
fs . append FileSync( 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 ) ;