Skip to content

Commit f14ab3c

Browse files
committed
feat: add registry-url and scope inputs for npm auth
Add `registry-url` and `scope` inputs matching the functionality from actions/setup-node. When `registry-url` is provided, a .npmrc file is written with the registry URL and auth token configuration, enabling authentication for private registries like GitHub Packages.
1 parent e1609b4 commit f14ab3c

File tree

9 files changed

+334
-85
lines changed

9 files changed

+334
-85
lines changed

.github/workflows/test.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,30 @@ jobs:
131131
echo "Installed version: ${{ steps.setup.outputs.version }}"
132132
echo "Cache hit: ${{ steps.setup.outputs.cache-hit }}"
133133
134+
test-registry-url:
135+
runs-on: ubuntu-latest
136+
steps:
137+
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
138+
139+
- name: Setup Vite+ with registry-url
140+
uses: ./
141+
with:
142+
run-install: false
143+
cache: false
144+
registry-url: "https://npm.pkg.github.com"
145+
scope: "@voidzero-dev"
146+
147+
- name: Verify .npmrc was created
148+
run: |
149+
echo "NPM_CONFIG_USERCONFIG=$NPM_CONFIG_USERCONFIG"
150+
cat "$NPM_CONFIG_USERCONFIG"
151+
grep -q "@voidzero-dev:registry=https://npm.pkg.github.com/" "$NPM_CONFIG_USERCONFIG"
152+
grep -q "_authToken=\${NODE_AUTH_TOKEN}" "$NPM_CONFIG_USERCONFIG"
153+
154+
- name: Verify NODE_AUTH_TOKEN is exported
155+
run: |
156+
echo "NODE_AUTH_TOKEN is set: ${NODE_AUTH_TOKEN:+yes}"
157+
134158
build:
135159
runs-on: ubuntu-latest
136160
steps:

README.md

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,21 @@ steps:
8080
- cwd: ./packages/lib
8181
```
8282
83+
### With Private Registry (GitHub Packages)
84+
85+
```yaml
86+
steps:
87+
- uses: actions/checkout@v6
88+
- uses: voidzero-dev/setup-vp@v1
89+
with:
90+
node-version: "22"
91+
registry-url: "https://npm.pkg.github.com"
92+
scope: "@myorg"
93+
- run: vp install
94+
env:
95+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
96+
```
97+
8398
### Matrix Testing with Multiple Node.js Versions
8499
85100
```yaml
@@ -100,14 +115,16 @@ jobs:
100115
101116
## Inputs
102117
103-
| Input | Description | Required | Default |
104-
| ----------------------- | ----------------------------------------------------------------------------------------------------- | -------- | ------------- |
105-
| `version` | Version of Vite+ to install | No | `latest` |
106-
| `node-version` | Node.js version to install via `vp env use` | No | Latest LTS |
107-
| `node-version-file` | Path to file containing Node.js version (`.nvmrc`, `.node-version`, `.tool-versions`, `package.json`) | No | |
108-
| `run-install` | Run `vp install` after setup. Accepts boolean or YAML object with `cwd`/`args` | No | `true` |
109-
| `cache` | Enable caching of project dependencies | No | `false` |
110-
| `cache-dependency-path` | Path to lock file for cache key generation | No | Auto-detected |
118+
| Input | Description | Required | Default |
119+
| ----------------------- | --------------------------------------------------------------------------------------------------------- | -------- | ------------- |
120+
| `version` | Version of Vite+ to install | No | `latest` |
121+
| `node-version` | Node.js version to install via `vp env use` | No | Latest LTS |
122+
| `node-version-file` | Path to file containing Node.js version (`.nvmrc`, `.node-version`, `.tool-versions`, `package.json`) | No | |
123+
| `run-install` | Run `vp install` after setup. Accepts boolean or YAML object with `cwd`/`args` | No | `true` |
124+
| `cache` | Enable caching of project dependencies | No | `false` |
125+
| `cache-dependency-path` | Path to lock file for cache key generation | No | Auto-detected |
126+
| `registry-url` | Optional registry to set up for auth. Sets the registry in `.npmrc` and reads auth from `NODE_AUTH_TOKEN` | No | |
127+
| `scope` | Optional scope for scoped registries. Falls back to repo owner for GitHub Packages | No | |
111128

112129
## Outputs
113130

action.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ inputs:
2727
cache-dependency-path:
2828
description: "Path to lock file for cache key generation. Auto-detected if not specified."
2929
required: false
30+
registry-url:
31+
description: "Optional registry to set up for auth. Will set the registry in a project level .npmrc and set up auth to read in from env.NODE_AUTH_TOKEN."
32+
required: false
33+
scope:
34+
description: "Optional scope for authenticating against scoped registries. Will fall back to the repository owner when using the GitHub Packages registry (https://npm.pkg.github.com/)."
35+
required: false
3036

3137
outputs:
3238
version:

