mirror of
https://github.com/supabase/setup-cli.git
synced 2026-08-12 08:45:15 +00:00
Honor workspace npm config for CLI installs (#443)
## Summary - Run npm commands from the caller workspace so npm remains the source of truth for `.npmrc` parsing, auth, delegated config files, env expansion, quoting, and precedence. - Keep the CLI install isolated with `--prefix`, while explicitly overriding action-owned npm policy such as `offline=false`, `bin-links=true`, and no package lock. - Treat existing `node` and `npm` commands on musl as sufficient only when they actually run, probing Alpine `/usr/bin` binaries with `/usr/bin` first on PATH. - Prefer Alpine `/usr/bin` runtime binaries when an earlier PATH entry shadows them, and keep apk installation for missing runtime libraries or missing commands. ## Validation - `bun run ci` - Real npm sanity check for workspace `.npmrc` with quoted/env delegated config, `globalconfig`, relative `cafile`, mTLS path keys, `offline=true`, `bin-links=false`, and package-lock policy - Fixture workflow: https://github.com/jgoux/setup-cli-testing/actions/runs/28663875606 Addresses https://github.com/supabase/setup-cli/pull/442#discussion_r3519786253 Addresses https://github.com/supabase/setup-cli/pull/442#discussion_r3519786255
This commit is contained in:
26
action.yml
26
action.yml
@@ -61,12 +61,33 @@ runs:
|
|||||||
# actions/setup-node uses glibc Node builds, so install Alpine's Node/npm instead.
|
# actions/setup-node uses glibc Node builds, so install Alpine's Node/npm instead.
|
||||||
if command -v apk >/dev/null 2>&1; then
|
if command -v apk >/dev/null 2>&1; then
|
||||||
missing_packages=""
|
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
|
if ! apk info -e "${package}" >/dev/null 2>&1; then
|
||||||
missing_packages="${missing_packages} ${package}"
|
missing_packages="${missing_packages} ${package}"
|
||||||
fi
|
fi
|
||||||
done
|
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
|
if [ -z "${missing_packages}" ]; then
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
@@ -77,6 +98,9 @@ runs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
apk add --no-cache ${missing_packages}
|
apk add --no-cache ${missing_packages}
|
||||||
|
if echo " ${missing_packages} " | grep -Eq ' (nodejs|npm) '; then
|
||||||
|
echo "/usr/bin" >> "$GITHUB_PATH"
|
||||||
|
fi
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
149
src/main.test.ts
149
src/main.test.ts
@@ -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 os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import process from "node:process";
|
import process from "node:process";
|
||||||
@@ -7,6 +7,7 @@ import * as core from "@actions/core";
|
|||||||
|
|
||||||
const CLI_CONFIG_REGISTRY = "SUPABASE_INTERNAL_IMAGE_REGISTRY";
|
const CLI_CONFIG_REGISTRY = "SUPABASE_INTERNAL_IMAGE_REGISTRY";
|
||||||
const originalPath = process.env.PATH;
|
const originalPath = process.env.PATH;
|
||||||
|
const originalNpmUserconfig = process.env.NPM_CONFIG_USERCONFIG;
|
||||||
const originalRunnerTemp = process.env.RUNNER_TEMP;
|
const originalRunnerTemp = process.env.RUNNER_TEMP;
|
||||||
const originalWorkspace = process.env.GITHUB_WORKSPACE;
|
const originalWorkspace = process.env.GITHUB_WORKSPACE;
|
||||||
const tempDirs = new Set<string>();
|
const tempDirs = new Set<string>();
|
||||||
@@ -15,14 +16,23 @@ let mainModule: typeof import("./main.ts") | null = null;
|
|||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
mock.restore();
|
mock.restore();
|
||||||
process.env.PATH = originalPath;
|
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.RUNNER_TEMP = originalRunnerTemp;
|
||||||
process.env.GITHUB_WORKSPACE = originalWorkspace;
|
process.env.GITHUB_WORKSPACE = originalWorkspace;
|
||||||
delete process.env.FAKE_CLI_VERSION;
|
delete process.env.FAKE_CLI_VERSION;
|
||||||
delete process.env.FAKE_NPM_BIN;
|
delete process.env.FAKE_NPM_BIN;
|
||||||
delete process.env.FAKE_NPM_INTEGRITY;
|
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_LOG;
|
||||||
delete process.env.FAKE_NPM_PACKAGE_VERSION;
|
delete process.env.FAKE_NPM_PACKAGE_VERSION;
|
||||||
|
delete process.env.FAKE_NPM_PREFIX_CONFIG_LOG;
|
||||||
delete process.env.FAKE_NPM_SCRIPTS;
|
delete process.env.FAKE_NPM_SCRIPTS;
|
||||||
|
delete process.env.SETUP_CLI_TEST_WORKSPACE;
|
||||||
delete process.env.SUPABASE_SETUP_CLI_NPM;
|
delete process.env.SUPABASE_SETUP_CLI_NPM;
|
||||||
|
|
||||||
for (const dir of tempDirs) {
|
for (const dir of tempDirs) {
|
||||||
@@ -147,13 +157,27 @@ function createFakeNpm(): string {
|
|||||||
mkdirSync(binDir, { recursive: true });
|
mkdirSync(binDir, { recursive: true });
|
||||||
writeFileSync(
|
writeFileSync(
|
||||||
scriptPath,
|
scriptPath,
|
||||||
`import { appendFileSync, mkdirSync, writeFileSync } from "node:fs";
|
`import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
|
||||||
const args = process.argv.slice(2);
|
const args = process.argv.slice(2);
|
||||||
appendFileSync(process.env.FAKE_NPM_LOG, JSON.stringify(args) + "\\n");
|
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") {
|
if (args[0] === "view") {
|
||||||
|
recordConfig(path.join(process.cwd(), ".npmrc"));
|
||||||
|
|
||||||
const bin =
|
const bin =
|
||||||
process.env.FAKE_NPM_BIN === "missing"
|
process.env.FAKE_NPM_BIN === "missing"
|
||||||
? undefined
|
? undefined
|
||||||
@@ -185,6 +209,7 @@ if (!prefix) {
|
|||||||
|
|
||||||
const binDir = path.join(prefix, "node_modules", ".bin");
|
const binDir = path.join(prefix, "node_modules", ".bin");
|
||||||
mkdirSync(binDir, { recursive: true });
|
mkdirSync(binDir, { recursive: true });
|
||||||
|
recordConfig(path.join(prefix, ".npmrc"));
|
||||||
|
|
||||||
if (process.platform === "win32") {
|
if (process.platform === "win32") {
|
||||||
writeFileSync(
|
writeFileSync(
|
||||||
@@ -229,10 +254,21 @@ function installFakeNpm(
|
|||||||
} = {},
|
} = {},
|
||||||
): string {
|
): string {
|
||||||
const binDir = createFakeNpm();
|
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 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(logPath, "");
|
||||||
|
writeFileSync(prefixConfigLogPath, "");
|
||||||
process.env.FAKE_CLI_VERSION = versionOutput;
|
process.env.FAKE_CLI_VERSION = versionOutput;
|
||||||
process.env.FAKE_NPM_BIN = options.bin ?? "dist/supabase.js";
|
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_INTEGRITY = options.integrity ?? "sha512-test";
|
||||||
process.env.FAKE_NPM_LOG = logPath;
|
process.env.FAKE_NPM_LOG = logPath;
|
||||||
process.env.FAKE_NPM_PACKAGE_VERSION =
|
process.env.FAKE_NPM_PACKAGE_VERSION =
|
||||||
@@ -248,6 +284,7 @@ function installFakeNpm(
|
|||||||
binDir,
|
binDir,
|
||||||
process.platform === "win32" ? "npm.cmd" : "npm",
|
process.platform === "win32" ? "npm.cmd" : "npm",
|
||||||
);
|
);
|
||||||
|
process.env.FAKE_NPM_PREFIX_CONFIG_LOG = prefixConfigLogPath;
|
||||||
|
|
||||||
return logPath;
|
return logPath;
|
||||||
}
|
}
|
||||||
@@ -260,8 +297,31 @@ function readNpmCalls(logPath: string): string[][] {
|
|||||||
.map((line) => JSON.parse(line) as 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[] {
|
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) {
|
function createActionSpies(inputVersion: string) {
|
||||||
@@ -442,6 +502,83 @@ test("installs the CLI with npm into an isolated prefix", async () => {
|
|||||||
"--no-audit",
|
"--no-audit",
|
||||||
"--no-fund",
|
"--no-fund",
|
||||||
"--no-package-lock",
|
"--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",
|
"--ignore-scripts=true",
|
||||||
"supabase@2.101.0",
|
"supabase@2.101.0",
|
||||||
],
|
],
|
||||||
@@ -471,6 +608,8 @@ test("allows install scripts for legacy npm packages that declare a preinstall",
|
|||||||
"--no-audit",
|
"--no-audit",
|
||||||
"--no-fund",
|
"--no-fund",
|
||||||
"--no-package-lock",
|
"--no-package-lock",
|
||||||
|
"--offline=false",
|
||||||
|
"--bin-links=true",
|
||||||
"--ignore-scripts=false",
|
"--ignore-scripts=false",
|
||||||
"supabase@1.15.1",
|
"supabase@1.15.1",
|
||||||
],
|
],
|
||||||
@@ -500,6 +639,8 @@ test("allows install scripts for npm packages that declare a postinstall", async
|
|||||||
"--no-audit",
|
"--no-audit",
|
||||||
"--no-fund",
|
"--no-fund",
|
||||||
"--no-package-lock",
|
"--no-package-lock",
|
||||||
|
"--offline=false",
|
||||||
|
"--bin-links=true",
|
||||||
"--ignore-scripts=false",
|
"--ignore-scripts=false",
|
||||||
"supabase@1.178.2",
|
"supabase@1.178.2",
|
||||||
],
|
],
|
||||||
@@ -527,6 +668,8 @@ test("verifies lockfile integrity before installing", async () => {
|
|||||||
"--no-audit",
|
"--no-audit",
|
||||||
"--no-fund",
|
"--no-fund",
|
||||||
"--no-package-lock",
|
"--no-package-lock",
|
||||||
|
"--offline=false",
|
||||||
|
"--bin-links=true",
|
||||||
"--ignore-scripts=true",
|
"--ignore-scripts=true",
|
||||||
"supabase@2.101.0",
|
"supabase@2.101.0",
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -252,6 +252,7 @@ async function getPackageMetadata(resolution: PackageResolution): Promise<Packag
|
|||||||
"scripts",
|
"scripts",
|
||||||
"dist.integrity",
|
"dist.integrity",
|
||||||
"--json",
|
"--json",
|
||||||
|
"--offline=false",
|
||||||
]);
|
]);
|
||||||
const metadata = JSON.parse(output) as unknown;
|
const metadata = JSON.parse(output) as unknown;
|
||||||
|
|
||||||
@@ -293,6 +294,7 @@ function createInstallRoot(): string {
|
|||||||
async function runNpm(args: string[]): Promise<string> {
|
async function runNpm(args: string[]): Promise<string> {
|
||||||
const executable = process.env[NPM_EXECUTABLE_ENV]?.trim() || "npm";
|
const executable = process.env[NPM_EXECUTABLE_ENV]?.trim() || "npm";
|
||||||
const proc = Bun.spawn([executable, ...args], {
|
const proc = Bun.spawn([executable, ...args], {
|
||||||
|
cwd: process.env.GITHUB_WORKSPACE?.trim() ?? process.cwd(),
|
||||||
env: process.env,
|
env: process.env,
|
||||||
stderr: "pipe",
|
stderr: "pipe",
|
||||||
stdout: "pipe",
|
stdout: "pipe",
|
||||||
@@ -311,12 +313,12 @@ async function runNpm(args: string[]): Promise<string> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function installCli(resolution: PackageResolution): Promise<string> {
|
export async function installCli(resolution: PackageResolution): Promise<string> {
|
||||||
|
const installRoot = createInstallRoot();
|
||||||
|
|
||||||
const metadata = await getPackageMetadata(resolution);
|
const metadata = await getPackageMetadata(resolution);
|
||||||
verifyPackageMetadata(resolution, metadata);
|
verifyPackageMetadata(resolution, metadata);
|
||||||
verifyPackageIntegrity(resolution, metadata);
|
verifyPackageIntegrity(resolution, metadata);
|
||||||
|
|
||||||
const installRoot = createInstallRoot();
|
|
||||||
|
|
||||||
await runNpm([
|
await runNpm([
|
||||||
"install",
|
"install",
|
||||||
"--prefix",
|
"--prefix",
|
||||||
@@ -326,6 +328,8 @@ export async function installCli(resolution: PackageResolution): Promise<string>
|
|||||||
"--no-audit",
|
"--no-audit",
|
||||||
"--no-fund",
|
"--no-fund",
|
||||||
"--no-package-lock",
|
"--no-package-lock",
|
||||||
|
"--offline=false",
|
||||||
|
"--bin-links=true",
|
||||||
`--ignore-scripts=${shouldIgnoreInstallScripts(metadata)}`,
|
`--ignore-scripts=${shouldIgnoreInstallScripts(metadata)}`,
|
||||||
resolution.spec,
|
resolution.spec,
|
||||||
]);
|
]);
|
||||||
|
|||||||
Reference in New Issue
Block a user