Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,33 @@ runs:
# actions/setup-node uses glibc Node builds, so install Alpine's Node/npm instead.
if command -v apk >/dev/null 2>&1; then
missing_packages=""
for package in libstdc++ libgcc nodejs npm; do
prefer_apk_bin=false
for package in libstdc++ libgcc; do
if ! apk info -e "${package}" >/dev/null 2>&1; then
missing_packages="${missing_packages} ${package}"
fi
done

if ! command -v node >/dev/null 2>&1 || ! node -p 'process.versions.node' >/dev/null 2>&1; then
if [ -x /usr/bin/node ] && PATH="/usr/bin:${PATH}" /usr/bin/node -p 'process.versions.node' >/dev/null 2>&1; then
prefer_apk_bin=true
else
missing_packages="${missing_packages} nodejs"
fi
fi

if ! command -v npm >/dev/null 2>&1 || ! npm --version >/dev/null 2>&1; then
if [ -x /usr/bin/npm ] && PATH="/usr/bin:${PATH}" /usr/bin/npm --version >/dev/null 2>&1; then
prefer_apk_bin=true
else
missing_packages="${missing_packages} npm"
fi
fi

if [ "${prefer_apk_bin}" = "true" ]; then
echo "/usr/bin" >> "$GITHUB_PATH"
fi

if [ -z "${missing_packages}" ]; then
exit 0
fi
Expand All @@ -77,6 +98,9 @@ runs:
fi

apk add --no-cache ${missing_packages}
if echo " ${missing_packages} " | grep -Eq ' (nodejs|npm) '; then
echo "/usr/bin" >> "$GITHUB_PATH"
fi
exit 0
fi

Expand Down
149 changes: 146 additions & 3 deletions src/main.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import process from "node:process";
Expand All @@ -7,6 +7,7 @@ import * as core from "@actions/core";

