feat: support for multiple registries

This commit is contained in:
Jozef Steinhübl 2024-11-16 11:56:08 +01:00
parent 8f1bc2eeb3
commit 7e228fa502
No known key found for this signature in database
GPG Key ID: E6BC90C91973B08F
6 changed files with 331 additions and 21 deletions

View File

@ -33,10 +33,26 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
tests:
name: Tests
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Bun
uses: ./
- name: Install dependencies
run: bun install
- name: Run tests
run: bun test --coverage
setup-bun:
runs-on: ${{ matrix.os }}
continue-on-error: true
needs: [remove-cache]
needs: [remove-cache, tests]
strategy:
matrix:
os:
@ -73,7 +89,7 @@ jobs:
name: setup-bun from (${{ matrix.os }}, ${{ matrix.file.name }})
runs-on: ${{ matrix.os }}
continue-on-error: true
needs: [remove-cache]
needs: [remove-cache, tests]
strategy:
matrix:
os:
@ -130,7 +146,7 @@ jobs:
name: setup-bun from (${{ matrix.os }}, download url)
runs-on: ${{ matrix.os }}
continue-on-error: true
needs: [remove-cache]
needs: [remove-cache, tests]
strategy:
matrix:
os:

View File

@ -21,6 +21,9 @@ inputs:
scope:
required: false
description: "The scope for authenticating with the package registry."
registries:
required: false
description: "An object of package registries mapped by scope for authenticating with the package registry."
no-cache:
required: false
type: boolean

View File

