Files

3031 lines
75 KiB
JavaScript
Raw Permalink Normal View History

2026-08-27 21:09:14 +00:00
// Contains utility functions for various scripts, including:
// CI, running tests, and code generation.
import { spawn as nodeSpawn, spawnSync as nodeSpawnSync } from "node:child_process";
import { createHash, createHmac } from "node:crypto";
import {
appendFileSync,
chmodSync,
copyFileSync,
existsSync,
mkdirSync,
mkdtempSync,
readdirSync,
readFileSync,
writeFileSync,
} from "node:fs";
import { connect } from "node:net";
import { hostname, homedir as nodeHomedir, tmpdir as nodeTmpdir, release, userInfo } from "node:os";
import { basename, dirname, join, relative, resolve } from "node:path";
import { normalize as normalizeWindows } from "node:path/win32";
export const isWindows = process.platform === "win32";
export const isMacOS = process.platform === "darwin";
// Node built for Termux/bionic reports "android"; CI models that as linux + abi=android.
export const isAndroid = process.platform === "android";
export const isLinux = process.platform === "linux" || isAndroid;
export const isFreeBSD = process.platform === "freebsd";
export const isPosix = isMacOS || isLinux || isFreeBSD;
export const isX64 = process.arch === "x64";
/**
* @param {string} name
* @param {boolean} [required]
* @returns {string}
*/
export function getEnv(name, required = true) {
const value = process.env[name];
if (required && !value) {
throw new Error(`Environment variable is missing: ${name}`);
}
return value;
}
export const isBuildkite = getEnv("BUILDKITE", false) === "true";
export const isGithubAction = getEnv("GITHUB_ACTIONS", false) === "true";
export const isCI = getEnv("CI", false) === "true" || isBuildkite || isGithubAction;
export const isDebug = getEnv("DEBUG", false) === "1";
/**
* @param {string} name
* @param {object} [options]
* @param {boolean} [options.required]
* @param {boolean} [options.redact]
* @returns {string}
*/
export function getSecret(name, options = { required: true, redact: true }) {
const value = getEnv(name, false);
if (value) {
return value;
}
if (isBuildkite) {
const command = ["buildkite-agent", "secret", "get", name];
if (options["redact"] === false) {
command.push("--skip-redaction");
}
const { error, stdout } = spawnSync(command);
const secret = stdout.trim();
if (error || !secret) {
const orgId = getEnv("BUILDKITE_ORGANIZATION_SLUG", false);
const clusterId = getEnv("BUILDKITE_CLUSTER_ID", false);
let hint;
if (orgId && clusterId) {
hint = `https://buildkite.com/organizations/${orgId}/clusters/${clusterId}/secrets`;
} else {
hint = "https://buildkite.com/docs/pipelines/buildkite-secrets";
}
throw new Error(`Secret not found: ${name} (hint: go to ${hint} and create a secret)`, { cause: error });
}
setEnv(name, secret);
return secret;
}
return getEnv(name, options["required"]);
}
/**
* @param {...unknown} args
*/
export function debugLog(...args) {
if (isDebug) {
console.log(...args);
}
}
/**
* @param {string} name
* @param {string | undefined} value
*/
export function setEnv(name, value) {
process.env[name] = value;
if (isGithubAction && !/^GITHUB_/i.test(name)) {
const envFilePath = process.env["GITHUB_ENV"];
if (envFilePath) {
const delimeter = Math.random().toString(36).substring(2, 15);
const content = `${name}<<${delimeter}\n${value}\n${delimeter}\n`;
appendFileSync(outputPath, content);
}
}
}
/**
* @typedef {object} SpawnOptions
* @property {string} [cwd]
* @property {number} [timeout]
* @property {Record<string, string | undefined>} [env]
* @property {boolean | ((error: Error) => boolean)} [throwOnError]
* @property {(error: Error) => boolean} [retryOnError]
* @property {string} [stdin]
* @property {boolean} [privileged]
*/
/**
* @typedef {object} SpawnResult
* @property {number} exitCode
* @property {number} [signalCode]
* @property {string} stdout
* @property {string} stderr
* @property {Error} [error]
*/
/**
* @param {TemplateStringsArray} strings
* @param {...any} values
* @returns {string[]}
*/
export function $(strings, ...values) {
const result = [];
for (let i = 0; i < strings.length; i++) {
result.push(...strings[i].trim().split(/\s+/).filter(Boolean));
if (i < values.length) {
const value = values[i];
if (Array.isArray(value)) {
result.push(...value);
} else if (typeof value === "string") {
if (result.at(-1)?.endsWith("=")) {
result[result.length - 1] += value;
} else {
result.push(value);
}
}
}
}
return result;
}
/**
* @param {string[]} command
* @param {SpawnOptions} options
*/
function parseCommand(command, options) {
if (options?.privileged) {
return [...getPrivilegedCommand(), ...command];
}
return command;
}
/** @type {string[] | undefined} */
let priviledgedCommand;
/**
* @returns {string[]}
*/
function getPrivilegedCommand() {
if (typeof priviledgedCommand !== "undefined") {
return priviledgedCommand;
}
if (isWindows) {
return (priviledgedCommand = []);
}
const sudo = ["sudo", "-n"];
const { error: sudoError } = spawnSync([...sudo, "true"]);
if (!sudoError) {
return (priviledgedCommand = sudo);
}
const su = ["su", "-s", "sh", "root", "-c"];
const { error: suError } = spawnSync([...su, "true"]);
if (!suError) {
return (priviledgedCommand = su);
}
const doas = ["doas", "-u", "root"];
const { error: doasError } = spawnSync([...doas, "true"]);
if (!doasError) {
return (priviledgedCommand = doas);
}
return (priviledgedCommand = []);
}
/** @type {boolean | undefined} */
let privileged;
/**
* @returns {boolean}
*/
export function isPrivileged() {
if (typeof privileged !== "undefined") {
return privileged;
}
const command = getPrivilegedCommand();
if (command.length) {
const { error } = spawnSync(command);
privileged = !error;
} else {
privileged = false;
}
return privileged;
}
/**
* @param {string[]} command
* @param {SpawnOptions} options
* @returns {Promise<SpawnResult>}
*/
export async function spawn(command, options = {}) {
const [cmd, ...args] = parseCommand(command, options);
debugLog("$", cmd, ...args);
const stdin = options["stdin"];
const spawnOptions = {
cwd: options["cwd"] ?? process.cwd(),
timeout: options["timeout"] ?? undefined,
env: options["env"] ?? undefined,
stdio: stdin === "inherit" ? "inherit" : [stdin ? "pipe" : "ignore", "pipe", "pipe"],
...options,
};
let exitCode = 1;
let signalCode;
let stdout = "";
let stderr = "";
let error;
const result = new Promise((resolve, reject) => {
const subprocess = nodeSpawn(cmd, args, spawnOptions);
if (typeof stdin !== "undefined") {
subprocess.stdin?.on("error", error => {
if (error.code !== "EPIPE") {
reject(error);
}
});
subprocess.stdin?.write(stdin);
subprocess.stdin?.end();
}
subprocess.stdout?.on("data", chunk => {
stdout += chunk;
});
subprocess.stderr?.on("data", chunk => {
stderr += chunk;
});
subprocess.on("error", error => reject(error));
subprocess.on("exit", (code, signal) => {
exitCode = code;
signalCode = signal;
resolve();
});
});
try {
await result;
} catch (cause) {
error = cause;
}
if (exitCode !== 0 && isWindows) {
const exitReason = getWindowsExitReason(exitCode);
if (exitReason) {
signalCode = exitReason;
}
}
if (error || signalCode || exitCode !== 0) {
const description = command.map(arg => (arg.includes(" ") ? `"${arg.replace(/"/g, '\\"')}"` : arg)).join(" ");
const cause = error || stderr.trim() || stdout.trim() || undefined;
if (signalCode) {
error = new Error(`Command killed with ${signalCode}: ${description}`, { cause });
} else {
error = new Error(`Command exited with code ${exitCode}: ${description}`, { cause });
}
}
if (error) {
const retryOnError = options["retryOnError"];
if (typeof retryOnError === "function") {
if (retryOnError(error)) {
return spawn(command, options);
}
}
const throwOnError = options["throwOnError"];
if (typeof throwOnError === "function") {
if (throwOnError(error)) {
throw error;
}
} else if (throwOnError) {
throw error;
}
}
return {
exitCode,
signalCode,
stdout,
stderr,
error,
};
}
/**
* @param {string[]} command
* @param {SpawnOptions} options
* @returns {Promise<SpawnResult>}
*/
export async function spawnSafe(command, options = {}) {
return spawn(command, { throwOnError: true, ...options });
}
/**
* @param {string[]} command
* @param {SpawnOptions} options
* @returns {SpawnResult}
*/
export function spawnSync(command, options = {}) {
const [cmd, ...args] = parseCommand(command, options);
debugLog("$", cmd, ...args);
const stdin = options["stdin"];
const spawnOptions = {
cwd: options["cwd"] ?? process.cwd(),
timeout: options["timeout"] ?? undefined,
env: options["env"] ?? undefined,
stdio: stdin === "inherit" ? "inherit" : [typeof stdin === "undefined" ? "ignore" : "pipe", "pipe", "pipe"],
input: stdin,
...options,
};
let exitCode = 1;
let signalCode;
let stdout = "";
let stderr = "";
let error;
let result;
try {
result = nodeSpawnSync(cmd, args, spawnOptions);
} catch (error) {
result = { error };
}
const { error: spawnError, status, signal, stdout: stdoutBuffer, stderr: stderrBuffer } = result;
if (spawnError) {
error = spawnError;
} else {
exitCode = status ?? 1;
signalCode = signal || undefined;
stdout = stdoutBuffer?.toString?.() ?? "";
stderr = stderrBuffer?.toString?.() ?? "";
}
if (exitCode !== 0 && isWindows) {
const exitReason = getWindowsExitReason(exitCode);
if (exitReason) {
signalCode = exitReason;
}
}
if (error || signalCode || exitCode !== 0) {
const description = command.map(arg => (arg.includes(" ") ? `"${arg.replace(/"/g, '\\"')}"` : arg)).join(" ");
const cause = error || stderr?.trim() || stdout?.trim() || undefined;
if (signalCode) {
error = new Error(`Command killed with ${signalCode}: ${description}`, { cause });
} else {
error = new Error(`Command exited with code ${exitCode}: ${description}`, { cause });
}
}
if (error) {
const retryOnError = options["retryOnError"];
if (typeof retryOnError === "function") {
if (retryOnError(error)) {
return spawn(command, options);
}
}
const throwOnError = options["throwOnError"];
if (typeof throwOnError === "function") {
if (throwOnError(error)) {
throw error;
}
} else if (throwOnError) {
throw error;
}
}
return {
exitCode,
signalCode,
stdout,
stderr,
error,
};
}
/**
* @param {string[]} command
* @param {SpawnOptions} options
* @returns {SpawnResult}
*/
export function spawnSyncSafe(command, options = {}) {
return spawnSync(command, { throwOnError: true, ...options });
}
/**
* @param {number} exitCode
* @returns {string | undefined}
*/
export function getWindowsExitReason(exitCode) {
const windowsKitPath = "C:\\Program Files (x86)\\Windows Kits";
if (!existsSync(windowsKitPath)) {
return;
}
const windowsKitPaths = readdirSync(windowsKitPath)
.filter(filename => isFinite(parseInt(filename)))
.sort((a, b) => parseInt(b) - parseInt(a));
let ntStatusPath;
for (const windowsKitPath of windowsKitPaths) {
const includePath = `${windowsKitPath}\\Include`;
if (!existsSync(includePath)) {
continue;
}
const windowsSdkPaths = readdirSync(includePath).sort();
for (const windowsSdkPath of windowsSdkPaths) {
const statusPath = `${includePath}\\${windowsSdkPath}\\shared\\ntstatus.h`;
if (existsSync(statusPath)) {
ntStatusPath = statusPath;
break;
}
}
}
if (!ntStatusPath) {
return;
}
const nthStatus = readFile(ntStatusPath, { cache: true });
const match = nthStatus.match(new RegExp(`(STATUS_\\w+).*0x${exitCode?.toString(16)}`, "i"));
if (match) {
const [, exitReason] = match;
return exitReason;
}
}
/**
* @param {string | URL} url
* @returns {URL | undefined}
*/
export function parseGitUrl(url) {
const string = typeof url === "string" ? url : url.toString();
const githubUrl = getEnv("GITHUB_SERVER_URL", false) || "https://github.com";
if (/^git@github\.com:/.test(string)) {
return new URL(string.slice(15).replace(/\.git$/, ""), githubUrl);
}
if (/^https:\/\/github\.com\//.test(string)) {
return new URL(string.slice(19).replace(/\.git$/, ""), githubUrl);
}
}
/**
* @param {string | URL} url
* @returns {string | undefined}
*/
export function parseGitRepository(url) {
const parsed = parseGitUrl(url);
if (parsed) {
const { hostname, pathname } = parsed;
if (hostname == "github.com") {
return pathname.slice(1);
}
}
}
/**
* @param {string} [cwd]
* @returns {URL | undefined}
*/
export function getRepositoryUrl(cwd) {
if (!cwd) {
if (isBuildkite) {
const repository = getEnv("BUILDKITE_REPO", false);
if (repository) {
return parseGitUrl(repository);
}
}
if (isGithubAction) {
const serverUrl = getEnv("GITHUB_SERVER_URL", false) || "https://github.com";
const repository = getEnv("GITHUB_REPOSITORY", false);
if (serverUrl && repository) {
return parseGitUrl(new URL(repository, serverUrl));
}
}
}
const { error, stdout } = spawnSync(["git", "remote", "get-url", "origin"], { cwd });
if (!error) {
return parseGitUrl(stdout.trim());
}
}
/**
* @param {string} [cwd]
* @returns {string | undefined}
*/
export function getRepository(cwd) {
if (!cwd) {
if (isGithubAction) {
const repository = getEnv("GITHUB_REPOSITORY", false);
if (repository) {
return repository;
}
}
}
const url = getRepositoryUrl(cwd);
if (url) {
return parseGitRepository(url);
}
}
/**
* @param {string} [cwd]
* @returns {string | undefined}
*/
export function getCommit(cwd) {
if (!cwd) {
if (isBuildkite) {
const commit = getEnv("BUILDKITE_COMMIT", false);
if (commit) {
return commit;
}
}
if (isGithubAction) {
const commit = getEnv("GITHUB_SHA", false);
if (commit) {
return commit;
}
}
}
const { error, stdout } = spawnSync(["git", "rev-parse", "HEAD"], { cwd });
if (!error) {
return stdout.trim();
}
}
/**
* @param {string} [cwd]
* @returns {string | undefined}
*/
export function getCommitMessage(cwd) {
if (!cwd) {
if (isBuildkite) {
const message = getEnv("BUILDKITE_MESSAGE", false);
if (message) {
return message;
}
}
}
const { error, stdout } = spawnSync(["git", "log", "-1", "--pretty=%B"], { cwd });
if (!error) {
return stdout.trim();
}
}
/**
* @param {string} [cwd]
* @returns {string | undefined}
*/
export function getBranch(cwd) {
if (!cwd) {
if (isBuildkite) {
const branch = getEnv("BUILDKITE_BRANCH", false);
if (branch) {
return branch;
}
}
if (isGithubAction) {
const ref = getEnv("GITHUB_REF_NAME", false);
if (ref) {
return ref;
}
}
}
const { error, stdout } = spawnSync(["git", "rev-parse", "--abbrev-ref", "HEAD"], { cwd });
if (!error) {
return stdout.trim();
}
}
/**
* @param {string} [cwd]
* @returns {string | undefined}
*/
export function getMainBranch(cwd) {
if (!cwd) {
if (isBuildkite) {
const branch = getEnv("BUILDKITE_PIPELINE_DEFAULT_BRANCH", false);
if (branch) {
return branch;
}
}
if (isGithubAction) {
const headRef = getEnv("GITHUB_HEAD_REF", false);
if (headRef) {
return headRef;
}
}
}
const { error, stdout } = spawnSync(["git", "symbolic-ref", "refs/remotes/origin/HEAD"], { cwd });
if (!error) {
return stdout.trim().replace("refs/remotes/origin/", "");
}
}
/**
* @param {string} [cwd]
* @returns {boolean}
*/
export function isMainBranch(cwd) {
return !isFork(cwd) && getBranch(cwd) === getMainBranch(cwd);
}
/**
* @returns {boolean}
*/
export function isPullRequest() {
if (isBuildkite) {
return !isNaN(parseInt(getEnv("BUILDKITE_PULL_REQUEST", false)));
}
if (isGithubAction) {
return /pull_request|merge_group/.test(getEnv("GITHUB_EVENT_NAME", false));
}
return false;
}
/**
* @returns {number | undefined}
*/
export function getPullRequest() {
if (isBuildkite) {
const pullRequest = getEnv("BUILDKITE_PULL_REQUEST", false);
if (pullRequest) {
return parseInt(pullRequest);
}
}
if (isGithubAction) {
const eventPath = getEnv("GITHUB_EVENT_PATH", false);
if (eventPath && existsSync(eventPath)) {
const event = JSON.parse(readFile(eventPath, { cache: true }));
const pullRequest = event["pull_request"];
if (pullRequest) {
return parseInt(pullRequest["number"]);
}
}
}
}
/**
* @returns {string | undefined}
*/
export function getTargetBranch() {
if (isPullRequest()) {
if (isBuildkite) {
return getEnv("BUILDKITE_PULL_REQUEST_BASE_BRANCH", false);
}
if (isGithubAction) {
return getEnv("GITHUB_BASE_REF", false);
}
}
}
/**
* @returns {boolean}
*/
export function isFork() {
if (isBuildkite) {
const repository = getEnv("BUILDKITE_PULL_REQUEST_REPO", false);
return !!repository && repository !== getEnv("BUILDKITE_REPO", false);
}
if (isGithubAction) {
const eventPath = getEnv("GITHUB_EVENT_PATH", false);
if (eventPath && existsSync(eventPath)) {
const event = JSON.parse(readFile(eventPath, { cache: true }));
const pullRequest = event["pull_request"];
if (pullRequest) {
return !!pullRequest["head"]["repo"]["fork"];
}
}
}
return false;
}
/**
* @param {string} [cwd]
* @returns {boolean}
*/
export function isMergeQueue(cwd) {
return /^gh-readonly-queue/.test(getBranch(cwd));
}
/**
* @returns {string | undefined}
*/
export function getGithubToken() {
const cachedToken = getSecret("GITHUB_TOKEN", { required: false });
if (typeof cachedToken === "string" || !which("gh")) {
return cachedToken || undefined;
}
const { error, stdout } = spawnSync(["gh", "auth", "token"]);
const token = error ? "" : stdout.trim();
setEnv("GITHUB_TOKEN", token);
return token || undefined;
}
/**
* @typedef {object} CurlOptions
* @property {string} [method]
* @property {string} [body]
* @property {Record<string, string | undefined>} [headers]
* @property {number} [timeout]
* @property {boolean} [cache]
* @property {number} [retries]
* @property {boolean} [json]
* @property {boolean} [arrayBuffer]
* @property {string} [filename]
*/
/**
* @typedef {object} CurlResult
* @property {number} status
* @property {string} statusText
* @property {Error | undefined} error
* @property {any} body
*/
/** @type {Record<string, CurlResult | undefined>} */
let cachedResults;
/**
* @param {string} url
* @param {CurlOptions} [options]
* @returns {Promise<CurlResult>}
*/
export async function curl(url, options = {}) {
let { hostname, href } = new URL(url);
let method = options["method"] || "GET";
let input = options["body"];
let headers = options["headers"] || {};
let retries = options["retries"] || 3;
let json = options["json"];
let arrayBuffer = options["arrayBuffer"];
let filename = options["filename"];
let cacheKey;
let cache = options["cache"];
if (cache) {
cacheKey = `${method} ${href}`;
if (cachedResults?.[cacheKey]) {
return cachedResults[cacheKey];
}
}
if (typeof headers["Authorization"] === "undefined") {
if (hostname === "api.github.com" || hostname === "uploads.github.com") {
const githubToken = getGithubToken();
if (githubToken) {
headers["Authorization"] = `Bearer ${githubToken}`;
}
}
}
let status;
let statusText;
let body;
let error;
for (let i = 0; i < retries; i++) {
if (i > 0) {
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
}
let response;
try {
response = await fetch(href, { method, headers, body: input });
} catch (cause) {
debugLog("$", "curl", href, "-> error");
error = new Error(`Fetch failed: ${method} ${url}`, { cause });
continue;
}
status = response["status"];
statusText = response["statusText"];
debugLog("$", "curl", href, "->", status, statusText);
const ok = response["ok"];
try {
if (filename && ok) {
const buffer = await response.arrayBuffer();
writeFile(filename, new Uint8Array(buffer));
} else if (arrayBuffer && ok) {
body = await response.arrayBuffer();
} else if (json && ok) {
body = await response.json();
} else {
body = await response.text();
}
} catch (cause) {
error = new Error(`Fetch failed: ${method} ${url}`, { cause });
continue;
}
if (response["ok"]) {
break;
}
error = new Error(`Fetch failed: ${method} ${url}: ${status} ${statusText}`, { cause: body });
if (status === 400 || status === 404 || status === 422) {
break;
}
}
if (cacheKey) {
cachedResults ||= {};
cachedResults[cacheKey] = { status, statusText, error, body };
}
return {
status,
statusText,
error,
body,
};
}
/**
* @param {string} url
* @param {CurlOptions} options
* @returns {Promise<any>}
*/
export async function curlSafe(url, options) {
const result = await curl(url, options);
const { error, body } = result;
if (error) {
throw error;
}
return body;
}
/** @type {Record<string, string> | undefined} */
let cachedFiles;
/**
* @param {string} filename
* @param {object} [options]
* @param {boolean} [options.cache]
* @returns {string}
*/
export function readFile(filename, options = {}) {
const absolutePath = resolve(filename);
if (options["cache"]) {
if (cachedFiles?.[absolutePath]) {
return cachedFiles[absolutePath];
}
}
debugLog("$", "cat", absolutePath);
let content;
try {
content = readFileSync(absolutePath, "utf-8");
} catch (cause) {
throw new Error(`Read failed: ${absolutePath}`, { cause });
}
if (options["cache"]) {
cachedFiles ||= {};
cachedFiles[absolutePath] = content;
}
return content;
}
/**
* @param {string} path
* @param {number} mode
*/
export function chmod(path, mode) {
debugLog("$", "chmod", path, mode);
chmodSync(path, mode);
}
/**
* @param {string} filename
* @param {string | Buffer} content
* @param {object} [options]
* @param {number} [options.mode]
*/
export function writeFile(filename, content, options) {
mkdir(dirname(filename));
debugLog("$", "touch", filename);
writeFileSync(filename, content);
if (options?.mode) {
chmod(filename, options.mode);
}
}
/**
* @param {string} source
* @param {string} destination
* @param {object} [options]
* @param {number} [options.mode]
*/
export function copyFile(source, destination, options) {
mkdir(dirname(destination));
debugLog("$", "cp", source, destination);
copyFileSync(source, destination);
if (options?.mode) {
chmod(destination, options.mode);
}
}
/**
* @param {string} path
* @param {object} [options]
* @param {number} [options.mode]
*/
export function mkdir(path, options = {}) {
if (existsSync(path)) {
return;
}
debugLog("$", "mkdir", path);
mkdirSync(path, { ...options, recursive: true });
}
/**
* @param {string} path
*/
export function rm(path) {
let stats;
try {
stats = statSync(path);
} catch {
return;
}
if (stats?.isDirectory()) {
debugLog("$", "rm", "-rf", path);
rmSync(path, { recursive: true, force: true });
} else {
debugLog("$", "rm", "-f", path);
rmSync(path, { force: true });
}
}
/**
* @param {string | string[]} command
* @param {object} [options]
* @param {boolean} [options.required]
* @returns {string | undefined}
*/
export function which(command, options = {}) {
const commands = Array.isArray(command) ? command : [command];
const executables = isWindows ? commands.flatMap(name => [name, `${name}.exe`, `${name}.cmd`]) : commands;
const path = getEnv("PATH", false) || "";
const binPaths = path.split(isWindows ? ";" : ":");
for (const binPath of binPaths) {
for (const executable of executables) {
const executablePath = join(binPath, executable);
if (existsSync(executablePath)) {
return executablePath;
}
}
}
if (options["required"]) {
const description = commands.join(" or ");
throw new Error(`Command not found: ${description}`);
}
}
/**
* @returns {string | undefined}
*/
export function getBuildId() {
if (isBuildkite) {
return getEnv("BUILDKITE_BUILD_ID");
}
if (isGithubAction) {
return getEnv("GITHUB_RUN_ID");
}
}
/**
* @returns {number | undefined}
*/
export function getBuildNumber() {
if (isBuildkite) {
return parseInt(getEnv("BUILDKITE_BUILD_NUMBER"));
}
if (isGithubAction) {
return parseInt(getEnv("GITHUB_RUN_ID"));
}
}
/**
* @returns {URL | undefined}
*/
export function getBuildUrl() {
if (isBuildkite) {
const buildUrl = getEnv("BUILDKITE_BUILD_URL");
const jobId = getEnv("BUILDKITE_JOB_ID");
return new URL(`#${jobId}`, buildUrl);
}
if (isGithubAction) {
const baseUrl = getEnv("GITHUB_SERVER_URL", false) || "https://github.com";
const repository = getEnv("GITHUB_REPOSITORY");
const runId = getEnv("GITHUB_RUN_ID");
return new URL(`${repository}/actions/runs/${runId}`, baseUrl);
}
}
/**
* @returns {string | undefined}
*/
export function getBuildLabel() {
if (isBuildkite) {
const label = getEnv("BUILDKITE_LABEL", false) || getEnv("BUILDKITE_GROUP_LABEL", false);
if (label) {
return label;
}
}
if (isGithubAction) {
const label = getEnv("GITHUB_WORKFLOW", false);
if (label) {
return label;
}
}
}
/**
* @returns {boolean | undefined}
*/
export function isBuildManual() {
if (isBuildkite) {
const buildSource = getEnv("BUILDKITE_SOURCE", false);
if (buildSource) {
const buildId = getEnv("BUILDKITE_REBUILT_FROM_BUILD_ID", false);
return buildSource === "ui" && !buildId;
}
}
}
/**
* @param {string} [os]
* @returns {number}
*/
export function getBootstrapVersion(os) {
const scriptPath = join(
import.meta.dirname,
os === "windows" || (!os && isWindows) ? "bootstrap.ps1" : "bootstrap.sh",
);
const scriptContent = readFile(scriptPath, { cache: true });
const match = /# Version: (\d+)/.exec(scriptContent);
if (match) {
const [, version] = match;
return parseInt(version);
}
return 0;
}
/**
* @param {string} [filename]
* @param {number} [line]
* @returns {URL | undefined}
*/
export function getFileUrl(filename, line) {
let cwd;
if (filename?.startsWith("vendor")) {
const parentPath = resolve(dirname(filename));
const { error, stdout } = spawnSync(["git", "rev-parse", "--show-toplevel"], { cwd: parentPath });
if (error) {
return;
}
cwd = stdout.trim();
}
const baseUrl = getRepositoryUrl(cwd);
if (!filename) {
return baseUrl;
}
const filePath = (cwd ? relative(cwd, filename) : filename).replace(/\\/g, "/");
const commit = getCommit(cwd);
const url = new URL(`blob/${commit}/${filePath}`, `${baseUrl}/`).toString();
if (typeof line !== "undefined") {
return new URL(`#L${line}`, url);
}
return url;
}
/**
* @typedef {object} BuildkiteBuild
* @property {string} id
* @property {string} commit_id
* @property {string} branch_name
*/
/**
* @returns {Promise<BuildkiteBuild | undefined>}
*/
export async function getLastSuccessfulBuild() {
if (isBuildkite) {
let depth = 0;
let url = getBuildUrl();
if (url) {
url.hash = "";
}
while (url) {
const { error, body } = await curl(`${url}.json`, { json: true, cache: true });
if (error) {
return;
}
const { state, prev_branch_build: previousBuild, steps } = body;
if (depth++) {
if (state === "failed" || state === "passed" || state === "canceled") {
const buildSteps = steps.filter(({ label }) => label.endsWith("build-bun"));
if (buildSteps.length) {
if (buildSteps.every(({ outcome }) => outcome === "passed")) {
return body;
}
return;
}
}
}
if (!previousBuild) {
return;
}
url = new URL(previousBuild["url"], url);
}
}
}
/**
* @param {string} filename Absolute path to file to upload
*/
export async function uploadArtifact(filename) {
if (isBuildkite) {
await spawnSafe(["buildkite-agent", "artifact", "upload", basename(filename)], {
cwd: dirname(filename),
stdio: "inherit",
});
} else {
console.warn(`not in buildkite. artifact ${filename} not uploaded.`);
}
}
/**
* @param {string} string
* @returns {string}
*/
export function stripAnsi(string) {
return string.replace(/\u001b\[[0-9;]*[a-zA-Z]/g, "");
}
/**
* @param {string} string
* @returns {string}
*/
export function unescapeGitHubAction(string) {
return string.replace(/%25/g, "%").replace(/%0D/g, "\r").replace(/%0A/g, "\n");
}
/**
* @param {string} string
* @returns {string}
*/
export function escapeHtml(string) {
return string
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;")
.replace(/`/g, "&#96;");
}
/**
* @param {string} string
* @returns {string}
*/
export function escapeCodeBlock(string) {
return string.replace(/`/g, "\\`");
}
/**
* @param {string} string
* @returns {string}
*/
export function escapePowershell(string) {
return string.replace(/'/g, "''").replace(/`/g, "``");
}
/**
* @returns {string}
*/
export function homedir() {
return nodeHomedir();
}
/**
* @returns {string}
*/
export function tmpdir() {
if (isWindows) {
for (const key of ["TMPDIR", "TEMP", "TEMPDIR", "TMP", "RUNNER_TEMP"]) {
const tmpdir = getEnv(key, false);
if (!tmpdir || /cygwin|cygdrive/i.test(tmpdir) || !/^[a-z]/i.test(tmpdir)) {
continue;
}
return normalizeWindows(tmpdir);
}
const appData = process.env["LOCALAPPDATA"];
if (appData) {
const appDataTemp = join(appData, "Temp");
if (existsSync(appDataTemp)) {
return appDataTemp;
}
}
}
if (isMacOS || isLinux) {
if (existsSync("/tmp")) {
return "/tmp";
}
}
return nodeTmpdir();
}
/**
* @param {string} [prefix]
* @param {string} [filename]
* @returns {string}
*/
export function mkdtemp(prefix, filename) {
const tmpPath = mkdtempSync(join(tmpdir(), prefix || "bun-"));
return filename ? join(tmpPath, filename) : tmpPath;
}
/**
* @param {string} filename
* @param {string} [output]
* @returns {Promise<string>}
*/
export async function unzip(filename, output) {
const destination = output || mkdtempSync(join(tmpdir(), "unzip-"));
if (isWindows) {
const command = `Expand-Archive -Force -LiteralPath "${escapePowershell(filename)}" -DestinationPath "${escapePowershell(destination)}"`;
await spawnSafe(["powershell", "-Command", command]);
} else {
await spawnSafe(["unzip", "-o", filename, "-d", destination]);
}
return destination;
}
/**
* @param {string} value
* @returns {boolean | undefined}
*/
export function parseBoolean(value) {
if (/^(true|yes|1|on)$/i.test(value)) {
return true;
}
if (/^(false|no|0|off)$/i.test(value)) {
return false;
}
}
/**
* @param {string} string
* @returns {"darwin" | "linux" | "windows" | "freebsd"}
*/
export function parseOs(string) {
if (/darwin|apple|mac/i.test(string)) {
return "darwin";
}
if (/linux|android/i.test(string)) {
return "linux";
}
if (/freebsd/i.test(string)) {
return "freebsd";
}
if (/win/i.test(string)) {
return "windows";
}
throw new Error(`Unsupported operating system: ${string}`);
}
/**
* @returns {"darwin" | "linux" | "windows" | "freebsd"}
*/
export function getOs() {
return parseOs(process.platform);
}
/**
* @param {string} string
* @returns {"x64" | "aarch64"}
*/
export function parseArch(string) {
if (/x64|amd64|x86_64/i.test(string)) {
return "x64";
}
if (/arm64|aarch64/i.test(string)) {
return "aarch64";
}
throw new Error(`Unsupported architecture: ${string}`);
}
/**
* @returns {"x64" | "aarch64"}
*/
export function getArch() {
return parseArch(process.arch);
}
/**
* @returns {string | undefined}
*/
export function getKernel() {
if (isWindows) {
return;
}
const kernel = release();
const match = /(\d+)\.(\d+)(?:\.(\d+))?/.exec(kernel);
if (match) {
const [, major, minor, patch] = match;
if (patch) {
return `${major}.${minor}.${patch}`;
}
return `${major}.${minor}`;
}
return kernel;
}
/**
* @returns {"musl" | "gnu" | "android" | undefined}
*/
export function getAbi() {
if (!isLinux) {
return;
}
if (isAndroid || existsSync("/system/bin/linker64")) {
return "android";
}
if (existsSync("/etc/alpine-release")) {
return "musl";
}
const arch = getArch() === "x64" ? "x86_64" : "aarch64";
const muslLibPath = `/lib/ld-musl-${arch}.so.1`;
if (existsSync(muslLibPath)) {
return "musl";
}
const gnuLibPath = `/lib/ld-linux-${arch}.so.2`;
if (existsSync(gnuLibPath)) {
return "gnu";
}
const { error, stdout } = spawnSync(["ldd", "--version"]);
if (!error) {
if (/musl/i.test(stdout)) {
return "musl";
}
if (/gnu|glibc/i.test(stdout)) {
return "gnu";
}
}
}
/**
* @returns {string | undefined}
*/
export function getAbiVersion() {
if (!isLinux) {
return;
}
const { error, stdout } = spawnSync(["ldd", "--version"]);
if (!error) {
const match = /(\d+)\.(\d+)(?:\.(\d+))?/.exec(stdout);
if (match) {
const [, major, minor, patch] = match;
if (patch) {
return `${major}.${minor}.${patch}`;
}
return `${major}.${minor}`;
}
}
}
/**
* @returns {string}
*/
export function getTailscale() {
if (isMacOS) {
const tailscaleApp = "/Applications/Tailscale.app/Contents/MacOS/tailscale";
if (existsSync(tailscaleApp)) {
return tailscaleApp;
}
}
if (isWindows) {
const tailscaleExe = "C:\\Program Files\\Tailscale\\tailscale.exe";
if (existsSync(tailscaleExe)) {
return tailscaleExe;
}
}
return "tailscale";
}
/**
* @returns {string | undefined}
*/
export function getTailscaleIp() {
const tailscale = getTailscale();
const { error, stdout } = spawnSync([tailscale, "ip", "--1"]);
if (!error) {
return stdout.trim();
}
}
/**
* @returns {string | undefined}
*/
export function getPublicIp() {
for (const url of ["https://checkip.amazonaws.com", "https://ipinfo.io/ip"]) {
const { error, stdout } = spawnSync(["curl", url]);
if (!error) {
return stdout.trim();
}
}
}
/**
* @returns {string}
*/
export function getHostname() {
if (isBuildkite) {
const agent = getEnv("BUILDKITE_AGENT_NAME", false);
if (agent) {
return agent;
}
}
if (isGithubAction) {
const runner = getEnv("RUNNER_NAME", false);
if (runner) {
return runner;
}
}
return hostname();
}
/**
* @returns {string}
*/
export function getUsername() {
const { username } = userInfo();
return username;
}
/**
* @param {string} distro
* @returns {string}
*/
export function getUsernameForDistro(distro) {
if (/windows/i.test(distro)) {
return "administrator";
}
if (/alpine|centos/i.test(distro)) {
return "root";
}
if (/debian/i.test(distro)) {
return "admin";
}
if (/ubuntu/i.test(distro)) {
return "ubuntu";
}
if (/amazon|amzn|al\d+|rhel/i.test(distro)) {
return "ec2-user";
}
throw new Error(`Unsupported distro: ${distro}`);
}
/**
* @returns {string | undefined}
*/
export function getDistro() {
if (isMacOS) {
return "macOS";
}
if (isLinux) {
const alpinePath = "/etc/alpine-release";
if (existsSync(alpinePath)) {
return "alpine";
}
const releasePath = "/etc/os-release";
if (existsSync(releasePath)) {
const releaseFile = readFile(releasePath, { cache: true });
const match = releaseFile.match(/^ID=(.*)/m);
if (match) {
const [, id] = match;
return id.includes('"') ? JSON.parse(id) : id;
}
}
const { error, stdout } = spawnSync(["lsb_release", "-is"]);
if (!error) {
return stdout.trim().toLowerCase();
}
}
if (isWindows) {
const { error, stdout } = spawnSync(["cmd", "/c", "ver"]);
if (!error) {
return stdout.trim();
}
}
}
/**
* @returns {string | undefined}
*/
export function getDistroVersion() {
if (isMacOS) {
const { error, stdout } = spawnSync(["sw_vers", "-productVersion"]);
if (!error) {
return stdout.trim();
}
}
if (isLinux) {
const alpinePath = "/etc/alpine-release";
if (existsSync(alpinePath)) {
const release = readFile(alpinePath, { cache: true }).trim();
if (release.includes("_")) {
const [version] = release.split("_");
return `${version}-edge`;
}
return release;
}
const releasePath = "/etc/os-release";
if (existsSync(releasePath)) {
const releaseFile = readFile(releasePath, { cache: true });
const match = releaseFile.match(/^VERSION_ID=(.*)/m);
if (match) {
const [, release] = match;
return release.includes('"') ? JSON.parse(release) : release;
}
}
const { error, stdout } = spawnSync(["lsb_release", "-rs"]);
if (!error) {
return stdout.trim();
}
}
if (isWindows) {
const { error, stdout } = spawnSync(["cmd", "/c", "ver"]);
if (!error) {
return stdout.trim();
}
}
}
/**
* @returns {string | undefined}
*/
export function getShell() {
if (isWindows) {
const pwsh = which(["pwsh", "powershell"]);
if (pwsh) {
return pwsh;
}
}
const sh = which(["bash", "sh"]);
if (sh) {
return sh;
}
return getEnv("SHELL", false);
}
/**
* @typedef {"aws" | "google" | "azure"} Cloud
*/
/** @type {Cloud | undefined} */
let detectedCloud;
/**
* @returns {Promise<boolean | undefined>}
*/
export async function isAws() {
if (typeof detectedCloud === "string") {
return detectedCloud === "aws";
}
async function checkAws() {
if (isLinux) {
const kernel = release();
if (kernel.endsWith("-aws")) {
return true;
}
const { error: systemdError, stdout } = await spawn(["systemd-detect-virt"]);
if (!systemdError) {
if (stdout.includes("amazon")) {
return true;
}
}
const dmiPath = "/sys/devices/virtual/dmi/id/board_asset_tag";
if (existsSync(dmiPath)) {
const dmiFile = readFileSync(dmiPath, { encoding: "utf-8" });
if (dmiFile.startsWith("i-")) {
return true;
}
}
}
if (isWindows) {
const executionEnv = getEnv("AWS_EXECUTION_ENV", false);
if (executionEnv === "EC2") {
return true;
}
const { error: powershellError, stdout } = await spawn([
"powershell",
"-Command",
"Get-CimInstance -ClassName Win32_ComputerSystem | Select-Object Manufacturer",
]);
if (!powershellError) {
return stdout.includes("Amazon");
}
}
}
if (await checkAws()) {
detectedCloud = "aws";
return true;
}
}
/**
* @returns {Promise<boolean | undefined>}
*/
export async function isGoogleCloud() {
if (typeof detectedCloud === "string") {
return detectedCloud === "google";
}
async function detectGoogleCloud() {
if (isLinux) {
const vendorPaths = [
"/sys/class/dmi/id/sys_vendor",
"/sys/class/dmi/id/bios_vendor",
"/sys/class/dmi/id/product_name",
];
for (const vendorPath of vendorPaths) {
if (existsSync(vendorPath)) {
const vendorFile = readFileSync(vendorPath, { encoding: "utf-8" });
if (vendorFile.includes("Google")) {
return true;
}
}
}
}
}
if (await detectGoogleCloud()) {
detectedCloud = "google";
return true;
}
}
/**
* @returns {Promise<boolean | undefined>}
*/
export async function isAzure() {
if (typeof detectedCloud === "string") {
return detectedCloud === "azure";
}
async function detectAzure() {
// Azure IMDS (Instance Metadata Service) — the official way to detect Azure VMs.
// https://learn.microsoft.com/en-us/azure/virtual-machines/instance-metadata-service
const { error, body } = await curl("http://169.254.169.254/metadata/instance?api-version=2021-02-01", {
headers: { "Metadata": "true" },
retries: 1,
});
if (!error && body) {
try {
const metadata = JSON.parse(body);
if (metadata?.compute?.azEnvironment) {
return true;
}
} catch {}
}
}
if (await detectAzure()) {
detectedCloud = "azure";
return true;
}
}
/**
* @returns {Promise<Cloud | undefined>}
*/
export async function getCloud() {
if (typeof detectedCloud === "string") {
return detectedCloud;
}
if (await isAws()) {
return "aws";
}
if (await isGoogleCloud()) {
return "google";
}
if (await isAzure()) {
return "azure";
}
}
/**
* @param {string | Record<Cloud, string>} name
* @param {Cloud} [cloud]
* @returns {Promise<string | undefined>}
*/
export async function getCloudMetadata(name, cloud) {
cloud ??= await getCloud();
if (!cloud) {
return;
}
if (typeof name === "object") {
name = name[cloud];
}
let url;
let headers;
if (cloud === "aws") {
url = new URL(name, "http://169.254.169.254/latest/meta-data/");
} else if (cloud === "google") {
url = new URL(name, "http://metadata.google.internal/computeMetadata/v1/instance/");
headers = { "Metadata-Flavor": "Google" };
} else if (cloud === "azure") {
// Azure IMDS uses a single JSON endpoint; individual fields are extracted by the caller.
url = new URL("http://169.254.169.254/metadata/instance?api-version=2021-02-01");
headers = { "Metadata": "true" };
} else {
throw new Error(`Unsupported cloud: ${inspect(cloud)}`);
}
const { error, body } = await curl(url, { headers, retries: 10 });
if (error) {
console.warn("Failed to get cloud metadata:", error);
return;
}
return body.trim();
}
/**
* @param {string} tag
* @param {Cloud} [cloud]
* @returns {Promise<string | undefined>}
*/
export async function getCloudMetadataTag(tag, cloud) {
cloud ??= await getCloud();
if (cloud === "azure") {
// Azure IMDS returns all tags in a single JSON response.
// Tags are in compute.tagsList as [{name, value}, ...].
const body = await getCloudMetadata("", cloud);
if (!body) return;
try {
const metadata = JSON.parse(body);
const tags = metadata?.compute?.tagsList;
if (Array.isArray(tags)) {
const entry = tags.find(t => t.name === tag);
return entry?.value;
}
} catch {}
return;
}
const metadata = {
"aws": `tags/instance/${tag}`,
"google": `labels/${tag.replace(":", "-")}`,
};
return getCloudMetadata(metadata, cloud);
}
/**
* @typedef {Object} AwsCredentials
* @property {string} AccessKeyId
* @property {string} SecretAccessKey
* @property {string} [Token]
*/
/**
* Instance-role credentials from IMDS.
* @returns {Promise<AwsCredentials | undefined>}
*/
async function getAwsInstanceCredentials() {
const role = await getCloudMetadata("iam/security-credentials/", "aws");
if (!role) {
return;
}
const body = await getCloudMetadata(`iam/security-credentials/${role.trim()}`, "aws");
if (!body) {
return;
}
try {
return JSON.parse(body);
} catch {
return;
}
}
/**
* Signs an AWS API request (SigV4). agent.mjs ships to the AMI as a single
* bundled file, so this avoids pulling in the SDK.
* @param {Object} request
* @param {string} request.method
* @param {string} request.host
* @param {string} request.path
* @param {string} request.body
* @param {string} request.service
* @param {string} request.region
* @param {Record<string, string>} request.headers
* @param {AwsCredentials} request.credentials
* @param {Date} [request.date]
* @returns {Record<string, string>} headers, including Authorization
*/
export function signAwsRequest({ method, host, path, body, service, region, headers, credentials, date }) {
const { AccessKeyId, SecretAccessKey, Token } = credentials;
const amzDate = (date ?? new Date()).toISOString().replace(/[:-]|\.\d{3}/g, "");
const day = amzDate.slice(0, 8);
const bodyHash = sha256(body);
const signed = {
...headers,
"host": host,
"x-amz-date": amzDate,
"x-amz-content-sha256": bodyHash,
};
if (Token) {
signed["x-amz-security-token"] = Token;
}
const canonical = Object.entries(signed)
.map(([key, value]) => [key.toLowerCase(), `${value}`.trim()])
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
const canonicalHeaders = canonical.map(([key, value]) => `${key}:${value}\n`).join("");
const signedHeaders = canonical.map(([key]) => key).join(";");
const canonicalRequest = [method, path, "", canonicalHeaders, signedHeaders, bodyHash].join("\n");
const scope = `${day}/${region}/${service}/aws4_request`;
const stringToSign = ["AWS4-HMAC-SHA256", amzDate, scope, sha256(canonicalRequest)].join("\n");
const hmac = (key, data) => createHmac("sha256", key).update(data).digest();
const signingKey = hmac(hmac(hmac(hmac(`AWS4${SecretAccessKey}`, day), region), service), "aws4_request");
const signature = createHmac("sha256", signingKey).update(stringToSign).digest("hex");
return {
...signed,
"Authorization": `AWS4-HMAC-SHA256 Credential=${AccessKeyId}/${scope}, SignedHeaders=${signedHeaders}, Signature=${signature}`,
};
}
/**
* Reads a secret from AWS Secrets Manager using the instance role.
* @param {string} secretId
* @param {Object} [options]
* @param {string} [options.region] defaults to the instance's region
* @param {AwsCredentials} [options.credentials] defaults to IMDS credentials
* @returns {Promise<string | undefined>}
*/
export async function getAwsSecret(secretId, options = {}) {
const region = options["region"] || (await getCloudMetadata("placement/region", "aws")) || "us-east-1";
const credentials = options["credentials"] || (await getAwsInstanceCredentials());
if (!credentials) {
console.warn("Failed to get AWS secret: no instance credentials");
return;
}
const host = `secretsmanager.${region}.amazonaws.com`;
const body = JSON.stringify({ SecretId: secretId });
const headers = signAwsRequest({
method: "POST",
host,
path: "/",
body,
service: "secretsmanager",
region,
credentials,
headers: {
"Content-Type": "application/x-amz-json-1.1",
"X-Amz-Target": "secretsmanager.GetSecretValue",
},
});
const { error, body: response } = await curl(`https://${host}/`, {
method: "POST",
headers,
body,
json: true,
retries: 5,
});
if (error) {
console.warn("Failed to get AWS secret:", error);
return;
}
return response?.["SecretString"];
}
/**
* Reads a secret from Azure Key Vault using the VM's managed identity.
* @param {string} vaultName
* @param {string} secretName
* @returns {Promise<string | undefined>}
*/
export async function getAzureSecret(vaultName, secretName) {
const identityUrl =
"http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https%3A%2F%2Fvault.azure.net";
const { error: identityError, body: identity } = await curl(identityUrl, {
headers: { "Metadata": "true" },
json: true,
retries: 10,
});
const accessToken = identity?.["access_token"];
if (identityError || !accessToken) {
console.warn("Failed to get Azure managed identity token:", identityError);
return;
}
const secretUrl = `https://${vaultName}.vault.azure.net/secrets/${secretName}?api-version=7.4`;
const { error, body } = await curl(secretUrl, {
headers: { "Authorization": `Bearer ${accessToken}` },
json: true,
retries: 5,
});
if (error) {
console.warn("Failed to get Azure secret:", error);
return;
}
return body?.["value"];
}
/**
* @param {string} name
* @returns {Promise<string | undefined>}
*/
export async function getBuildMetadata(name) {
if (isBuildkite) {
const { error, stdout } = await spawn(["buildkite-agent", "meta-data", "get", name]);
if (!error) {
const value = stdout.trim();
if (value) {
return value;
}
}
}
}
/**
* @param {string} name
* @param {string} value
* @returns {Promise<void>}
*/
export async function setBuildMetadata(name, value) {
if (isBuildkite) {
const { error } = await spawn(["buildkite-agent", "meta-data", "set", name, value]);
if (error) {
console.error(`Failed to set build meta-data '${name}':`, error);
}
}
}
/**
* @typedef ConnectOptions
* @property {string} hostname
* @property {number} port
* @property {number} [retries]
*/
/**
* @param {ConnectOptions} options
* @returns {Promise<Error | undefined>}
*/
export async function waitForPort(options) {
const { hostname, port, retries = 10 } = options;
console.log("Connecting...", `${hostname}:${port}`);
let cause;
for (let i = 0; i < retries; i++) {
if (cause) {
await new Promise(resolve => setTimeout(resolve, Math.pow(2, i) * 1000));
}
const connected = new Promise((resolve, reject) => {
const socket = connect({ host: hostname, port });
socket.on("connect", () => {
socket.destroy();
console.log("Connected:", `${hostname}:${port}`);
resolve();
});
socket.on("error", error => {
socket.destroy();
reject(error);
});
});
try {
return await connected;
} catch (error) {
cause = error;
}
}
console.error("Connection failed:", `${hostname}:${port}`);
return cause;
}
/**
* @returns {Promise<number>}
*/
export async function getCanaryRevision() {
if (isPullRequest() || isFork()) {
return 1;
}
const repository = getRepository() || "oven-sh/bun";
const { error: releaseError, body: release } = await curl(
new URL(`repos/${repository}/releases/latest`, getGithubApiUrl()),
{ json: true },
);
if (releaseError) {
return 1;
}
const commit = getCommit();
const { tag_name: latest } = release;
const { error: compareError, body: compare } = await curl(
new URL(`repos/${repository}/compare/${latest}...${commit}`, getGithubApiUrl()),
{ json: true },
);
if (compareError) {
return 1;
}
const { ahead_by: revision } = compare;
if (typeof revision === "number") {
return revision;
}
return 1;
}
/**
* @returns {URL}
*/
export function getGithubApiUrl() {
return new URL(getEnv("GITHUB_API_URL", false) || "https://api.github.com");
}
/**
* @returns {URL}
*/
export function getGithubUrl() {
return new URL(getEnv("GITHUB_SERVER_URL", false) || "https://github.com");
}
/**
* @param {string} string
* @returns {string}
*/
export function sha256(string) {
return createHash("sha256").update(Buffer.from(string)).digest("hex");
}
/**
* @param {string} [level]
* @returns {"info" | "warning" | "error"}
*/
function parseLevel(level) {
if (/error|fatal|fail/i.test(level)) {
return "error";
}
if (/warn|caution/i.test(level)) {
return "warning";
}
return "notice";
}
/**
* @typedef {Object} Annotation
* @property {string} title
* @property {string} [content]
* @property {string} [source]
* @property {"notice" | "warning" | "error"} [level]
* @property {string} [url]
* @property {string} [filename]
* @property {number} [line]
* @property {number} [column]
* @property {Record<string, string>} [metadata]
*/
/**
* @typedef {Object} AnnotationContext
* @property {string} [cwd]
* @property {string[]} [command]
*/
/**
* @param {Partial<Record<keyof Annotation, unknown>>} options
* @param {AnnotationContext} [context]
* @returns {Annotation}
*/
export function parseAnnotation(options, context) {
const cwd = (context?.["cwd"] || process.cwd()).replace(/\\/g, "/");
const source = options["source"];
const level = parseLevel(options["level"]);
const title = options["title"] || (source ? `${source} ${level}` : level);
const path = options["filename"]?.replace(/\\/g, "/");
const line = parseInt(options["line"]) || undefined;
const column = parseInt(options["column"]) || undefined;
const content = options["content"];
const lines = Array.isArray(content) ? content : content?.split(/\r?\n/) || [];
const metadata = Object.fromEntries(
Object.entries(options["metadata"] || {}).filter(([, value]) => value !== undefined),
);
// Drop leading blank lines, collapse runs of blank lines, and drop the
// trailing blank line(s) a readUntil() in parseAnnotations() may have
// consumed as a block terminator.
const relevantLines = [];
let lastLine;
for (const line of lines) {
if (!lastLine && !line.trim()) {
continue;
}
lastLine = line.trim();
relevantLines.push(line);
}
while (relevantLines.length > 0 && !relevantLines[relevantLines.length - 1].trim()) {
relevantLines.pop();
}
let filename;
if (path?.startsWith(cwd)) {
filename = path.slice(cwd.length + 1);
} else {
filename = path;
}
return {
source,
title,
level,
filename,
line,
column,
content: relevantLines.join("\n"),
metadata,
};
}
/**
* @typedef {Object} AnnotationFormatOptions
* @property {boolean} [concise]
* @property {boolean} [buildkite]
*/
/**
* @param {Annotation} annotation
* @param {AnnotationFormatOptions} [options]
* @returns {string}
*/
export function formatAnnotationToHtml(annotation, options = {}) {
const { title, content, source, level, filename, line } = annotation;
const { concise, buildkite = isBuildkite } = options;
let html;
if (concise) {
html = "<li>";
} else {
html = "<details><summary>";
}
if (filename) {
const filePath = filename.replace(/\\/g, "/");
const fileUrl = getFileUrl(filePath, line);
if (fileUrl) {
html += `<a href="${fileUrl}"><code>${filePath}</code></a>`;
} else {
html += `<code>${filePath}</code>`;
}
html += " - ";
}
if (title) {
html += title;
} else if (source) {
if (level) {
html += `${source} ${level}`;
} else {
html += source;
}
} else if (level) {
html += level;
} else {
html += "unknown error";
}
const buildLabel = getBuildLabel();
if (buildLabel) {
html += " on ";
const buildUrl = getBuildUrl();
if (buildUrl) {
html += `<a href="${buildUrl}">${buildLabel}</a>`;
} else {
html += buildLabel;
}
}
if (concise) {
html += "</li>\n";
} else {
html += "</summary>\n\n";
if (buildkite) {
const preview = escapeCodeBlock(content);
html += `\`\`\`terminal\n${preview}\n\`\`\`\n`;
} else {
const preview = escapeHtml(stripAnsi(content));
html += `<pre><code>${preview}</code></pre>\n`;
}
html += "\n\n</details>\n\n";
}
return html;
}
/**
* @typedef {Object} AnnotationResult
* @property {Annotation[]} annotations
* @property {string} content
* @property {string} preview
*/
/**
* @param {string} content
* @param {AnnotationOptions} [options]
* @returns {AnnotationResult}
*/
export function parseAnnotations(content) {
/** @type {Annotation[]} */
const annotations = [];
const originalLines = content.split(/\r?\n/);
const lines = [];
for (let i = 0; i < originalLines.length; i++) {
const originalLine = originalLines[i];
const line = stripAnsi(originalLine).trim();
const bufferedLines = [originalLine];
/**
* Consume the lines after the current one into `bufferedLines`, through
* the first line matching `pattern` (inclusive) or `maxLines` lines if
* none matches. Leaves `i` on the last consumed line, so the outer loop
* resumes after it; can be called again to consume further.
*
* @param {RegExp} pattern
* @param {number} [maxLines]
* @returns {{lines: string[], match: RegExpExecArray | undefined}}
*/
const readUntil = (pattern, maxLines = 100) => {
const start = i + 1;
let match;
while (i + 1 < originalLines.length && i + 1 - start < maxLines) {
i++;
const patternMatch = pattern.exec(stripAnsi(originalLines[i]).trim());
if (patternMatch) {
match = patternMatch;
break;
}
}
const lines = originalLines.slice(start, i + 1);
bufferedLines.push(...lines);
return { lines, match };
};
// Github Actions
// https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions
const githubAnnotation = line.match(/^::(error|warning|notice|debug)(?: (.*))?::(.*)$/);
if (githubAnnotation) {
const [, level, attributes, content] = githubAnnotation;
const { file, line, col, title } = Object.fromEntries(
attributes?.split(",")?.map(entry => entry.split("=")) || {},
);
const annotation = parseAnnotation({
level,
filename: file,
line,
column: col,
content: unescapeGitHubAction(title) + unescapeGitHubAction(content),
});
annotations.push(annotation);
continue;
}
const githubCommand = line.match(/^::(group|endgroup|add-mask|stop-commands)::$/);
if (githubCommand) {
continue;
}
// CMake error format
// e.g. CMake Error at /path/to/thing.cmake:123 (message): ...
const cmakeMessage = line.match(/CMake (Error|Warning|Deprecation Warning) at (.*):(\d+)/i);
if (cmakeMessage) {
let [, level, filename, line] = cmakeMessage;
const { match: callStackMatch } = readUntil(/Call Stack \(most recent call first\)/i);
if (callStackMatch) {
const { match: callFrameMatch } = readUntil(/(CMakeLists\.txt|[^\s]+\.cmake):(\d+)/i, 5);
if (callFrameMatch) {
const [, frame, location] = callFrameMatch;
filename = frame;
line = location;
}
}
const annotation = parseAnnotation({
source: "cmake",
level,
filename,
line,
content: bufferedLines,
});
annotations.push(annotation);
}
// rustc / cargo error
// e.g. error[E0308]: mismatched types
// --> src/http/lib.rs:553:5
// The header line carries the level + (optional) code; the location
// arrives on the following `-->` line (absent for diagnostics without a
// span, e.g. "error: linking with `cc` failed"). The body runs until the
// blank line rustc emits after every diagnostic, so the annotation
// contains the rendered span + help/note lines; the cap is only a guard
// against output that never has one.
const rustHeader = line.match(/^(error|warning)(\[[A-Z0-9]+\])?: (.+)$/);
if (rustHeader && !/\b(generated|emitted)\b/.test(line) /* "warning: 3 warnings emitted" */) {
const [, level, code, title] = rustHeader;
const { lines: body } = readUntil(/^$/, 30);
const locMatch = stripAnsi(body[0] ?? "").match(/-->\s+(.+?):(\d+):(\d+)/);
const annotation = parseAnnotation({
source: "rustc",
level,
filename: locMatch?.[1],
line: locMatch?.[2],
column: locMatch?.[3],
title: code ? `${code} ${title}` : title,
content: bufferedLines,
});
annotations.push(annotation);
continue;
}
const nodeJsError = line.match(/^file:\/\/(.+\.(?:c|m)js):(\d+)/i);
if (nodeJsError) {
const [, filename, line] = nodeJsError;
let metadata;
const { match: nodeJsVersionMatch } = readUntil(/^Node\.js v(\d+\.\d+\.\d+)/i);
if (nodeJsVersionMatch) {
const [, version] = nodeJsVersionMatch;
metadata = {
"node-version": version,
};
}
const annotation = parseAnnotation({
source: "node",
level: "error",
filename,
line,
content: bufferedLines,
metadata,
});
annotations.push(annotation);
}
const clangError = line.match(/^(.+\.(?:cpp|c|m|h)):(\d+):(\d+): (error|warning): (.+)/i);
if (clangError) {
const [, filename, line, column, level] = clangError;
readUntil(/^\d+ (?:error|warning)s? generated/);
const annotation = parseAnnotation({
source: "clang",
level,
filename,
line,
column,
content: bufferedLines,
});
annotations.push(annotation);
}
const shellMessage = line.match(/(.+\.sh): line (\d+): (.+)/i);
if (shellMessage) {
const [, filename, line] = shellMessage;
const annotation = parseAnnotation({
source: "shell",
level: "error",
filename,
line,
content: bufferedLines,
});
annotations.push(annotation);
}
lines.push(originalLine);
}
return {
annotations,
content: lines.join("\n"),
};
}
/**
* @typedef {object} BuildkiteAnnotation
* @property {string} [context]
* @property {string} label
* @property {string} content
* @property {"error" | "warning" | "info"} [style]
* @property {number} [priority]
* @property {number} [attempt]
*/
/**
* @param {BuildkiteAnnotation} annotation
*/
export function reportAnnotationToBuildKite({ context, label, content, style = "error", priority = 3, attempt = 0 }) {
if (!isBuildkite) {
return;
}
// BuildKite rejects annotation contexts > 100 chars (`400 Bad Request: This
// context is too long`). rustc diagnostic titles routinely exceed that and
// were silently dropped, leaving only short warnings visible in the UI.
const ctx = `${context || label}`.slice(0, 100);
const { error, status, signal, stderr } = nodeSpawnSync(
"buildkite-agent",
["annotate", "--append", "--style", `${style}`, "--context", ctx, "--priority", `${priority}`],
{
input: content,
stdio: ["pipe", "ignore", "pipe"],
encoding: "utf-8",
timeout: 30_000,
},
);
if (status === 0) {
return;
}
const cause = error?.message || signal || (status == null ? "timed out" : `exit code ${status}`);
if (attempt === 0) {
console.error(`buildkite-agent annotate failed for '${label}' (${cause}), retrying...`);
return reportAnnotationToBuildKite({ context, label, content, style, priority, attempt: attempt + 1 });
}
// Annotations are best-effort: log and move on rather than throwing, which
// would abort the test runner mid-suite over a cosmetic failure.
console.error(`buildkite-agent annotate failed for '${label}' after retry (${cause}), giving up`);
if (stderr) console.error(stderr);
}
/**
* Mark this Buildkite job as having handled its own failure reporting.
*
* The repository `.buildkite/hooks/pre-exit` hook posts a generic fallback
* annotation for any step that exits non-zero without this marker set, so
* infra failures that happen before (or crash) the runner/build scripts are
* still surfaced in the build's annotation list instead of being visible only
* in the raw job log. Call this from every controlled exit path that has
* already posted (or had nothing to post) so the fallback stays quiet. The
* marker is build meta-data, which is server-side and so remains visible to
* the host pre-exit hook even when the reporter ran inside an ephemeral VM.
*/
export function markBuildkiteStepReported() {
if (!isBuildkite) return;
const jobId = getEnv("BUILDKITE_JOB_ID", false);
if (!jobId) return;
const { status } = nodeSpawnSync("buildkite-agent", ["meta-data", "set", `reported-${jobId}`, "1"], {
stdio: "ignore",
timeout: 30_000,
});
if (status !== 0) {
console.error(`buildkite-agent meta-data set reported-${jobId} failed (non-fatal)`);
}
}
/**
* @param {object} obj
* @param {number} indent
* @returns {string}
*/
export function toYaml(obj, indent = 0) {
const spaces = " ".repeat(indent);
let result = "";
for (const [key, value] of Object.entries(obj)) {
if (value === undefined) {
continue;
}
if (value === null) {
result += `${spaces}${key}: null\n`;
continue;
}
if (Array.isArray(value)) {
result += `${spaces}${key}:\n`;
value.forEach(item => {
if (typeof item === "object" && item !== null) {
result += `${spaces}- \n${toYaml(item, indent + 2)
.split("\n")
.map(line => `${spaces} ${line}`)
.join("\n")}\n`;
} else {
result += `${spaces}- ${item}\n`;
}
});
continue;
}
if (typeof value === "object") {
result += `${spaces}${key}:\n${toYaml(value, indent + 2)}`;
continue;
}
if (
typeof value === "string" &&
(value.includes(":") ||
value.includes("#") ||
value.includes("'") ||
value.includes('"') ||
value.includes("\\") ||
value.includes("\n") ||
value.includes("*") ||
value.includes("&") ||
value.includes("!") ||
value.includes("|") ||
value.includes(">") ||
value.includes("%") ||
value.includes("@") ||
value.includes("`") ||
value.includes("{") ||
value.includes("}") ||
value.includes("[") ||
value.includes("]") ||
value.includes(",") ||
value.includes(";"))
) {
result += `${spaces}${key}: "${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"\n`;
continue;
}
result += `${spaces}${key}: ${value}\n`;
}
return result;
}
/** @type {string | undefined} */
let lastGroup;
/**
* @param {string} title
* @param {function} [fn]
*/
export function startGroup(title, fn) {
if (lastGroup && lastGroup !== title) {
lastGroup = title;
endGroup();
}
if (isGithubAction) {
console.log(`::group::${stripAnsi(title)}`);
} else if (isBuildkite) {
console.log(`--- ${title}`);
} else {
console.group(title);
}
if (typeof fn === "function") {
let result;
try {
result = fn();
} finally {
if (result instanceof Promise) {
return result.finally(() => endGroup());
} else {
endGroup();
}
}
}
}
export function endGroup() {
if (lastGroup) {
lastGroup = undefined;
}
if (isGithubAction) {
console.log("::endgroup::");
} else {
console.groupEnd();
}
// when a file exits with an ASAN error, there is no trailing newline so we add one here to make sure `console.group()` detection doesn't get broken in CI.
console.log();
}
export function printEnvironment() {
startGroup("Machine", () => {
console.log("Operating System:", getOs());
console.log("Architecture:", getArch());
console.log("Kernel:", getKernel());
if (isLinux) {
console.log("ABI:", getAbi());
console.log("ABI Version:", getAbiVersion());
}
console.log("Distro:", getDistro());
console.log("Distro Version:", getDistroVersion());
console.log("Hostname:", getHostname());
if (isCI) {
console.log("Tailscale IP:", getTailscaleIp());
console.log("Public IP:", getPublicIp());
}
console.log("Username:", getUsername());
console.log("Working Directory:", process.cwd());
console.log("Temporary Directory:", tmpdir());
if (process.isBun) {
console.log("Bun Version:", Bun.version, Bun.revision);
} else {
console.log("Node Version:", process.version);
}
});
if (isCI) {
startGroup("Environment", () => {
for (const [key, value] of Object.entries(process.env).toSorted()) {
console.log(`${key}:`, value);
}
});
if (isPosix) {
startGroup("Limits", () => {
const shell = which(["sh", "bash"]);
if (shell) {
spawnSync([shell, "-c", "ulimit -a"], { stdio: "inherit" });
}
});
startGroup("Disk (df)", () => {
const shell = which(["sh", "bash"]);
if (shell) {
spawnSync([shell, "-c", "df"], { stdio: "inherit" });
}
});
}
if (isLinux) {
startGroup("Memory", () => {
const shell = which(["sh", "bash"]);
if (shell) {
spawnSync([shell, "-c", "free -m -w"], { stdio: "inherit" });
}
});
startGroup("Docker", () => {
const shell = which(["sh", "bash"]);
if (shell) {
spawnSync([shell, "-c", "docker ps"], { stdio: "inherit" });
}
});
}
if (isWindows) {
startGroup("Disk (win)", () => {
const shell = which(["pwsh"]);
if (shell) {
spawnSync([shell, "-c", "get-psdrive"], { stdio: "inherit" });
}
});
startGroup("Memory", () => {
const shell = which(["pwsh"]);
if (shell) {
spawnSync([shell, "-c", "Get-Counter '\\Memory\\Available MBytes'"], { stdio: "inherit" });
console.log();
spawnSync([shell, "-c", "Get-CimInstance Win32_PhysicalMemory"], { stdio: "inherit" });
}
});
}
}
startGroup("Repository", () => {
console.log("Commit:", getCommit());
console.log("Message:", getCommitMessage());
console.log("Branch:", getBranch());
console.log("Main Branch:", getMainBranch());
console.log("Is Fork:", isFork());
console.log("Is Merge Queue:", isMergeQueue());
console.log("Is Main Branch:", isMainBranch());
console.log("Is Pull Request:", isPullRequest());
if (isPullRequest()) {
console.log("Pull Request:", getPullRequest());
console.log("Target Branch:", getTargetBranch());
}
});
if (isCI) {
startGroup("CI", () => {
console.log("Build ID:", getBuildId());
console.log("Build Label:", getBuildLabel());
console.log("Build URL:", getBuildUrl()?.toString());
});
}
}
/**
* @returns {number | undefined}
*/
export function getLoggedInUserCountOrDetails() {
if (isWindows) {
const pwsh = which(["pwsh", "powershell"]);
if (pwsh) {
const { error, stdout } = spawnSync([
pwsh,
"-Command",
`Get-CimInstance -ClassName Win32_Process -Filter "Name = 'sshd.exe'" | Get-CimAssociatedInstance -Association Win32_SessionProcess | Get-CimAssociatedInstance -Association Win32_LoggedOnUser | Where-Object {$_.Name -ne 'SYSTEM'} | Measure-Object | Select-Object -ExpandProperty Count`,
]);
if (!error) {
return parseInt(stdout) || undefined;
}
}
}
const { error, stdout } = spawnSync(["who"]);
if (!error) {
const users = stdout
.split("\n")
.filter(line => /tty|pts/i.test(line))
// Only count REMOTE logins (have an `(ip)` suffix from sshd). A local
// console/auto-login (e.g. cirruslabs CI VM images log the admin user in
// on ttys000 at boot) has no source host and isn't a human debugging the
// job — waiting for it would hang the runner forever.
.filter(line => /\([^)]+\)\s*$/.test(line))
.map(line => {
// `who` output: `username terminal date time (host)`. The date/time
// field has spaces, so a plain split() can't slice it cleanly — take
// the first two tokens and pull the host from the trailing `(...)`.
const [username, terminal] = line.split(/\s+/);
const ip = line.match(/\(([^)]+)\)\s*$/)?.[1] || "";
return { username, terminal, ip };
});
if (users.length === 0) {
return 0;
}
let message = `${users.length} currently logged in users:`;
for (const user of users) {
message += `\n- ${user.username} on ${user.terminal}${user.ip ? ` from ${user.ip}` : ""}`;
}
return message;
}
}
/** @typedef {keyof typeof emojiMap} Emoji */
const emojiMap = {
darwin: ["🍎", "darwin"],
linux: ["🐧", "linux"],
debian: ["🐧", "debian"],
ubuntu: ["🐧", "ubuntu"],
alpine: ["🐧", "alpine"],
aws: ["☁️", "aws"],
amazonlinux: ["🐧", "aws"],
nix: ["🐧", "nix"],
windows: ["🪟", "windows"],
true: ["✅", "white_check_mark"],
false: ["❌", "x"],
debug: ["🐞", "bug"],
asan: ["🐛", "bug"],
assert: ["🔍", "mag"],
release: ["🏆", "trophy"],
gear: ["⚙️", "gear"],
clipboard: ["📋", "clipboard"],
package: ["📦", "package"],
rocket: ["🚀", "rocket"],
openbsd: ["🐡", "openbsd"],
netbsd: ["🚩", "netbsd"],
freebsd: ["😈", "freebsd"],
};
/**
* @param {Emoji} emoji
* @returns {string}
*/
export function getEmoji(emoji) {
const [unicode] = emojiMap[emoji] || [];
return unicode || "";
}
/**
* @param {Emoji} emoji
* @returns {string}
* @link https://github.com/buildkite/emojis#emoji-reference
*/
export function getBuildkiteEmoji(emoji) {
const [, name] = emojiMap[emoji] || [];
return name ? `:${name}:` : "";
}
/**
* @param {SshOptions} options
* @param {import("./utils.mjs").SpawnOptions} [spawnOptions]
* @returns {Promise<import("./utils.mjs").SpawnResult>}
*/
export async function spawnSshSafe(options, spawnOptions = {}) {
return spawnSsh(options, { throwOnError: true, ...spawnOptions });
}
/**
* @param {SshOptions} options
* @param {import("./utils.mjs").SpawnOptions} [spawnOptions]
* @returns {Promise<import("./utils.mjs").SpawnResult>}
*/
export async function spawnSsh(options, spawnOptions = {}) {
const { hostname, port, username, identityPaths, password, retries = 10, command: spawnCommand } = options;
if (!hostname.includes("@")) {
await waitForPort({
hostname,
port: port || 22,
});
}
const logPath = mkdtemp("ssh-", "ssh.log");
const command = ["ssh", hostname, "-v", "-C", "-E", logPath, "-o", "StrictHostKeyChecking=no"];
if (!password) {
command.push("-o", "BatchMode=yes");
}
if (port) {
command.push("-p", port);
}
if (username) {
command.push("-l", username);
}
if (password) {
const sshPass = which("sshpass", { required: true });
command.unshift(sshPass, "-p", password);
} else if (identityPaths) {
command.push(...identityPaths.flatMap(path => ["-i", path]));
}
const stdio = spawnCommand ? "pipe" : "inherit";
if (spawnCommand) {
command.push(...spawnCommand);
}
/** @type {import("./utils.mjs").SpawnResult} */
let result;
for (let i = 0; i < retries; i++) {
result = await spawn(command, { stdio, ...spawnOptions, throwOnError: undefined });
const { exitCode } = result;
if (exitCode !== 255) {
break;
}
const sshLogs = readFile(logPath, { encoding: "utf-8" });
if (sshLogs.includes("Authenticated")) {
break;
}
await new Promise(resolve => setTimeout(resolve, (i + 1) * 15000));
}
if (spawnOptions?.throwOnError) {
const { error } = result;
if (error) {
throw error;
}
}
return result;
}
/**
* @param {MachineOptions} options
* @returns {Promise<Machine>}
*/
export async function setupUserData(machine, options) {
const { os, userData } = options;
if (!userData) {
return;
}
// Write user data to a temporary file
const tmpFile = mkdtemp("user-data-", os === "windows" ? "setup.ps1" : "setup.sh");
await writeFile(tmpFile, userData);
try {
// Upload the script
const remotePath = os === "windows" ? "C:\\Windows\\Temp\\setup.ps1" : "/tmp/setup.sh";
await machine.upload(tmpFile, remotePath);
// Execute the script
if (os === "windows") {
await machine.spawnSafe(["powershell", remotePath], { stdio: "inherit" });
} else {
await machine.spawnSafe(["bash", remotePath], { stdio: "inherit" });
}
} finally {
// Clean up the temporary file
rm(tmpFile);
}
}