From 7e228fa5028171fa2bfc3cd13cb4919625b89597 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jozef=20Steinh=C3=BCbl?= Date: Sat, 16 Nov 2024 11:56:08 +0100 Subject: [PATCH] feat: support for multiple registries --- .github/workflows/test.yml | 22 +++++- action.yml | 3 + src/action.ts | 7 +- src/bunfig.ts | 142 ++++++++++++++++++++++++++++++--- src/index.ts | 20 ++++- tests/bunfig.spec.ts | 158 +++++++++++++++++++++++++++++++++++++ 6 files changed, 331 insertions(+), 21 deletions(-) create mode 100644 tests/bunfig.spec.ts diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cb9ac3a..a2d5be1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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: diff --git a/action.yml b/action.yml index 5c1d85e..f1366bf 100644 --- a/action.yml +++ b/action.yml @@ -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 diff --git a/src/action.ts b/src/action.ts index 2e4fdae..9db6df4 100644 --- a/src/action.ts +++ b/src/action.ts @@ -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 => { const bunfigPath = join(process.cwd(), "bunfig.toml"); - writeBunfig(bunfigPath, options); + writeBunfig(bunfigPath, options.registries); const url = getDownloadUrl(options); const cacheEnabled = isCacheEnabled(options); diff --git a/src/bunfig.ts b/src/bunfig.ts index b408f5f..3b0cb94 100644 --- a/src/bunfig.ts +++ b/src/bunfig.ts @@ -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", }); } diff --git a/src/index.ts b/src/index.ts index e7578d8..8c091ea 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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 }) => { diff --git a/tests/bunfig.spec.ts b/tests/bunfig.spec.ts new file mode 100644 index 0000000..3230860 --- /dev/null +++ b/tests/bunfig.spec.ts @@ -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); + }); + }); +});