dist/index.mjs

Lines changed: 75 additions & 75 deletions
Large diffs are not rendered by default.

src/auth.test.ts

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { describe, it, expect, beforeEach, afterEach, vi } from "vite-plus/test";
2+
import { join } from "node:path";
3+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
4+
import { EOL } from "node:os";
5+
import { configAuthentication } from "./auth.js";
6+
import { exportVariable } from "@actions/core";
7+
8+
vi.mock("@actions/core", () => ({
9+
debug: vi.fn(),
10+
exportVariable: vi.fn(),
11+
}));
12+
13+
vi.mock("node:fs", async () => {
14+
const actual = await vi.importActual<typeof import("node:fs")>("node:fs");
15+
return {
16+
...actual,
17+
existsSync: vi.fn(),
18+
readFileSync: vi.fn(),
19+
writeFileSync: vi.fn(),
20+
};
21+
});
22+
23+
describe("configAuthentication", () => {
24+
const runnerTemp = "/tmp/runner";
25+
26+
beforeEach(() => {
27+
vi.stubEnv("RUNNER_TEMP", runnerTemp);
28+
vi.mocked(existsSync).mockReturnValue(false);
29+
});
30+
31+
afterEach(() => {
32+
vi.unstubAllEnvs();
33+
vi.resetAllMocks();
34+
});
35+
36+
it("should write .npmrc with registry and auth token", () => {
37+
configAuthentication("https://registry.npmjs.org/");
38+
39+
const expectedPath = join(runnerTemp, ".npmrc");
40+
expect(writeFileSync).toHaveBeenCalledWith(
41+
expectedPath,
42+
expect.stringContaining("//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}"),
43+
);
44+
expect(writeFileSync).toHaveBeenCalledWith(
45+
expectedPath,
46+
expect.stringContaining("registry=https://registry.npmjs.org/"),
47+
);
48+
});
49+
50+
it("should append trailing slash if missing", () => {
51+
configAuthentication("https://registry.npmjs.org");
52+
53+
expect(writeFileSync).toHaveBeenCalledWith(
54+
expect.any(String),
55+
expect.stringContaining("registry=https://registry.npmjs.org/"),
56+
);
57+
});
58+
59+
it("should auto-detect scope for GitHub Packages registry", () => {
60+
vi.stubEnv("GITHUB_REPOSITORY_OWNER", "voidzero-dev");
61+
62+
configAuthentication("https://npm.pkg.github.com");
63+
64+
expect(writeFileSync).toHaveBeenCalledWith(
65+
expect.any(String),
66+
expect.stringContaining("@voidzero-dev:registry=https://npm.pkg.github.com/"),
67+
);
68+
});
69+
70+
it("should use explicit scope", () => {
71+
configAuthentication("https://npm.pkg.github.com", "@myorg");
72+
73+
expect(writeFileSync).toHaveBeenCalledWith(
74+
expect.any(String),
75+
expect.stringContaining("@myorg:registry=https://npm.pkg.github.com/"),
76+
);
77+
});
78+
79+
it("should prepend @ to scope if missing", () => {
80+
configAuthentication("https://npm.pkg.github.com", "myorg");
81+
82+
expect(writeFileSync).toHaveBeenCalledWith(
83+
expect.any(String),
84+
expect.stringContaining("@myorg:registry=https://npm.pkg.github.com/"),
85+
);
86+
});
87+
88+
it("should lowercase scope", () => {
89+
configAuthentication("https://npm.pkg.github.com", "@MyOrg");
90+
91+
expect(writeFileSync).toHaveBeenCalledWith(
92+
expect.any(String),
93+
expect.stringContaining("@myorg:registry=https://npm.pkg.github.com/"),
94+
);
95+
});
96+
97+
it("should preserve existing .npmrc content except registry lines", () => {
98+
vi.mocked(existsSync).mockReturnValue(true);
99+
vi.mocked(readFileSync).mockReturnValue(
100+
`always-auth=true${EOL}registry=https://old.reg/${EOL}`,
101+
);
102+
103+
configAuthentication("https://registry.npmjs.org/");
104+
105+
const written = vi.mocked(writeFileSync).mock.calls[0]![1] as string;
106+
expect(written).toContain("always-auth=true");
107+
expect(written).not.toContain("https://old.reg/");
108+
expect(written).toContain("registry=https://registry.npmjs.org/");
109+
});
110+
111+
it("should export NPM_CONFIG_USERCONFIG", () => {
112+
configAuthentication("https://registry.npmjs.org/");
113+
114+
expect(exportVariable).toHaveBeenCalledWith(
115+
"NPM_CONFIG_USERCONFIG",
116+
join(runnerTemp, ".npmrc"),
117+
);
118+
});
119+
120+
it("should export NODE_AUTH_TOKEN placeholder when not set", () => {
121+
configAuthentication("https://registry.npmjs.org/");
122+
123+
expect(exportVariable).toHaveBeenCalledWith("NODE_AUTH_TOKEN", "XXXXX-XXXXX-XXXXX-XXXXX");
124+
});
125+
126+
it("should preserve existing NODE_AUTH_TOKEN", () => {
127+
vi.stubEnv("NODE_AUTH_TOKEN", "my-real-token");
128+
129+
configAuthentication("https://registry.npmjs.org/");
130+
131+
expect(exportVariable).toHaveBeenCalledWith("NODE_AUTH_TOKEN", "my-real-token");
132+
});
133+
});

