runneryard 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/runneryard.mjs +115 -0
- package/package.json +31 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
5
|
+
import { homedir, tmpdir } from "node:os";
|
|
6
|
+
import { dirname, join } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { spawn } from "node:child_process";
|
|
9
|
+
|
|
10
|
+
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
11
|
+
const packageData = JSON.parse(await readFile(join(packageRoot, "package.json"), "utf8"));
|
|
12
|
+
const version = packageData.version;
|
|
13
|
+
const repository = process.env.RUNNERYARD_REPOSITORY || "gwendall/runneryard";
|
|
14
|
+
|
|
15
|
+
function targetFor(platform = process.platform, arch = process.arch) {
|
|
16
|
+
const platforms = { darwin: "Darwin", linux: "Linux" };
|
|
17
|
+
const arches = { x64: "x86_64", arm64: "arm64" };
|
|
18
|
+
if (!platforms[platform] || !arches[arch]) {
|
|
19
|
+
throw new Error(`runneryard does not publish a binary for ${platform}/${arch}`);
|
|
20
|
+
}
|
|
21
|
+
return `${platforms[platform]}_${arches[arch]}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function download(url) {
|
|
25
|
+
const response = await fetch(url, { redirect: "follow" });
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
throw new Error(`download failed (${response.status}) from ${url}`);
|
|
28
|
+
}
|
|
29
|
+
return Buffer.from(await response.arrayBuffer());
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function expectedChecksum(checksums, asset) {
|
|
33
|
+
for (const line of checksums.split("\n")) {
|
|
34
|
+
const [checksum, filename] = line.trim().split(/\s+/, 2);
|
|
35
|
+
if (filename?.replace(/^\*/, "") === asset) return checksum;
|
|
36
|
+
}
|
|
37
|
+
throw new Error(`release checksums do not contain ${asset}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function installBinary() {
|
|
41
|
+
if (process.env.RUNNERYARD_BINARY) return process.env.RUNNERYARD_BINARY;
|
|
42
|
+
const target = targetFor();
|
|
43
|
+
const cacheDir = join(homedir(), ".cache", "runneryard", version, target);
|
|
44
|
+
const binary = join(cacheDir, "runneryard");
|
|
45
|
+
try {
|
|
46
|
+
await chmod(binary, 0o755);
|
|
47
|
+
return binary;
|
|
48
|
+
} catch {}
|
|
49
|
+
|
|
50
|
+
await mkdir(cacheDir, { recursive: true });
|
|
51
|
+
const base = `https://github.com/${repository}/releases/download/v${version}`;
|
|
52
|
+
const asset = `runneryard_${version}_${target}.tar.gz`;
|
|
53
|
+
const [archive, sums] = await Promise.all([
|
|
54
|
+
download(`${base}/${asset}`),
|
|
55
|
+
download(`${base}/runneryard_${version}_checksums.txt`),
|
|
56
|
+
]);
|
|
57
|
+
const actual = createHash("sha256").update(archive).digest("hex");
|
|
58
|
+
const expected = expectedChecksum(sums.toString("utf8"), asset);
|
|
59
|
+
if (actual !== expected) throw new Error(`checksum mismatch for ${asset}`);
|
|
60
|
+
|
|
61
|
+
const staging = join(tmpdir(), `runneryard-${process.pid}-${Date.now()}`);
|
|
62
|
+
const archivePath = `${staging}.tar.gz`;
|
|
63
|
+
await writeFile(archivePath, archive);
|
|
64
|
+
await mkdir(staging, { recursive: true });
|
|
65
|
+
await new Promise((resolve, reject) => {
|
|
66
|
+
const child = spawn("tar", ["-xzf", archivePath, "-C", staging], { stdio: "inherit" });
|
|
67
|
+
child.once("error", reject);
|
|
68
|
+
child.once("exit", (code) => (code === 0 ? resolve() : reject(new Error(`tar exited with ${code}`))));
|
|
69
|
+
});
|
|
70
|
+
await chmod(join(staging, "runneryard"), 0o755);
|
|
71
|
+
await rename(join(staging, "runneryard"), binary);
|
|
72
|
+
await rm(staging, { recursive: true, force: true });
|
|
73
|
+
await rm(archivePath, { force: true });
|
|
74
|
+
return binary;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function forwardSignals(child, host = process) {
|
|
78
|
+
const handlers = new Map();
|
|
79
|
+
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
|
|
80
|
+
const handler = () => child.kill(signal);
|
|
81
|
+
handlers.set(signal, handler);
|
|
82
|
+
host.on(signal, handler);
|
|
83
|
+
}
|
|
84
|
+
const cleanup = () => {
|
|
85
|
+
for (const [signal, handler] of handlers) host.off(signal, handler);
|
|
86
|
+
};
|
|
87
|
+
child.once("error", cleanup);
|
|
88
|
+
child.once("exit", cleanup);
|
|
89
|
+
return cleanup;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function main() {
|
|
93
|
+
try {
|
|
94
|
+
const binary = await installBinary();
|
|
95
|
+
const child = spawn(binary, process.argv.slice(2), { stdio: "inherit" });
|
|
96
|
+
forwardSignals(child);
|
|
97
|
+
child.once("error", (error) => {
|
|
98
|
+
console.error(`runneryard: ${error.message}`);
|
|
99
|
+
process.exitCode = 1;
|
|
100
|
+
});
|
|
101
|
+
child.once("exit", (code, signal) => {
|
|
102
|
+
if (signal) process.kill(process.pid, signal);
|
|
103
|
+
else process.exitCode = code ?? 1;
|
|
104
|
+
});
|
|
105
|
+
} catch (error) {
|
|
106
|
+
console.error(`runneryard: ${error.message}`);
|
|
107
|
+
process.exitCode = 1;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
112
|
+
await main();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export { expectedChecksum, forwardSignals, main, targetFor };
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "runneryard",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Run ephemeral GitHub Actions runners on infrastructure you control",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"runneryard": "bin/runneryard.mjs"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=20"
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/gwendall/runneryard.git"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://github.com/gwendall/runneryard#readme",
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/gwendall/runneryard/issues"
|
|
23
|
+
},
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public",
|
|
26
|
+
"provenance": true
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"test": "node --test test/*.test.mjs"
|
|
30
|
+
}
|
|
31
|
+
}
|