const CLI_CONFIG_REGISTRY = "SUPABASE_INTERNAL_IMAGE_REGISTRY";
const originalPath = process.env.PATH;
const originalNpmUserconfig = process.env.NPM_CONFIG_USERCONFIG;
const originalRunnerTemp = process.env.RUNNER_TEMP;
const originalWorkspace = process.env.GITHUB_WORKSPACE;
const tempDirs = new Set<string>();
Expand All @@ -15,14 +16,23 @@ let mainModule: typeof import("./main.ts") | null = null;
afterEach(() => {
mock.restore();
process.env.PATH = originalPath;
if (originalNpmUserconfig === undefined) {
delete process.env.NPM_CONFIG_USERCONFIG;
} else {
process.env.NPM_CONFIG_USERCONFIG = originalNpmUserconfig;
}
process.env.RUNNER_TEMP = originalRunnerTemp;
process.env.GITHUB_WORKSPACE = originalWorkspace;
delete process.env.FAKE_CLI_VERSION;
delete process.env.FAKE_NPM_BIN;
delete process.env.FAKE_NPM_INTEGRITY;
delete process.env.FAKE_NPM_CWD_LOG;
delete process.env.FAKE_NPM_ENV_LOG;
delete process.env.FAKE_NPM_LOG;
delete process.env.FAKE_NPM_PACKAGE_VERSION;
delete process.env.FAKE_NPM_PREFIX_CONFIG_LOG;
delete process.env.FAKE_NPM_SCRIPTS;
delete process.env.SETUP_CLI_TEST_WORKSPACE;
delete process.env.SUPABASE_SETUP_CLI_NPM;

for (const dir of tempDirs) {
Expand Down Expand Up @@ -147,13 +157,27 @@ function createFakeNpm(): string {
mkdirSync(binDir, { recursive: true });
writeFileSync(
scriptPath,
`import { appendFileSync, mkdirSync, writeFileSync } from "node:fs";
`import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import path from "node:path";

const args = process.argv.slice(2);
appendFileSync(process.env.FAKE_NPM_LOG, JSON.stringify(args) + "\\n");
appendFileSync(process.env.FAKE_NPM_CWD_LOG, process.cwd() + "\\n");
appendFileSync(
process.env.FAKE_NPM_ENV_LOG,
JSON.stringify({ NPM_CONFIG_USERCONFIG: process.env.NPM_CONFIG_USERCONFIG ?? null }) + "\\n",
);

function recordConfig(configPath) {
appendFileSync(
process.env.FAKE_NPM_PREFIX_CONFIG_LOG,
JSON.stringify(existsSync(configPath) ? readFileSync(configPath, "utf8") : null) + "\\n",
);
}

if (args[0] === "view") {
recordConfig(path.join(process.cwd(), ".npmrc"));

const bin =
process.env.FAKE_NPM_BIN === "missing"
? undefined
Expand Down Expand Up @@ -185,6 +209,7 @@ if (!prefix) {

const binDir = path.join(prefix, "node_modules", ".bin");
mkdirSync(binDir, { recursive: true });
recordConfig(path.join(prefix, ".npmrc"));

if (process.platform === "win32") {
writeFileSync(
Expand Down Expand Up @@ -229,10 +254,21 @@ function installFakeNpm(
} = {},
): string {
const binDir = createFakeNpm();
const cwdLogPath = path.join(createTempDir("setup-cli-fake-npm-cwd-log-"), "npm-cwd.log");
const envLogPath = path.join(createTempDir("setup-cli-fake-npm-env-log-"), "npm-env.log");
const logPath = path.join(createTempDir("setup-cli-fake-npm-log-"), "npm.log");
const prefixConfigLogPath = path.join(
createTempDir("setup-cli-fake-npm-prefix-config-log-"),
"npm-prefix-config.log",
);
writeFileSync(cwdLogPath, "");
writeFileSync(envLogPath, "");
writeFileSync(logPath, "");
writeFileSync(prefixConfigLogPath, "");
process.env.FAKE_CLI_VERSION = versionOutput;
process.env.FAKE_NPM_BIN = options.bin ?? "dist/supabase.js";
process.env.FAKE_NPM_CWD_LOG = cwdLogPath;
process.env.FAKE_NPM_ENV_LOG = envLogPath;
process.env.FAKE_NPM_INTEGRITY = options.integrity ?? "sha512-test";
process.env.FAKE_NPM_LOG = logPath;
process.env.FAKE_NPM_PACKAGE_VERSION =
Expand All @@ -248,6 +284,7 @@ function installFakeNpm(
binDir,
process.platform === "win32" ? "npm.cmd" : "npm",
);
process.env.FAKE_NPM_PREFIX_CONFIG_LOG = prefixConfigLogPath;

return logPath;
}
Expand All @@ -260,8 +297,31 @@ function readNpmCalls(logPath: string): string[][] {
.map((line) => JSON.parse(line) as string[]);
}

function readNpmCwds(): string[] {
return readFileSync(process.env.FAKE_NPM_CWD_LOG ?? "", "utf8")
.trim()
.split("\n")
.filter(Boolean);
}

function readNpmEnvs(): Array<{ NPM_CONFIG_USERCONFIG: string | null }> {
return readFileSync(process.env.FAKE_NPM_ENV_LOG ?? "", "utf8")
.trim()
.split("\n")
.filter(Boolean)
.map((line) => JSON.parse(line) as { NPM_CONFIG_USERCONFIG: string | null });
}

function readNpmPrefixConfigs(): Array<string | null> {
return readFileSync(process.env.FAKE_NPM_PREFIX_CONFIG_LOG ?? "", "utf8")
.trim()
.split("\n")
.filter(Boolean)
.map((line) => JSON.parse(line) as string | null);
}

function viewMetadataCall(spec: string): string[] {
return ["view", spec, "version", "bin", "scripts", "dist.integrity", "--json"];
return ["view", spec, "version", "bin", "scripts", "dist.integrity", "--json", "--offline=false"];
}

function createActionSpies(inputVersion: string) {
Expand Down Expand Up @@ -442,6 +502,83 @@ test("installs the CLI with npm into an isolated prefix", async () => {
"--no-audit",
"--no-fund",
"--no-package-lock",
"--offline=false",
"--bin-links=true",
"--ignore-scripts=true",
"supabase@2.101.0",
],
]);
});

test("runs npm with caller workspace config and action-owned overrides", async () => {
const workspace = createWorkspace({
".npmrc": [
"registry=https://registry.example.test",
"@internal:registry=https://registry.internal.example.test",
"//registry.example.test/:_authToken=${NPM_TOKEN}",
"//registry.example.test/:certfile=certs/client.pem",
"//registry.example.test/:keyfile=certs/client-key.pem",
"always-auth",
'cafile="certs/project-ca.pem"',
'userconfig="${SETUP_CLI_TEST_WORKSPACE}/.npmrc-ci"',
"globalconfig=.npmrc-global",
"bin-links=false",
"offline=true",
"package-lock=true",
].join("\n"),
".npmrc-ci": [
"registry=https://delegated-registry.example.test",
"//registry.example.test/:_password=delegated",
"cafile=certs/delegated-ca.pem",
"bin-links=false",
"offline=true",
].join("\n"),
".npmrc-global": [
"registry=https://global-registry.example.test",
"//registry.example.test/:username=global",
].join("\n"),
"certs/client-key.pem": "client-key",
"certs/client.pem": "client",
"certs/delegated-ca.pem": "delegated-ca",
"certs/project-ca.pem": "project-ca",
});
process.env.SETUP_CLI_TEST_WORKSPACE = workspace;
const userconfigPath = path.join(createTempDir("setup-cli-userconfig-"), ".npmrc");
writeFileSync(userconfigPath, "//registry.example.test/:username=existing\n");
process.env.NPM_CONFIG_USERCONFIG = userconfigPath;
process.env.GITHUB_WORKSPACE = workspace;
const logPath = installFakeNpm();
const { installCli } = await getMainModule();

await installCli({
spec: "supabase@2.101.0",
version: "2.101.0",
});

const realWorkspace = realpathSync(workspace);
const npmCwds = readNpmCwds().map((cwd) => realpathSync(cwd));
expect(npmCwds).toEqual([realWorkspace, realWorkspace]);
expect(readNpmEnvs().map((env) => env.NPM_CONFIG_USERCONFIG)).toEqual([
userconfigPath,
userconfigPath,
]);
expect(readNpmPrefixConfigs()).toEqual([
readFileSync(path.join(workspace, ".npmrc"), "utf8"),
null,
]);
expect(readNpmCalls(logPath)).toEqual([
viewMetadataCall("supabase@2.101.0"),
[
"install",
"--prefix",
expect.any(String),
"--omit=dev",
"--include=optional",
"--no-audit",
"--no-fund",
"--no-package-lock",
"--offline=false",
"--bin-links=true",
"--ignore-scripts=true",
"supabase@2.101.0",
],
Expand Down Expand Up @@ -471,6 +608,8 @@ test("allows install scripts for legacy npm packages that declare a preinstall",
"--no-audit",
"--no-fund",
"--no-package-lock",
"--offline=false",
"--bin-links=true",
"--ignore-scripts=false",
"supabase@1.15.1",
],
Expand Down Expand Up @@ -500,6 +639,8 @@ test("allows install scripts for npm packages that declare a postinstall", async
"--no-audit",
"--no-fund",
"--no-package-lock",
"--offline=false",
"--bin-links=true",
"--ignore-scripts=false",
"supabase@1.178.2",
],
Expand Down Expand Up @@ -527,6 +668,8 @@ test("verifies lockfile integrity before installing", async () => {
"--no-audit",
"--no-fund",
"--no-package-lock",
"--offline=false",
"--bin-links=true",
"--ignore-scripts=true",
"supabase@2.101.0",
],
Expand Down
8 changes: 6 additions & 2 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ async function getPackageMetadata(resolution: PackageResolution): Promise<Packag
"scripts",
"dist.integrity",
"--json",
"--offline=false",
]);
const metadata = JSON.parse(output) as unknown;

Expand Down Expand Up @@ -293,6 +294,7 @@ function createInstallRoot(): string {
async function runNpm(args: string[]): Promise<string> {
const executable = process.env[NPM_EXECUTABLE_ENV]?.trim() || "npm";
const proc = Bun.spawn([executable, ...args], {
cwd: process.env.GITHUB_WORKSPACE?.trim() ?? process.cwd(),
Comment thread
jgoux marked this conversation as resolved.
env: process.env,
stderr: "pipe",
stdout: "pipe",
Expand All @@ -311,12 +313,12 @@ async function runNpm(args: string[]): Promise<string> {
}

export async function installCli(resolution: PackageResolution): Promise<string> {
const installRoot = createInstallRoot();

const metadata = await getPackageMetadata(resolution);
verifyPackageMetadata(resolution, metadata);
verifyPackageIntegrity(resolution, metadata);

const installRoot = createInstallRoot();

await runNpm([
"install",
"--prefix",
Expand All @@ -326,6 +328,8 @@ export async function installCli(resolution: PackageResolution): Promise<string>
"--no-audit",
"--no-fund",
"--no-package-lock",
"--offline=false",
"--bin-links=true",
`--ignore-scripts=${shouldIgnoreInstallScripts(metadata)}`,
resolution.spec,
]);
Expand Down
Loading