@ -12,7 +12,7 @@ import { addPath, info, warning } from "@actions/core";
import { isFeatureAvailable, restoreCache } from "@actions/cache";
import { downloadTool, extractZip } from "@actions/tool-cache";
import { getExecOutput } from "@actions/exec";
import { writeBunfig } from "./bunfig";
import { writeBunfig, Registry } from "./bunfig";
import { saveState } from "@actions/core";
import { addExtension, retry } from "./utils";
@ -23,8 +23,7 @@ export type Input = {
arch?: string;
avx2?: boolean;
profile?: boolean;
scope?: string;
registryUrl?: string;
registries?: Registry[];
noCache?: boolean;
};
@ -45,7 +44,7 @@ export type CacheState = {
export default async (options: Input): Promise<Output> => {
const bunfigPath = join(process.cwd(), "bunfig.toml");
writeBunfig(bunfigPath, options);
writeBunfig(bunfigPath, options.registries);
const url = getDownloadUrl(options);
const cacheEnabled = isCacheEnabled(options);

View File

@ -1,21 +1,32 @@
import { EOL } from "node:os";
import { appendFileSync } from "node:fs";
import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { info } from "@actions/core";
type BunfigOptions = {
registryUrl?: string;
scope?: string;
export type Registry = {
url: string;
scope: string;
token: string;
};
export function createBunfig(options: BunfigOptions): string | null {
const { registryUrl, scope } = options;
enum FieldType {
GLOBAL_REGISTRY,
INSTALL_WITH_SCOPE,
}
type Field = {
type: FieldType;
value: string;
};
export function createField(registry: Registry): Field {
const { url: registryUrl, scope, token } = registry;
let url: URL | undefined;
if (registryUrl) {
try {
url = new URL(registryUrl);
} catch {
throw new Error(`Invalid registry-url: ${registryUrl}`);
throw new Error(`Invalid registry url ${registryUrl}`);
}
}
@ -27,24 +38,131 @@ export function createBunfig(options: BunfigOptions): string | null {
}
if (url && owner) {
return `[install.scopes]${EOL}'${owner}' = { token = "$BUN_AUTH_TOKEN", url = "${url}"}${EOL}`;
return {
type: FieldType.INSTALL_WITH_SCOPE,
value: `'${owner}' = { token = "${token}", url = "${url}" }`,
};
}
if (url && !owner) {
return `[install]${EOL}registry = "${url}"${EOL}`;
return {
type: FieldType.GLOBAL_REGISTRY,
value: `registry = "${url}"`,
};
}
return null;
}
export function writeBunfig(path: string, options: BunfigOptions): void {
const bunfig = createBunfig(options);
export function createBunfig(registries: Registry[]): Field[] | null {
const fields = registries.map(createField).filter((field) => field);
if (fields.length === 0) {
return null;
}
if (
fields.filter((field) => field.type === FieldType.GLOBAL_REGISTRY).length >
1
) {
throw new Error("You can't have more than one global registry.");
}
return fields;
}
export function serializeInstallScopes(
fields: Field[],
header: boolean = false
): string {
const installScopes = fields
.filter((field) => field.type === FieldType.INSTALL_WITH_SCOPE)
.map((field) => field.value)
.join(EOL);
if (!installScopes) {
return "";
}
return `${header ? `[install.scopes]${EOL}` : ""}${installScopes}${EOL}`;
}
export function serializeGlobalRegistry(
fields: Field[],
header: boolean = false
): string {
const globalRegistry = fields
.filter((field) => field.type === FieldType.GLOBAL_REGISTRY)
.map((field) => field.value)
.join(EOL);
if (!globalRegistry) {
return "";
}
return `${header ? `[install]${EOL}` : ""}${globalRegistry}${EOL}`;
}
export function writeBunfig(path: string, registries: Registry[]): void {
const bunfig = createBunfig(registries);
if (!bunfig) {
return;
}
info(`Writing bunfig.toml to '${path}'.`);
appendFileSync(path, bunfig, {
if (!existsSync(path)) {
writeFileSync(
path,
`${serializeGlobalRegistry(bunfig, true)}${serializeInstallScopes(
bunfig,
true
)}`,
{
encoding: "utf8",
}
);
return;
}
let newContent = "";
const contents = readFileSync(path, {
encoding: "utf-8",
}).split(EOL);
contents.forEach((line, index, array) => {
if (index > 0 && array[index - 1].includes("[install.scopes]")) {
newContent += serializeInstallScopes(bunfig);
}
if (index > 0 && array[index - 1].includes("[install]")) {
newContent += serializeGlobalRegistry(bunfig);
}
if (
!bunfig.some(
(field) =>
field.type === FieldType.INSTALL_WITH_SCOPE &&
(line.startsWith(field.value.split(" ")[0]) ||
((line[0] === "'" || line[0] === '"') &&
line
.toLowerCase()
.startsWith(field.value.split(" ")[0].slice(1).slice(0, -1))))
)
) {
newContent += line + EOL;
}
});
if (!contents.includes("[install.scopes]")) {
newContent += serializeInstallScopes(bunfig, true);
}
if (!contents.includes("[install]")) {
newContent += serializeGlobalRegistry(bunfig, true);
}
writeFileSync(path, newContent, {
encoding: "utf8",
});
}

View File

@ -7,14 +7,30 @@ if (!process.env.RUNNER_TEMP) {
process.env.RUNNER_TEMP = tmpdir();
}
const registries = JSON.parse(getInput("registries") || "[]");
const registryUrl = getInput("registry-url");
const scope = getInput("scope");
if (registries.length > 0 && (registryUrl || scope)) {
setFailed("Cannot specify both 'registries' and 'registry-url' or 'scope'.");
process.exit(1);
}
if (registryUrl) {
registries.push({
url: registryUrl,
scope: scope,
token: "$$BUN_AUTH_TOKEN",
});
}
runAction({
version:
getInput("bun-version") ||
readVersionFromFile(getInput("bun-version-file")) ||
undefined,
customUrl: getInput("bun-download-url") || undefined,
registryUrl: getInput("registry-url") || undefined,
scope: getInput("scope") || undefined,
registries: registries.length > 0 ? registries : undefined,
noCache: getBooleanInput("no-cache") || false,
})
.then(({ version, revision, bunPath, url, cacheHit }) => {

158
tests/bunfig.spec.ts Normal file
View File

@ -0,0 +1,158 @@
import { afterEach, describe, expect, it } from "bun:test";
import { unlink } from "fs";
import { writeBunfig } from "../src/bunfig";
import { EOL } from "os";
describe("writeBunfig", () => {
const filePath = "bunfig.toml";
async function getFileAndContents() {
const file = Bun.file(filePath);
const contents = (await file.text()).split(EOL);
return { file, contents };
}
afterEach(() => {
unlink(filePath, () => console.log(`${filePath} was deleted`));
});
describe("when no bunfig.toml file exists", () => {
it("should create a new file with scopes content", async () => {
writeBunfig(filePath, [
{
url: "https://npm.pkg.github.com",
scope: "foo-bar",
token: "$BUN_AUTH_TOKEN",
},
]);
const { file, contents } = await getFileAndContents();
expect(file.exists()).resolves.toBeTrue();
const expectedContents = [
"[install.scopes]",
'\'@foo-bar\' = { token = "$BUN_AUTH_TOKEN", url = "https://npm.pkg.github.com/" }',
"",
];
contents.forEach((content, index) =>
expect(content).toBe(expectedContents[index])
);
expect(contents.length).toBe(expectedContents.length);
});
});
describe("when local bunfig.toml file exists", () => {
it("and no [install.scopes] exists, should concatenate file correctly", async () => {
const bunfig = `[install]${EOL}optional = true${EOL}${EOL}[install.cache]${EOL}disable = true`;
await Bun.write(filePath, bunfig);
writeBunfig(filePath, [
{
url: "https://npm.pkg.github.com",
scope: "foo-bar",
token: "$BUN_AUTH_TOKEN",
},
]);
const { file, contents } = await getFileAndContents();
expect(file.exists()).resolves.toBeTrue();
const expectedContents = [
"[install]",
"optional = true",
"",
"[install.cache]",
"disable = true",
"[install.scopes]",
'\'@foo-bar\' = { token = "$BUN_AUTH_TOKEN", url = "https://npm.pkg.github.com/" }',
"",
];
contents.forEach((content, index) =>
expect(content).toBe(expectedContents[index])
);
expect(contents.length).toBe(expectedContents.length);
});
it("and [install.scopes] exists and it's not the same registry, should concatenate file correctly", async () => {
const bunfig = `[install]${EOL}optional = true${EOL}${EOL}[install.scopes]${EOL}'@bla-ble' = { token = "$BUN_AUTH_TOKEN", url = "https://npm.pkg.github.com/" }${EOL}${EOL}[install.cache]${EOL}disable = true`;
await Bun.write(filePath, bunfig);
writeBunfig(filePath, [
{
url: "https://npm.pkg.github.com",
scope: "foo-bar",
token: "$BUN_AUTH_TOKEN",
},
]);
const { file, contents } = await getFileAndContents();
expect(file.exists()).resolves.toBeTrue();
const expectedContents = [
"[install]",
"optional = true",
"",
"[install.scopes]",
'\'@foo-bar\' = { token = "$BUN_AUTH_TOKEN", url = "https://npm.pkg.github.com/" }',
'\'@bla-ble\' = { token = "$BUN_AUTH_TOKEN", url = "https://npm.pkg.github.com/" }',
"",
"[install.cache]",
"disable = true",
"",
];
contents.forEach((content, index) =>
expect(content).toBe(expectedContents[index])
);
expect(contents.length).toBe(expectedContents.length);
});
it("and [install.scopes] exists and it's the same registry, should concatenate file correctly", async () => {
const bunfig = `[install]${EOL}optional = true${EOL}${EOL}[install.scopes]${EOL}'@foo-bar' = { token = "$BUN_AUTH_TOKEN", url = "https://npm.pkg.github.com/" }${EOL}'@bla-ble' = { token = "$BUN_AUTH_TOKEN", url = "https://npm.pkg.github.com/" }${EOL}${EOL}[install.cache]${EOL}disable = true`;
await Bun.write(filePath, bunfig);
writeBunfig(filePath, [
{
url: "https://npm.pkg.github.com",
scope: "foo-bar",
token: "$BUN_AUTH_TOKEN",
},
]);
const { file, contents } = await getFileAndContents();
expect(file.exists()).resolves.toBeTrue();
const expectedContents = [
"[install]",
"optional = true",
"",
"[install.scopes]",
'\'@foo-bar\' = { token = "$BUN_AUTH_TOKEN", url = "https://npm.pkg.github.com/" }',
'\'@bla-ble\' = { token = "$BUN_AUTH_TOKEN", url = "https://npm.pkg.github.com/" }',
"",
"[install.cache]",
"disable = true",
"",
];
contents.forEach((content, index) =>
expect(content).toBe(expectedContents[index])
);
expect(contents.length).toBe(expectedContents.length);
});
});
});