mirror of
https://github.com/oven-sh/setup-bun.git
synced 2025-07-21 14:08:24 +02:00
Clean up
This commit is contained in:
parent
830e319e28
commit
8f31cd5980
13 changed files with 403 additions and 78879 deletions
204
src/action.ts
204
src/action.ts
|
@ -1,42 +1,176 @@
|
|||
import { tmpdir } from "node:os";
|
||||
import * as action from "@actions/core";
|
||||
import setup from "./setup.js";
|
||||
import { existsSync, readFileSync } from "fs";
|
||||
import * as path from "path";
|
||||
import { homedir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import {
|
||||
mkdirSync,
|
||||
readdirSync,
|
||||
symlinkSync,
|
||||
renameSync,
|
||||
copyFileSync,
|
||||
readFileSync,
|
||||
} from "node:fs";
|
||||
import { addPath, info, warning } from "@actions/core";
|
||||
import { isFeatureAvailable, restoreCache, saveCache } from "@actions/cache";
|
||||
import { downloadTool, extractZip } from "@actions/tool-cache";
|
||||
import { getExecOutput } from "@actions/exec";
|
||||
import { writeBunfig } from "./bunfig";
|
||||
|
||||
if (!process.env.RUNNER_TEMP) {
|
||||
process.env.RUNNER_TEMP = tmpdir();
|
||||
export type Input = {
|
||||
customUrl?: string;
|
||||
version?: string;
|
||||
os?: string;
|
||||
arch?: string;
|
||||
avx2?: boolean;
|
||||
profile?: boolean;
|
||||
scope?: string;
|
||||
registryUrl?: string;
|
||||
};
|
||||
|
||||
export type Output = {
|
||||
version: string;
|
||||
revision: string;
|
||||
cacheHit: boolean;
|
||||
};
|
||||
|
||||
export default async (options: Input): Promise<Output> => {
|
||||
const bunfigPath = join(process.cwd(), "bunfig.toml");
|
||||
writeBunfig(bunfigPath, options);
|
||||
|
||||
const url = getDownloadUrl(options);
|
||||
const cacheEnabled = isCacheEnabled(options);
|
||||
|
||||
const binPath = join(homedir(), ".bun", "bin");
|
||||
try {
|
||||
mkdirSync(binPath, { recursive: true });
|
||||
} catch (error) {
|
||||
if (error.code !== "EEXIST") {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
addPath(binPath);
|
||||
const bunPath = join(binPath, "bun");
|
||||
try {
|
||||
symlinkSync(bunPath, join(binPath, "bunx"));
|
||||
} catch (error) {
|
||||
if (error.code !== "EEXIST") {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
let revision: string | undefined;
|
||||
let cacheHit = false;
|
||||
if (cacheEnabled) {
|
||||
const cacheRestored = await restoreCache([bunPath], url);
|
||||
if (cacheRestored) {
|
||||
revision = await getRevision(bunPath);
|
||||
if (revision) {
|
||||
cacheHit = true;
|
||||
info(`Using a cached version of Bun: ${revision}`);
|
||||
} else {
|
||||
warning(
|
||||
`Found a cached version of Bun: ${revision} (but it appears to be corrupted?)`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!cacheHit) {
|
||||
info(`Downloading a new version of Bun: ${url}`);
|
||||
const zipPath = await downloadTool(url);
|
||||
const extractedZipPath = await extractZip(zipPath);
|
||||
const extractedBunPath = await extractBun(extractedZipPath);
|
||||
try {
|
||||
renameSync(extractedBunPath, bunPath);
|
||||
} catch {
|
||||
// If mv does not work, try to copy the file instead.
|
||||
// For example: EXDEV: cross-device link not permitted
|
||||
copyFileSync(extractedBunPath, bunPath);
|
||||
}
|
||||
revision = await getRevision(bunPath);
|
||||
}
|
||||
|
||||
if (!revision) {
|
||||
throw new Error(
|
||||
"Downloaded a new version of Bun, but failed to check its version? Try again."
|
||||
);
|
||||
}
|
||||
|
||||
if (cacheEnabled && !cacheHit) {
|
||||
try {
|
||||
await saveCache([bunPath], url);
|
||||
} catch (error) {
|
||||
warning("Failed to save Bun to cache.");
|
||||
}
|
||||
}
|
||||
|
||||
const [version] = revision.split("+");
|
||||
return {
|
||||
version,
|
||||
revision,
|
||||
cacheHit,
|
||||
};
|
||||
};
|
||||
|
||||
function isCacheEnabled(options: Input): boolean {
|
||||
const { customUrl, version } = options;
|
||||
if (customUrl) {
|
||||
return false;
|
||||
}
|
||||
if (!version || /latest|canary|action/i.test(version)) {
|
||||
return false;
|
||||
}
|
||||
return isFeatureAvailable();
|
||||
}
|
||||
|
||||
function readVersionFromPackageJson(): string | undefined {
|
||||
const { GITHUB_WORKSPACE } = process.env;
|
||||
if (!GITHUB_WORKSPACE) {
|
||||
return;
|
||||
function getDownloadUrl(options: Input): string {
|
||||
const { customUrl } = options;
|
||||
if (customUrl) {
|
||||
return customUrl;
|
||||
}
|
||||
const pathToPackageJson = path.join(GITHUB_WORKSPACE, "package.json");
|
||||
if (!existsSync(pathToPackageJson)) {
|
||||
return;
|
||||
}
|
||||
const { packageManager } = JSON.parse(
|
||||
readFileSync(pathToPackageJson, "utf8")
|
||||
) as { packageManager?: string };
|
||||
return packageManager?.split("bun@")[1];
|
||||
const { version, os, arch, avx2, profile } = options;
|
||||
const eversion = encodeURIComponent(version ?? "latest");
|
||||
const eos = encodeURIComponent(os ?? process.platform);
|
||||
const earch = encodeURIComponent(arch ?? process.arch);
|
||||
const eavx2 = encodeURIComponent(avx2 ?? true);
|
||||
const eprofile = encodeURIComponent(profile ?? false);
|
||||
const { href } = new URL(
|
||||
`${eversion}/${eos}/${earch}?avx2=${eavx2}&profile=${eprofile}`,
|
||||
"https://bun.sh/download/"
|
||||
);
|
||||
return href;
|
||||
}
|
||||
|
||||
setup({
|
||||
version:
|
||||
readVersionFromPackageJson() || action.getInput("bun-version") || undefined,
|
||||
customUrl: action.getInput("bun-download-url") || undefined,
|
||||
registryUrl: action.getInput("registry-url") || undefined,
|
||||
scope: action.getInput("scope") || undefined,
|
||||
})
|
||||
.then(({ version, revision, cacheHit, registryUrl, scope }) => {
|
||||
action.setOutput("bun-version", version);
|
||||
action.setOutput("bun-revision", revision);
|
||||
action.setOutput("cache-hit", cacheHit);
|
||||
action.setOutput("registry-url", registryUrl);
|
||||
action.setOutput("scope", scope);
|
||||
})
|
||||
.catch((error) => {
|
||||
action.setFailed(error);
|
||||
async function extractBun(path: string): Promise<string> {
|
||||
for (const entry of readdirSync(path, { withFileTypes: true })) {
|
||||
const { name } = entry;
|
||||
const entryPath = join(path, name);
|
||||
if (entry.isFile()) {
|
||||
if (name === "bun" || name === "bun.exe") {
|
||||
return entryPath;
|
||||
}
|
||||
if (/^bun.*\.zip/.test(name)) {
|
||||
const extractedPath = await extractZip(entryPath);
|
||||
return extractBun(extractedPath);
|
||||
}
|
||||
}
|
||||
if (/^bun/.test(name) && entry.isDirectory()) {
|
||||
return extractBun(entryPath);
|
||||
}
|
||||
}
|
||||
throw new Error("Could not find executable: bun");
|
||||
}
|
||||
|
||||
async function getRevision(exe: string): Promise<string | undefined> {
|
||||
const revision = await getExecOutput(exe, ["--revision"], {
|
||||
ignoreReturnCode: true,
|
||||
});
|
||||
if (revision.exitCode === 0 && /^\d+\.\d+\.\d+/.test(revision.stdout)) {
|
||||
return revision.stdout.trim();
|
||||
}
|
||||
const version = await getExecOutput(exe, ["--version"], {
|
||||
ignoreReturnCode: true,
|
||||
});
|
||||
if (version.exitCode === 0 && /^\d+\.\d+\.\d+/.test(version.stdout)) {
|
||||
return version.stdout.trim();
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
|
57
src/auth.ts
57
src/auth.ts
|
@ -1,57 +0,0 @@
|
|||
import { EOL } from "node:os";
|
||||
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import * as core from "@actions/core";
|
||||
|
||||
export function configureAuthentication(registryUrl: string, scope: string) {
|
||||
const bunfigPath = resolve(process.cwd(), "bunfig.toml");
|
||||
|
||||
if (!registryUrl.endsWith("/")) {
|
||||
registryUrl += "/";
|
||||
}
|
||||
|
||||
writeRegistryToConfigFile({ registryUrl, fileLocation: bunfigPath, scope });
|
||||
}
|
||||
|
||||
type WriteRegistryToConfigFile = {
|
||||
registryUrl: string;
|
||||
fileLocation: string;
|
||||
scope: string;
|
||||
};
|
||||
|
||||
function writeRegistryToConfigFile({
|
||||
registryUrl,
|
||||
fileLocation,
|
||||
scope,
|
||||
}: WriteRegistryToConfigFile) {
|
||||
if (scope && scope[0] !== "@") {
|
||||
scope = "@" + scope;
|
||||
}
|
||||
|
||||
if (scope) {
|
||||
scope = scope.toLocaleLowerCase();
|
||||
}
|
||||
|
||||
core.info(`Setting auth in ${fileLocation}`);
|
||||
|
||||
let newContents = "";
|
||||
|
||||
if (existsSync(fileLocation)) {
|
||||
const curContents = readFileSync(fileLocation, "utf8");
|
||||
|
||||
curContents.split(EOL).forEach((line: string) => {
|
||||
// Add current contents unless they are setting the registry
|
||||
if (!line.toLowerCase().startsWith(scope)) {
|
||||
newContents += line + EOL;
|
||||
}
|
||||
});
|
||||
|
||||
newContents += EOL;
|
||||
}
|
||||
|
||||
const bunRegistryString = `'${scope}' = { token = "$BUN_AUTH_TOKEN", url = "${registryUrl}"}`;
|
||||
|
||||
newContents += `[install.scopes]${EOL}${EOL}${bunRegistryString}${EOL}`;
|
||||
|
||||
writeFileSync("./bunfig.toml", newContents);
|
||||
}
|
50
src/bunfig.ts
Normal file
50
src/bunfig.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
import { EOL } from "node:os";
|
||||
import { appendFileSync } from "node:fs";
|
||||
import { info } from "@actions/core";
|
||||
|
||||
type BunfigOptions = {
|
||||
registryUrl?: string;
|
||||
scope?: string;
|
||||
};
|
||||
|
||||
export function createBunfig(options: BunfigOptions): string | null {
|
||||
const { registryUrl, scope } = options;
|
||||
|
||||
let url: URL | undefined;
|
||||
if (registryUrl) {
|
||||
try {
|
||||
url = new URL(registryUrl);
|
||||
} catch {
|
||||
throw new Error(`Invalid registry-url: ${registryUrl}`);
|
||||
}
|
||||
}
|
||||
|
||||
let owner: string | undefined;
|
||||
if (scope) {
|
||||
owner = scope.startsWith("@")
|
||||
? scope.toLocaleLowerCase()
|
||||
: `@${scope.toLocaleLowerCase()}`;
|
||||
}
|
||||
|
||||
if (url && owner) {
|
||||
return `[install.scopes]${EOL}'${owner}' = { token = "$BUN_AUTH_TOKEN", url = "${url}"}${EOL}`;
|
||||
}
|
||||
|
||||
if (url && !owner) {
|
||||
return `[install]${EOL}registry = "${url}"${EOL}`;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function writeBunfig(path: string, options: BunfigOptions): void {
|
||||
const bunfig = createBunfig(options);
|
||||
if (!bunfig) {
|
||||
return;
|
||||
}
|
||||
|
||||
info(`Writing bunfig.toml to '${path}'.`);
|
||||
appendFileSync(path, bunfig, {
|
||||
encoding: "utf8",
|
||||
});
|
||||
}
|
46
src/index.ts
Normal file
46
src/index.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { getInput, setOutput, setFailed, warning } from "@actions/core";
|
||||
import runAction from "./action.js";
|
||||
|
||||
if (!process.env.RUNNER_TEMP) {
|
||||
process.env.RUNNER_TEMP = tmpdir();
|
||||
}
|
||||
|
||||
function readVersionFromPackageJson(): string | undefined {
|
||||
const cwd = process.env.GITHUB_WORKSPACE;
|
||||
if (!cwd) {
|
||||
return;
|
||||
}
|
||||
const path = join(cwd, "package.json");
|
||||
try {
|
||||
if (!existsSync(path)) {
|
||||
return;
|
||||
}
|
||||
const { packageManager } = JSON.parse(readFileSync(path, "utf8"));
|
||||
if (!packageManager?.startsWith("bun@")) {
|
||||
return;
|
||||
}
|
||||
const [_, version] = packageManager.split("bun@");
|
||||
return version;
|
||||
} catch (error) {
|
||||
const { message } = error as Error;
|
||||
warning(`Failed to read package.json: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
runAction({
|
||||
version: getInput("bun-version") || readVersionFromPackageJson() || undefined,
|
||||
customUrl: getInput("bun-download-url") || undefined,
|
||||
registryUrl: getInput("registry-url") || undefined,
|
||||
scope: getInput("scope") || undefined,
|
||||
})
|
||||
.then(({ version, revision, cacheHit }) => {
|
||||
setOutput("bun-version", version);
|
||||
setOutput("bun-revision", revision);
|
||||
setOutput("cache-hit", cacheHit);
|
||||
})
|
||||
.catch((error) => {
|
||||
setFailed(error);
|
||||
});
|
158
src/setup.ts
158
src/setup.ts
|
@ -1,158 +0,0 @@
|
|||
import { homedir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { readdir, symlink } from "node:fs/promises";
|
||||
import * as action from "@actions/core";
|
||||
import * as cache from "@actions/cache";
|
||||
import { downloadTool, extractZip } from "@actions/tool-cache";
|
||||
import { cp, mkdirP, rmRF } from "@actions/io";
|
||||
import { getExecOutput } from "@actions/exec";
|
||||
import { configureAuthentication } from "./auth";
|
||||
|
||||
export default async (options?: {
|
||||
version?: string;
|
||||
customUrl?: string;
|
||||
scope?: string;
|
||||
registryUrl?: string;
|
||||
}): Promise<{
|
||||
version: string;
|
||||
revision: string;
|
||||
cacheHit: boolean;
|
||||
scope?: string;
|
||||
registryUrl?: string;
|
||||
}> => {
|
||||
const { url, cacheKey } = getDownloadUrl(options);
|
||||
const cacheEnabled = cacheKey && cache.isFeatureAvailable();
|
||||
const dir = join(homedir(), ".bun", "bin");
|
||||
action.addPath(dir);
|
||||
const path = join(dir, "bun");
|
||||
let revision: string | undefined;
|
||||
let cacheHit = false;
|
||||
if (cacheEnabled) {
|
||||
const cacheRestored = await cache.restoreCache([path], cacheKey);
|
||||
if (cacheRestored) {
|
||||
revision = await verifyBun(path);
|
||||
if (revision) {
|
||||
cacheHit = true;
|
||||
action.info("Using a cached version of Bun.");
|
||||
} else {
|
||||
action.warning(
|
||||
"Found a cached version of Bun, but it appears to be corrupted? Attempting to download a new version."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!cacheHit) {
|
||||
action.info(`Downloading a new version of Bun: ${url}`);
|
||||
const zipPath = await downloadTool(url);
|
||||
const extractedPath = await extractZip(zipPath);
|
||||
const exePath = await extractBun(extractedPath);
|
||||
await mkdirP(dir);
|
||||
await cp(exePath, path);
|
||||
await rmRF(exePath);
|
||||
revision = await verifyBun(path);
|
||||
}
|
||||
try {
|
||||
await symlink(path, join(dir, "bunx"));
|
||||
} catch (error) {
|
||||
if (error.code !== "EEXIST") {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
if (!revision) {
|
||||
throw new Error(
|
||||
"Downloaded a new version of Bun, but failed to check its version? Try again in debug mode."
|
||||
);
|
||||
}
|
||||
if (cacheEnabled) {
|
||||
try {
|
||||
await cache.saveCache([path], cacheKey);
|
||||
} catch (error) {
|
||||
action.warning("Failed to save Bun to cache.");
|
||||
}
|
||||
}
|
||||
const [version] = revision.split("+");
|
||||
|
||||
const { registryUrl, scope } = options;
|
||||
|
||||
if (!!registryUrl && !!scope) {
|
||||
configureAuthentication(registryUrl, scope);
|
||||
}
|
||||
|
||||
return {
|
||||
version,
|
||||
revision,
|
||||
cacheHit,
|
||||
registryUrl,
|
||||
scope,
|
||||
};
|
||||
};
|
||||
|
||||
function getDownloadUrl(options?: {
|
||||
customUrl?: string;
|
||||
version?: string;
|
||||
os?: string;
|
||||
arch?: string;
|
||||
avx2?: boolean;
|
||||
profile?: boolean;
|
||||
}): {
|
||||
url: string;
|
||||
cacheKey: string | null;
|
||||
} {
|
||||
if (options?.customUrl) {
|
||||
return {
|
||||
url: options.customUrl,
|
||||
cacheKey: null,
|
||||
};
|
||||
}
|
||||
const release = encodeURIComponent(options?.version ?? "latest");
|
||||
const os = encodeURIComponent(options?.os ?? process.platform);
|
||||
const arch = encodeURIComponent(options?.arch ?? process.arch);
|
||||
const avx2 = encodeURIComponent(options?.avx2 ?? true);
|
||||
const profile = encodeURIComponent(options?.profile ?? false);
|
||||
const { href } = new URL(
|
||||
`${release}/${os}/${arch}?avx2=${avx2}&profile=${profile}`,
|
||||
"https://bun.sh/download/"
|
||||
);
|
||||
return {
|
||||
url: href,
|
||||
cacheKey: /^latest|canary|action/i.test(release)
|
||||
? null
|
||||
: `bun-${release}-${os}-${arch}-${avx2}-${profile}`,
|
||||
};
|
||||
}
|
||||
|
||||
async function extractBun(path: string): Promise<string> {
|
||||
for (const entry of await readdir(path, { withFileTypes: true })) {
|
||||
const { name } = entry;
|
||||
const entryPath = join(path, name);
|
||||
if (entry.isFile()) {
|
||||
if (name === "bun") {
|
||||
return entryPath;
|
||||
}
|
||||
if (/^bun.*\.zip/.test(name)) {
|
||||
const extractedPath = await extractZip(entryPath);
|
||||
return extractBun(extractedPath);
|
||||
}
|
||||
}
|
||||
if (/^bun/.test(name) && entry.isDirectory()) {
|
||||
return extractBun(entryPath);
|
||||
}
|
||||
}
|
||||
throw new Error("Could not find executable: bun");
|
||||
}
|
||||
|
||||
async function verifyBun(path: string): Promise<string | undefined> {
|
||||
const revision = await getExecOutput(path, ["--revision"], {
|
||||
ignoreReturnCode: true,
|
||||
});
|
||||
if (revision.exitCode === 0 && /^\d+\.\d+\.\d+/.test(revision.stdout)) {
|
||||
return revision.stdout.trim();
|
||||
}
|
||||
const version = await getExecOutput(path, ["--version"], {
|
||||
ignoreReturnCode: true,
|
||||
});
|
||||
if (version.exitCode === 0 && /^\d+\.\d+\.\d+/.test(version.stdout)) {
|
||||
return version.stdout.trim();
|
||||
}
|
||||
return undefined;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue