2023-06-26 05:18:41 +02:00
// Main
import * as fs from 'node:fs/promises' ;
import * as os from 'node:os' ;
import * as path from 'node:path' ;
2024-01-09 15:01:39 +01:00
import { spawn , exec } from 'node:child_process' ;
2024-02-27 18:32:36 +01:00
import { openSync , readFileSync } from 'node:fs' ;
2024-01-09 15:01:39 +01:00
import { inspect , promisify } from 'node:util' ;
2024-02-23 16:09:29 +01:00
import * as http from 'http' ;
2023-06-26 05:18:41 +02:00
import * as core from '@actions/core' ;
import { Tail } from 'tail' ;
import got from "got" ;
const ENV_CACHE_DAEMONDIR = 'MAGIC_NIX_CACHE_DAEMONDIR' ;
2023-06-27 18:22:21 +02:00
const gotClient = got . extend ( {
retry : {
2023-12-05 02:12:12 +01:00
limit : 1 ,
2023-06-27 18:22:21 +02:00
methods : [ 'POST' , 'GET' , 'PUT' , 'HEAD' , 'DELETE' , 'OPTIONS' , 'TRACE' ] ,
} ,
hooks : {
beforeRetry : [
( error , retryCount ) = > {
core . info ( ` Retrying after error ${ error . code } , retry #: ${ retryCount } ` ) ;
}
] ,
} ,
} ) ;
2023-06-26 05:18:41 +02:00
function getCacherUrl ( ) : string {
const runnerArch = process . env . RUNNER_ARCH ;
const runnerOs = process . env . RUNNER_OS ;
2024-01-18 17:58:07 +01:00
const binarySuffix = ` ${ runnerArch } - ${ runnerOs } ` ;
2024-01-18 17:36:58 +01:00
const urlPrefix = ` https://install.determinate.systems/magic-nix-cache-priv ` ;
2023-06-26 05:18:41 +02:00
if ( core . getInput ( 'source-url' ) ) {
return core . getInput ( 'source-url' ) ;
}
if ( core . getInput ( 'source-tag' ) ) {
return ` ${ urlPrefix } /tag/ ${ core . getInput ( 'source-tag' ) } / ${ binarySuffix } ` ;
}
if ( core . getInput ( 'source-pr' ) ) {
2024-01-18 17:36:58 +01:00
return ` ${ urlPrefix } /pr/ ${ core . getInput ( 'source-pr' ) } / ${ binarySuffix } ` ;
2023-06-26 05:18:41 +02:00
}
if ( core . getInput ( 'source-branch' ) ) {
return ` ${ urlPrefix } /branch/ ${ core . getInput ( 'source-branch' ) } / ${ binarySuffix } ` ;
}
if ( core . getInput ( 'source-revision' ) ) {
return ` ${ urlPrefix } /rev/ ${ core . getInput ( 'source-revision' ) } / ${ binarySuffix } ` ;
}
return ` ${ urlPrefix } /latest/ ${ binarySuffix } ` ;
}
2024-01-09 15:01:39 +01:00
async function fetchAutoCacher() {
2023-06-26 05:18:41 +02:00
const binary_url = getCacherUrl ( ) ;
2023-12-11 15:29:18 +01:00
core . info ( ` Fetching the Magic Nix Cache from ${ binary_url } ` ) ;
2023-06-26 05:18:41 +02:00
2024-01-18 18:23:19 +01:00
const { stdout } = await promisify ( exec ) ( ` curl -L " ${ binary_url } " | xz -d | nix-store --import ` ) ;
2024-01-09 15:01:39 +01:00
const paths = stdout . split ( os . EOL ) ;
2024-01-09 18:51:52 +01:00
// Since the export is in reverse topologically sorted order, magic-nix-cache is always the penultimate entry in the list (the empty string left by split being the last).
2024-01-09 15:01:39 +01:00
const last_path = paths . at ( - 2 ) ;
return ` ${ last_path } /bin/magic-nix-cache ` ;
2023-06-26 05:18:41 +02:00
}
2024-01-11 16:55:51 +01:00
function tailLog ( daemonDir ) {
const log = new Tail ( path . join ( daemonDir , 'daemon.log' ) ) ;
core . debug ( ` tailing daemon.log... ` ) ;
log . on ( 'line' , ( line ) = > {
core . info ( line ) ;
} ) ;
return log ;
}
2023-06-26 05:18:41 +02:00
async function setUpAutoCache() {
const tmpdir = process . env [ 'RUNNER_TEMP' ] || os . tmpdir ( ) ;
const required_env = [ 'ACTIONS_CACHE_URL' , 'ACTIONS_RUNTIME_URL' , 'ACTIONS_RUNTIME_TOKEN' ] ;
var anyMissing = false ;
for ( const n of required_env ) {
if ( ! process . env . hasOwnProperty ( n ) ) {
anyMissing = true ;
core . warning ( ` Disabling automatic caching since required environment ${ n } isn't available ` ) ;
}
}
if ( anyMissing ) {
return ;
}
core . debug ( ` GitHub Action Cache URL: ${ process . env [ 'ACTIONS_CACHE_URL' ] } ` ) ;
const daemonDir = await fs . mkdtemp ( path . join ( tmpdir , 'magic-nix-cache-' ) ) ;
2023-12-11 15:29:18 +01:00
var daemonBin : string ;
if ( core . getInput ( 'source-binary' ) ) {
2023-06-26 05:18:41 +02:00
daemonBin = core . getInput ( 'source-binary' ) ;
} else {
2024-01-09 15:01:39 +01:00
daemonBin = await fetchAutoCacher ( ) ;
2023-06-26 05:18:41 +02:00
}
var runEnv ;
if ( core . isDebug ( ) ) {
runEnv = {
2023-06-26 18:27:45 +02:00
RUST_LOG : "trace,magic_nix_cache=debug,gha_cache=debug" ,
2023-06-26 05:18:41 +02:00
RUST_BACKTRACE : "full" ,
. . . process . env
} ;
} else {
runEnv = process . env ;
}
2024-02-23 16:09:29 +01:00
const notifyPort = core . getInput ( 'startup-notification-port' ) ;
const notifyPromise = new Promise < Promise < void > > ( ( resolveListening ) = > {
const promise = new Promise < void > ( async ( resolveQuit ) = > {
const notifyServer = http . createServer ( ( req , res ) = > {
if ( req . method === 'POST' && req . url === '/' ) {
core . debug ( ` Notify server shutting down. ` ) ;
res . writeHead ( 200 , { 'Content-Type' : 'application/json' } ) ;
res . end ( '{}' ) ;
notifyServer . close ( ( ) = > {
resolveQuit ( ) ;
} ) ;
}
} ) ;
notifyServer . listen ( notifyPort , ( ) = > {
core . debug ( ` Notify server running. ` ) ;
resolveListening ( promise ) ;
} ) ;
} ) ;
} ) ;
2024-02-23 19:51:28 +01:00
// Start tailing the daemon log.
2023-12-14 15:58:27 +01:00
const outputPath = ` ${ daemonDir } /daemon.log ` ;
2023-12-04 22:47:15 +01:00
const output = openSync ( outputPath , 'a' ) ;
2024-01-11 16:55:51 +01:00
const log = tailLog ( daemonDir ) ;
2024-02-12 23:13:43 +01:00
const netrc = await netrcPath ( ) ;
2024-02-23 19:51:28 +01:00
// Start the server. Once it is ready, it will notify us via the notification server.
2023-12-14 15:58:27 +01:00
const daemon = spawn (
2023-06-26 05:18:41 +02:00
daemonBin ,
[
2024-02-23 16:09:29 +01:00
'--startup-notification-url' , ` http://127.0.0.1: ${ notifyPort } ` ,
2023-06-26 05:18:41 +02:00
'--listen' , core . getInput ( 'listen' ) ,
'--upstream' , core . getInput ( 'upstream-cache' ) ,
2023-12-04 22:47:15 +01:00
'--diagnostic-endpoint' , core . getInput ( 'diagnostic-endpoint' ) ,
'--nix-conf' , ` ${ process . env [ "HOME" ] } /.config/nix/nix.conf `
] . concat (
core . getInput ( 'use-flakehub' ) === 'true' ? [
'--use-flakehub' ,
2023-12-11 13:46:39 +01:00
'--flakehub-cache-server' , core . getInput ( 'flakehub-cache-server' ) ,
2023-12-04 22:47:15 +01:00
'--flakehub-api-server' , core . getInput ( 'flakehub-api-server' ) ,
2024-02-12 23:13:43 +01:00
'--flakehub-api-server-netrc' , netrc ,
2024-02-27 18:24:08 +01:00
'--flakehub-flake-name' , core . getInput ( 'flakehub-flake-name' ) ,
2023-12-04 22:47:15 +01:00
] : [ ] ) . concat (
core . getInput ( 'use-gha-cache' ) === 'true' ? [
'--use-gha-cache'
] : [ ] ) ,
2023-06-26 05:18:41 +02:00
{
2024-02-23 16:09:29 +01:00
stdio : [ 'ignore' , output , output ] ,
2023-12-14 15:58:27 +01:00
env : runEnv ,
detached : true
2023-06-26 05:18:41 +02:00
}
) ;
2023-12-14 15:58:27 +01:00
const pidFile = path . join ( daemonDir , 'daemon.pid' ) ;
await fs . writeFile ( pidFile , ` ${ daemon . pid } ` ) ;
2024-02-23 16:09:29 +01:00
core . info ( "Waiting for magic-nix-cache to start..." ) ;
2023-06-26 05:18:41 +02:00
await new Promise < void > ( ( resolve , reject ) = > {
2024-02-23 16:09:29 +01:00
notifyPromise . then ( ( value ) = > {
resolve ( ) ;
2023-12-14 15:58:27 +01:00
} ) ;
daemon . on ( 'exit' , async ( code , signal ) = > {
2023-06-26 05:18:41 +02:00
if ( signal ) {
2024-01-11 16:55:51 +01:00
reject ( new Error ( ` Daemon was killed by signal ${ signal } ` ) ) ;
2023-06-26 05:18:41 +02:00
} else if ( code ) {
2024-01-11 16:55:51 +01:00
reject ( new Error ( ` Daemon exited with code ${ code } ` ) ) ;
2023-06-26 05:18:41 +02:00
} else {
2024-01-11 16:55:51 +01:00
reject ( new Error ( ` Daemon unexpectedly exited ` ) ) ;
2023-06-26 05:18:41 +02:00
}
} ) ;
} ) ;
2023-12-14 15:58:27 +01:00
daemon . unref ( ) ;
2023-12-04 22:47:15 +01:00
core . info ( 'Launched Magic Nix Cache' ) ;
2023-06-26 05:18:41 +02:00
core . exportVariable ( ENV_CACHE_DAEMONDIR , daemonDir ) ;
2024-01-11 16:55:51 +01:00
log . unwatch ( ) ;
2023-06-26 05:18:41 +02:00
}
async function notifyAutoCache() {
const daemonDir = process . env [ ENV_CACHE_DAEMONDIR ] ;
if ( ! daemonDir ) {
return ;
}
2023-06-27 18:22:21 +02:00
try {
core . debug ( ` Indicating workflow start ` ) ;
const res : any = await gotClient . post ( ` http:// ${ core . getInput ( 'listen' ) } /api/workflow-start ` ) . json ( ) ;
core . debug ( ` back from post ` ) ;
core . debug ( res ) ;
} catch ( e ) {
core . info ( ` Error marking the workflow as started: ` ) ;
core . info ( inspect ( e ) ) ;
core . info ( ` Magic Nix Cache may not be running for this workflow. ` ) ;
}
2023-06-26 05:18:41 +02:00
}
2024-02-12 23:13:43 +01:00
async function netrcPath() {
const expectedNetrcPath = path . join ( process . env [ 'RUNNER_TEMP' ] , 'determinate-nix-installer-netrc' )
try {
await fs . access ( expectedNetrcPath )
return expectedNetrcPath ;
} catch {
// `nix-installer` was not used, the user may be registered with FlakeHub though.
const destinedNetrcPath = path . join ( process . env [ 'RUNNER_TEMP' ] , 'magic-nix-cache-netrc' )
try {
await flakehub_login ( destinedNetrcPath ) ;
2024-02-13 20:40:27 +01:00
} catch ( e ) {
core . info ( "FlakeHub cache disabled." ) ;
core . debug ( ` Error while logging into FlakeHub: ${ e } ` )
2024-02-12 23:13:43 +01:00
}
return destinedNetrcPath ;
}
}
async function flakehub_login ( netrc : string ) {
const jwt = await core . getIDToken ( "api.flakehub.com" ) ;
await fs . writeFile (
netrc ,
[
` machine api.flakehub.com login flakehub password ${ jwt } ` ,
` machine flakehub.com login flakehub password ${ jwt } ` ,
] . join ( "\n" ) ,
) ;
2024-02-13 20:40:27 +01:00
core . info ( "Logged in to FlakeHub." ) ;
2024-02-12 23:13:43 +01:00
}
2023-06-26 05:18:41 +02:00
async function tearDownAutoCache() {
const daemonDir = process . env [ ENV_CACHE_DAEMONDIR ] ;
if ( ! daemonDir ) {
core . debug ( 'magic-nix-cache not started - Skipping' ) ;
return ;
}
const pidFile = path . join ( daemonDir , 'daemon.pid' ) ;
const pid = parseInt ( await fs . readFile ( pidFile , { encoding : 'ascii' } ) ) ;
core . debug ( ` found daemon pid: ${ pid } ` ) ;
if ( ! pid ) {
throw new Error ( "magic-nix-cache did not start successfully" ) ;
}
2024-01-11 16:55:51 +01:00
const log = tailLog ( daemonDir ) ;
2023-06-26 05:18:41 +02:00
try {
core . debug ( ` about to post to localhost ` ) ;
2023-06-27 18:22:21 +02:00
const res : any = await gotClient . post ( ` http:// ${ core . getInput ( 'listen' ) } /api/workflow-finish ` ) . json ( ) ;
2023-06-26 05:18:41 +02:00
core . debug ( ` back from post ` ) ;
core . debug ( res ) ;
} finally {
core . debug ( ` unwatching the daemon log ` ) ;
log . unwatch ( ) ;
}
core . debug ( ` killing ` ) ;
try {
process . kill ( pid , 'SIGTERM' ) ;
} catch ( e ) {
if ( e . code !== 'ESRCH' ) {
throw e ;
}
2023-12-05 02:30:23 +01:00
} finally {
if ( core . isDebug ( ) ) {
core . info ( "Entire log:" ) ;
const log = readFileSync ( path . join ( daemonDir , 'daemon.log' ) ) ;
core . info ( log . toString ( ) ) ;
}
2023-06-26 05:18:41 +02:00
}
}
const isPost = ! ! process . env [ 'STATE_isPost' ] ;
try {
if ( ! isPost ) {
core . saveState ( 'isPost' , 'true' ) ;
await setUpAutoCache ( ) ;
await notifyAutoCache ( ) ;
} else {
await tearDownAutoCache ( ) ;
}
} catch ( e ) {
core . info ( ` got an exception: ` ) ;
core . info ( e ) ;
if ( ! isPost ) {
core . setFailed ( e . message ) ;
throw e ;
} else {
core . info ( "not considering this a failure: finishing the upload is optional, anyway." ) ;
process . exit ( ) ;
} }
core . debug ( ` rip ` ) ;