src/auth.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
2+
import { EOL } from "node:os";
3+
import { resolve } from "node:path";
4+
import { debug, exportVariable } from "@actions/core";
5+
6+
/**
7+
* Configure npm registry authentication by writing a .npmrc file.
8+
* Ported from actions/setup-node's authutil.ts.
9+
*/
10+
export function configAuthentication(registryUrl: string, scope?: string): void {
11+
const npmrc = resolve(process.env.RUNNER_TEMP || process.cwd(), ".npmrc");
12+
13+
if (!registryUrl.endsWith("/")) {
14+
registryUrl += "/";
15+
}
16+
17+
writeRegistryToFile(registryUrl, npmrc, scope);
18+
}
19+
20+
function writeRegistryToFile(registryUrl: string, fileLocation: string, scope?: string): void {
21+
// Auto-detect scope for GitHub Packages registry
22+
if (!scope && registryUrl.includes("npm.pkg.github.com")) {
23+
scope = process.env.GITHUB_REPOSITORY_OWNER;
24+
}
25+
26+
if (scope && !scope.startsWith("@")) {
27+
scope = "@" + scope;
28+
}
29+
30+
if (scope) {
31+
scope = scope.toLowerCase() + ":";
32+
} else {
33+
scope = "";
34+
}
35+
36+
debug(`Setting auth in ${fileLocation}`);
37+
38+
let newContents = "";
39+
if (existsSync(fileLocation)) {
40+
const curContents = readFileSync(fileLocation, "utf8");
41+
for (const line of curContents.split(EOL)) {
42+
// Preserve lines that don't set the scoped registry
43+
if (!line.toLowerCase().startsWith(`${scope}registry`)) {
44+
newContents += line + EOL;
45+
}
46+
}
47+
}
48+
49+
// Auth token line: remove protocol prefix from registry URL
50+
const authString = registryUrl.replace(/^\w+:/, "") + ":_authToken=${NODE_AUTH_TOKEN}";
51+
const registryString = `${scope}registry=${registryUrl}`;
52+
newContents += `${authString}${EOL}${registryString}`;
53+
54+
writeFileSync(fileLocation, newContents);
55+
56+
exportVariable("NPM_CONFIG_USERCONFIG", fileLocation);
57+
// Export placeholder if NODE_AUTH_TOKEN is not set so npm doesn't error
58+
exportVariable("NODE_AUTH_TOKEN", process.env.NODE_AUTH_TOKEN || "XXXXX-XXXXX-XXXXX-XXXXX");
59+
}

src/index.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { saveVpCache } from "./cache-vp.js";
99
import { State, Outputs } from "./types.js";
1010
import type { Inputs } from "./types.js";
1111
import { resolveNodeVersionFile } from "./node-version-file.js";
12+
import { configAuthentication } from "./auth.js";
1213

1314
async function runMain(inputs: Inputs): Promise<void> {
1415
// Mark that post action should run
@@ -29,12 +30,17 @@ async function runMain(inputs: Inputs): Promise<void> {
2930
await exec("vp", ["env", "use", nodeVersion]);
3031
}
3132

32-
// Step 4: Restore cache if enabled
33+
// Step 4: Configure registry authentication if specified
34+
if (inputs.registryUrl) {
35+
configAuthentication(inputs.registryUrl, inputs.scope);
36+
}
37+
38+
// Step 5: Restore cache if enabled
3339
if (inputs.cache) {
3440
await restoreCache(inputs);
3541
}
3642

37-
// Step 5: Run vp install if requested
43+
// Step 6: Run vp install if requested
3844
if (inputs.runInstall.length > 0) {
3945
await runViteInstall(inputs);
4046
}

src/inputs.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ export function getInputs(): Inputs {
1212
runInstall: parseRunInstall(getInput("run-install")),
1313
cache: getBooleanInput("cache"),
1414
cacheDependencyPath: getInput("cache-dependency-path") || undefined,
15+
registryUrl: getInput("registry-url") || undefined,
16+
scope: getInput("scope") || undefined,
1517
};
1618
}
1719

src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ export interface Inputs {
2424
readonly runInstall: RunInstall[];
2525
readonly cache: boolean;
2626
readonly cacheDependencyPath?: string;
27+
readonly registryUrl?: string;
28+
readonly scope?: string;
2729
}
2830

2931
// Lock file types

0 commit comments

Comments
 (0)