2024-04-12 01:21:15 +02:00
import * as fs from "node:fs/promises" ;
import * as os from "node:os" ;
import * as path from "node:path" ;
import { spawn , exec , SpawnOptions } from "node:child_process" ;
import { openSync , readFileSync } from "node:fs" ;
import { inspect , promisify } from "node:util" ;
import * as http from "http" ;
import * as core from "@actions/core" ;
import { Tail } from "tail" ;
2023-06-26 05:18:41 +02:00
import got from "got" ;
2024-04-12 01:21:15 +02:00
import { IdsToolbox } from "detsys-ts" ;
2023-06-26 05:18:41 +02:00
2024-04-12 01:21:15 +02:00
const ENV_CACHE_DAEMONDIR = "MAGIC_NIX_CACHE_DAEMONDIR" ;
2023-06-26 05:18:41 +02:00
2023-06-27 18:22:21 +02:00
const gotClient = got . extend ( {
retry : {
2023-12-05 02:12:12 +01:00
limit : 1 ,
2024-04-12 01:21:15 +02:00
methods : [ "POST" , "GET" , "PUT" , "HEAD" , "DELETE" , "OPTIONS" , "TRACE" ] ,
2023-06-27 18:22:21 +02:00
} ,
hooks : {
beforeRetry : [
( error , retryCount ) = > {
core . info ( ` Retrying after error ${ error . code } , retry #: ${ retryCount } ` ) ;
2024-04-12 01:21:15 +02:00
} ,
2023-06-27 18:22:21 +02:00
] ,
} ,
} ) ;
2024-04-12 01:21:15 +02:00
async function fetchAutoCacher ( toolbox : IdsToolbox ) : Promise < string > {
const closurePath = await toolbox . fetch ( ) ;
toolbox . recordEvent ( "load_closure" ) ;
const { stdout } = await promisify ( exec ) (
` cat " ${ closurePath } " | 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-04-12 01:21:15 +02:00
function tailLog ( daemonDir : string ) : Tail {
const log = new Tail ( path . join ( daemonDir , "daemon.log" ) ) ;
2024-01-11 16:55:51 +01:00
core . debug ( ` tailing daemon.log... ` ) ;
2024-04-12 01:21:15 +02:00
log . on ( "line" , ( line ) = > {
2024-01-11 16:55:51 +01:00
core . info ( line ) ;
} ) ;
return log ;
}
2024-04-12 01:21:15 +02:00
async function setUpAutoCache ( toolbox : IdsToolbox ) : Promise < void > {
const tmpdir = process . env [ "RUNNER_TEMP" ] || os . tmpdir ( ) ;
const required_env = [
"ACTIONS_CACHE_URL" ,
"ACTIONS_RUNTIME_URL" ,
"ACTIONS_RUNTIME_TOKEN" ,
] ;
2023-06-26 05:18:41 +02:00
2024-04-12 01:21:15 +02:00
let anyMissing = false ;
2023-06-26 05:18:41 +02:00
for ( const n of required_env ) {
if ( ! process . env . hasOwnProperty ( n ) ) {
anyMissing = true ;
2024-04-12 01:21:15 +02:00
core . warning (
` Disabling automatic caching since required environment ${ n } isn't available ` ,
) ;
2023-06-26 05:18:41 +02:00
}
}
if ( anyMissing ) {
return ;
}
2024-04-12 01:21:15 +02:00
core . debug ( ` GitHub Action Cache URL: ${ process . env [ "ACTIONS_CACHE_URL" ] } ` ) ;
2023-06-26 05:18:41 +02:00
2024-04-12 01:21:15 +02:00
const daemonDir = await fs . mkdtemp ( path . join ( tmpdir , "magic-nix-cache-" ) ) ;
2023-06-26 05:18:41 +02:00
2024-04-12 01:21:15 +02:00
let daemonBin : string ;
if ( core . getInput ( "source-binary" ) ) {
daemonBin = core . getInput ( "source-binary" ) ;
2023-06-26 05:18:41 +02:00
} else {
2024-04-12 01:21:15 +02:00
daemonBin = await fetchAutoCacher ( toolbox ) ;
2023-06-26 05:18:41 +02:00
}
2024-04-12 01:21:15 +02:00
let runEnv ;
2023-06-26 05:18:41 +02:00
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" ,
2024-04-12 01:21:15 +02:00
. . . process . env ,
2023-06-26 05:18:41 +02:00
} ;
} else {
runEnv = process . env ;
}
2024-04-12 01:21:15 +02:00
const notifyPort = core . getInput ( "startup-notification-port" ) ;
2024-02-23 16:09:29 +01:00
const notifyPromise = new Promise < Promise < void > > ( ( resolveListening ) = > {
const promise = new Promise < void > ( async ( resolveQuit ) = > {
const notifyServer = http . createServer ( ( req , res ) = > {
2024-04-12 01:21:15 +02:00
if ( req . method === "POST" && req . url === "/" ) {
2024-02-23 16:09:29 +01:00
core . debug ( ` Notify server shutting down. ` ) ;
2024-04-12 01:21:15 +02:00
res . writeHead ( 200 , { "Content-Type" : "application/json" } ) ;
res . end ( "{}" ) ;
2024-02-23 16:09:29 +01:00
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 ` ;
2024-04-12 01:21:15 +02: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-03-25 21:42:40 +01:00
const nixConfPath = ` ${ process . env [ "HOME" ] } /.config/nix/nix.conf ` ;
const daemonCliFlags : string [ ] = [
2024-04-12 01:21:15 +02:00
"--startup-notification-url" ,
` http://127.0.0.1: ${ notifyPort } ` ,
"--listen" ,
core . getInput ( "listen" ) ,
"--upstream" ,
core . getInput ( "upstream-cache" ) ,
"--diagnostic-endpoint" ,
core . getInput ( "diagnostic-endpoint" ) ,
"--nix-conf" ,
nixConfPath ,
]
. concat (
core . getBooleanInput ( "use-flakehub" )
? [
"--use-flakehub" ,
"--flakehub-cache-server" ,
core . getInput ( "flakehub-cache-server" ) ,
"--flakehub-api-server" ,
core . getInput ( "flakehub-api-server" ) ,
"--flakehub-api-server-netrc" ,
netrc ,
"--flakehub-flake-name" ,
core . getInput ( "flakehub-flake-name" ) ,
]
: [ ] ,
)
. concat ( core . getBooleanInput ( "use-gha-cache" ) ? [ "--use-gha-cache" ] : [ ] ) ;
2024-03-25 21:42:40 +01:00
const opts : SpawnOptions = {
2024-04-12 01:21:15 +02:00
stdio : [ "ignore" , output , output ] ,
2024-03-25 21:42:40 +01:00
env : runEnv ,
2024-04-12 01:21:15 +02:00
detached : true ,
} ;
2024-03-25 21:42:40 +01:00
// Display the final command for debugging purposes
core . debug ( "Full daemon start command:" ) ;
core . debug ( ` ${ daemonBin } ${ daemonCliFlags . join ( " " ) } ` ) ;
2024-02-12 23:13:43 +01:00
2024-02-23 19:51:28 +01:00
// Start the server. Once it is ready, it will notify us via the notification server.
2024-03-25 21:42:40 +01:00
const daemon = spawn ( daemonBin , daemonCliFlags , opts ) ;
2023-06-26 05:18:41 +02:00
2024-04-12 01:21:15 +02:00
const pidFile = path . join ( daemonDir , "daemon.pid" ) ;
2023-12-14 15:58:27 +01:00
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-04-12 01:21:15 +02:00
notifyPromise
// eslint-disable-next-line github/no-then
. then ( ( _value ) = > {
resolve ( ) ;
} )
// eslint-disable-next-line github/no-then
. catch ( ( err ) = > {
reject ( new Error ( ` error in notifyPromise: ${ err } ` ) ) ;
} ) ;
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 ( ) ;
2024-04-12 01:21:15 +02: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
}
2024-04-12 01:21:15 +02:00
async function notifyAutoCache ( ) : Promise < void > {
2023-06-26 05:18:41 +02:00
const daemonDir = process . env [ ENV_CACHE_DAEMONDIR ] ;
if ( ! daemonDir ) {
return ;
}
2023-06-27 18:22:21 +02:00
try {
core . debug ( ` Indicating workflow start ` ) ;
2024-04-12 01:21:15 +02:00
const res : Response = await gotClient
. post ( ` http:// ${ core . getInput ( "listen" ) } /api/workflow-start ` )
. json ( ) ;
core . debug ( ` back from post: ${ res } ` ) ;
2023-06-27 18:22:21 +02:00
} 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-04-12 01:21:15 +02:00
async function netrcPath ( ) : Promise < string > {
const expectedNetrcPath = path . join (
process . env [ "RUNNER_TEMP" ] || os . tmpdir ( ) ,
"determinate-nix-installer-netrc" ,
) ;
2024-02-12 23:13:43 +01:00
try {
2024-04-12 01:21:15 +02:00
await fs . access ( expectedNetrcPath ) ;
2024-02-12 23:13:43 +01:00
return expectedNetrcPath ;
} catch {
// `nix-installer` was not used, the user may be registered with FlakeHub though.
2024-04-12 01:21:15 +02:00
const destinedNetrcPath = path . join (
process . env [ "RUNNER_TEMP" ] || os . tmpdir ( ) ,
"magic-nix-cache-netrc" ,
) ;
2024-02-12 23:13:43 +01:00
try {
await flakehub_login ( destinedNetrcPath ) ;
2024-02-13 20:40:27 +01:00
} catch ( e ) {
core . info ( "FlakeHub cache disabled." ) ;
2024-04-12 01:21:15 +02:00
core . debug ( ` Error while logging into FlakeHub: ${ e } ` ) ;
2024-02-12 23:13:43 +01:00
}
return destinedNetrcPath ;
}
}
2024-04-12 01:21:15 +02:00
async function flakehub_login ( netrc : string ) : Promise < void > {
2024-02-12 23:13:43 +01:00
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 } ` ,
2024-02-27 18:42:14 +01:00
` machine cache.flakehub.com login flakehub password ${ jwt } ` ,
2024-02-12 23:13:43 +01:00
] . join ( "\n" ) ,
) ;
2024-02-13 20:40:27 +01:00
core . info ( "Logged in to FlakeHub." ) ;
2024-02-12 23:13:43 +01:00
}
2024-04-12 01:21:15 +02:00
async function tearDownAutoCache ( ) : Promise < void > {
2023-06-26 05:18:41 +02:00
const daemonDir = process . env [ ENV_CACHE_DAEMONDIR ] ;
if ( ! daemonDir ) {
2024-04-12 01:21:15 +02:00
core . debug ( "magic-nix-cache not started - Skipping" ) ;
2023-06-26 05:18:41 +02:00
return ;
}
2024-04-12 01:21:15 +02:00
const pidFile = path . join ( daemonDir , "daemon.pid" ) ;
const pid = parseInt ( await fs . readFile ( pidFile , { encoding : "ascii" } ) ) ;
2023-06-26 05:18:41 +02:00
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 ` ) ;
2024-04-12 01:21:15 +02:00
const res : Response = await gotClient
. post ( ` http:// ${ core . getInput ( "listen" ) } /api/workflow-finish ` )
. json ( ) ;
core . debug ( ` back from post: ${ res } ` ) ;
2023-06-26 05:18:41 +02:00
} finally {
core . debug ( ` unwatching the daemon log ` ) ;
log . unwatch ( ) ;
}
core . debug ( ` killing ` ) ;
try {
2024-04-12 01:21:15 +02:00
process . kill ( pid , "SIGTERM" ) ;
2023-06-26 05:18:41 +02:00
} catch ( e ) {
2024-04-12 01:21:15 +02:00
if ( typeof e === "object" && e && "code" in e && e . code !== "ESRCH" ) {
2023-06-26 05:18:41 +02:00
throw e ;
}
2023-12-05 02:30:23 +01:00
} finally {
if ( core . isDebug ( ) ) {
core . info ( "Entire log:" ) ;
2024-04-12 01:21:15 +02:00
const entireLog = readFileSync ( path . join ( daemonDir , "daemon.log" ) ) ;
core . info ( entireLog . toString ( ) ) ;
2023-12-05 02:30:23 +01:00
}
2023-06-26 05:18:41 +02:00
}
}
2024-04-12 01:21:15 +02:00
const idslib = new IdsToolbox ( {
name : "magic-nix-cache" ,
fetchStyle : "gh-env-style" ,
2024-04-12 01:25:26 +02:00
idsProjectName : "magic-nix-cache-closure" ,
2024-04-18 20:14:44 +02:00
requireNix : "warn" ,
2024-04-12 01:21:15 +02:00
} ) ;
2023-06-26 05:18:41 +02:00
2024-04-12 01:21:15 +02:00
idslib . onMain ( async ( ) = > {
await setUpAutoCache ( idslib ) ;
await notifyAutoCache ( ) ;
} ) ;
idslib . onPost ( async ( ) = > {
await tearDownAutoCache ( ) ;
} ) ;
2023-06-26 05:18:41 +02:00
2024-04-12 01:21:15 +02:00
idslib . execute ( ) ;