magic-nix-cache-action/src/index.ts

322 lines
8.9 KiB
TypeScript
Raw Normal View History

// Main
import * as fs from 'node:fs/promises';
import * as os from 'node:os';
import * as path from 'node:path';
import { spawn, exec } 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';
import got from "got";
const ENV_CACHE_DAEMONDIR = 'MAGIC_NIX_CACHE_DAEMONDIR';
const gotClient = got.extend({
retry: {
2023-12-05 02:12:12 +01:00
limit: 1,
methods: [ 'POST', 'GET', 'PUT', 'HEAD', 'DELETE', 'OPTIONS', 'TRACE' ],
},
hooks: {
beforeRetry: [
(error, retryCount) => {
core.info(`Retrying after error ${error.code}, retry #: ${retryCount}`);
}
],
},
});
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`;
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}`;
}
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}`;
}
async function fetchAutoCacher() {
const binary_url = getCacherUrl();
core.info(`Fetching the Magic Nix Cache from ${binary_url}`);
2024-01-18 18:23:19 +01:00
const { stdout } = await promisify(exec)(`curl -L "${binary_url}" | xz -d | nix-store --import`);
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).
const last_path = paths.at(-2);
return `${last_path}/bin/magic-nix-cache`;
}
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;
}
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-'));
var daemonBin: string;
if (core.getInput('source-binary')) {
daemonBin = core.getInput('source-binary');
} else {
daemonBin = await fetchAutoCacher();
}
var runEnv;
if (core.isDebug()) {
runEnv = {
2023-06-26 18:27:45 +02:00
RUST_LOG: "trace,magic_nix_cache=debug,gha_cache=debug",
RUST_BACKTRACE: "full",
...process.env
};
} else {
runEnv = process.env;
}
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.
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);
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.
const daemon = spawn(
daemonBin,
[
'--startup-notification-url', `http://127.0.0.1:${notifyPort}`,
'--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'),
'--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'
] : []),
{
stdio: ['ignore', output, output],
env: runEnv,
detached: true
}
);
const pidFile = path.join(daemonDir, 'daemon.pid');
await fs.writeFile(pidFile, `${daemon.pid}`);
core.info("Waiting for magic-nix-cache to start...");
await new Promise<void>((resolve, reject) => {
notifyPromise.then((value) => {
resolve();
});
daemon.on('exit', async (code, signal) => {
if (signal) {
2024-01-11 16:55:51 +01:00
reject(new Error(`Daemon was killed by signal ${signal}`));
} else if (code) {
2024-01-11 16:55:51 +01:00
reject(new Error(`Daemon exited with code ${code}`));
} else {
2024-01-11 16:55:51 +01:00
reject(new Error(`Daemon unexpectedly exited`));
}
});
});
daemon.unref();
2023-12-04 22:47:15 +01:00
core.info('Launched Magic Nix Cache');
core.exportVariable(ENV_CACHE_DAEMONDIR, daemonDir);
2024-01-11 16:55:51 +01:00
log.unwatch();
}
async function notifyAutoCache() {
const daemonDir = process.env[ENV_CACHE_DAEMONDIR];
if (!daemonDir) {
return;
}
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.`);
}
}
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}`)
}
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.");
}
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);
try {
core.debug(`about to post to localhost`);
const res: any = await gotClient.post(`http://${core.getInput('listen')}/api/workflow-finish`).json();
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());
}
}
}
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`);