-
Notifications
You must be signed in to change notification settings - Fork 4
feat: add registry-url and scope inputs for npm auth #9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
f14ab3c
feat: add registry-url and scope inputs for npm auth
fengmk2 ba0d868
fix: address review feedback for registry-url auth
fengmk2 ab3bdf7
fix: match setup-node's registry-url description wording
fengmk2 af16239
fix: describe registry-url behavior accurately (writes to $RUNNER_TEMP)
fengmk2 bbcd7f9
fix: validate registry URL, avoid param mutation, fix private registr…
fengmk2 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,179 @@ | ||
| import { describe, it, expect, beforeEach, afterEach, vi } from "vite-plus/test"; | ||
| import { join } from "node:path"; | ||
| import { existsSync, readFileSync, writeFileSync } from "node:fs"; | ||
| import { configAuthentication } from "./auth.js"; | ||
| import { exportVariable } from "@actions/core"; | ||
|
|
||
| vi.mock("@actions/core", () => ({ | ||
| debug: vi.fn(), | ||
| exportVariable: vi.fn(), | ||
| })); | ||
|
|
||
| vi.mock("node:fs", async () => { | ||
| const actual = await vi.importActual<typeof import("node:fs")>("node:fs"); | ||
| return { | ||
| ...actual, | ||
| existsSync: vi.fn(), | ||
| readFileSync: vi.fn(), | ||
| writeFileSync: vi.fn(), | ||
| }; | ||
| }); | ||
|
|
||
| describe("configAuthentication", () => { | ||
| const runnerTemp = "/tmp/runner"; | ||
|
|
||
| beforeEach(() => { | ||
| vi.stubEnv("RUNNER_TEMP", runnerTemp); | ||
| vi.mocked(existsSync).mockReturnValue(false); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| vi.unstubAllEnvs(); | ||
| vi.resetAllMocks(); | ||
| }); | ||
|
|
||
| it("should write .npmrc with registry and auth token", () => { | ||
| configAuthentication("https://registry.npmjs.org/"); | ||
|
|
||
| const expectedPath = join(runnerTemp, ".npmrc"); | ||
| expect(writeFileSync).toHaveBeenCalledWith( | ||
| expectedPath, | ||
| expect.stringContaining("//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}"), | ||
| ); | ||
| expect(writeFileSync).toHaveBeenCalledWith( | ||
| expectedPath, | ||
| expect.stringContaining("registry=https://registry.npmjs.org/"), | ||
| ); | ||
| }); | ||
|
|
||
| it("should append trailing slash if missing", () => { | ||
| configAuthentication("https://registry.npmjs.org"); | ||
|
|
||
| expect(writeFileSync).toHaveBeenCalledWith( | ||
| expect.any(String), | ||
| expect.stringContaining("registry=https://registry.npmjs.org/"), | ||
| ); | ||
| }); | ||
|
|
||
| it("should auto-detect scope for GitHub Packages registry", () => { | ||
| vi.stubEnv("GITHUB_REPOSITORY_OWNER", "voidzero-dev"); | ||
|
|
||
| configAuthentication("https://npm.pkg.github.com"); | ||
|
|
||
| expect(writeFileSync).toHaveBeenCalledWith( | ||
| expect.any(String), | ||
| expect.stringContaining("@voidzero-dev:registry=https://npm.pkg.github.com/"), | ||
| ); | ||
| }); | ||
|
|
||
| it("should use explicit scope", () => { | ||
| configAuthentication("https://npm.pkg.github.com", "@myorg"); | ||
|
|
||
| expect(writeFileSync).toHaveBeenCalledWith( | ||
| expect.any(String), | ||
| expect.stringContaining("@myorg:registry=https://npm.pkg.github.com/"), | ||
| ); | ||
| }); | ||
|
|
||
| it("should prepend @ to scope if missing", () => { | ||
| configAuthentication("https://npm.pkg.github.com", "myorg"); | ||
|
|
||
| expect(writeFileSync).toHaveBeenCalledWith( | ||
| expect.any(String), | ||
| expect.stringContaining("@myorg:registry=https://npm.pkg.github.com/"), | ||
| ); | ||
| }); | ||
|
|
||
| it("should lowercase scope", () => { | ||
| configAuthentication("https://npm.pkg.github.com", "@MyOrg"); | ||
|
|
||
| expect(writeFileSync).toHaveBeenCalledWith( | ||
| expect.any(String), | ||
| expect.stringContaining("@myorg:registry=https://npm.pkg.github.com/"), | ||
| ); | ||
| }); | ||
|
|
||
| it("should preserve existing .npmrc content except registry and auth lines", () => { | ||
| vi.mocked(existsSync).mockReturnValue(true); | ||
| vi.mocked(readFileSync).mockReturnValue( | ||
| [ | ||
| "always-auth=true", | ||
| "registry=https://old.reg/", | ||
| "//old.reg/:_authToken=${NODE_AUTH_TOKEN}", | ||
| ].join("\n"), | ||
| ); | ||
|
|
||
| configAuthentication("https://registry.npmjs.org/"); | ||
|
|
||
| const written = vi.mocked(writeFileSync).mock.calls[0]![1] as string; | ||
| expect(written).toContain("always-auth=true"); | ||
| expect(written).not.toContain("https://old.reg/"); | ||
| expect(written).toContain("registry=https://registry.npmjs.org/"); | ||
| }); | ||
|
|
||
| it("should remove existing auth token lines for the same registry", () => { | ||
| vi.mocked(existsSync).mockReturnValue(true); | ||
| vi.mocked(readFileSync).mockReturnValue( | ||
| [ | ||
| "//registry.npmjs.org/:_authToken=old-token", | ||
| "registry=https://registry.npmjs.org/", | ||
| "other-config=true", | ||
| ].join("\n"), | ||
| ); | ||
|
|
||
| configAuthentication("https://registry.npmjs.org/"); | ||
|
|
||
| const written = vi.mocked(writeFileSync).mock.calls[0]![1] as string; | ||
| expect(written).not.toContain("old-token"); | ||
| expect(written).toContain("other-config=true"); | ||
| expect(written).toContain("//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}"); | ||
| }); | ||
|
|
||
| it("should handle Windows-style line endings in existing .npmrc", () => { | ||
| vi.mocked(existsSync).mockReturnValue(true); | ||
| vi.mocked(readFileSync).mockReturnValue("always-auth=true\r\nregistry=https://old.reg/\r\n"); | ||
|
|
||
| configAuthentication("https://registry.npmjs.org/"); | ||
|
|
||
| const written = vi.mocked(writeFileSync).mock.calls[0]![1] as string; | ||
| expect(written).toContain("always-auth=true"); | ||
| expect(written).not.toContain("https://old.reg/"); | ||
| }); | ||
|
|
||
| it("should not auto-detect scope for lookalike GitHub Packages URLs", () => { | ||
| vi.stubEnv("GITHUB_REPOSITORY_OWNER", "voidzero-dev"); | ||
|
|
||
| configAuthentication("https://npm.pkg.github.com.evil.example"); | ||
|
|
||
| const written = vi.mocked(writeFileSync).mock.calls[0]![1] as string; | ||
| // Should NOT have scoped registry — the host doesn't match exactly | ||
| expect(written).not.toContain("@voidzero-dev:"); | ||
| }); | ||
|
|
||
| it("should throw on invalid URL", () => { | ||
| expect(() => configAuthentication("not-a-url")).toThrow("Invalid registry-url"); | ||
| }); | ||
|
|
||
| it("should export NPM_CONFIG_USERCONFIG", () => { | ||
| configAuthentication("https://registry.npmjs.org/"); | ||
|
|
||
| expect(exportVariable).toHaveBeenCalledWith( | ||
| "NPM_CONFIG_USERCONFIG", | ||
| join(runnerTemp, ".npmrc"), | ||
| ); | ||
| }); | ||
|
|
||
| it("should export NODE_AUTH_TOKEN placeholder when not set", () => { | ||
| configAuthentication("https://registry.npmjs.org/"); | ||
|
|
||
| expect(exportVariable).toHaveBeenCalledWith("NODE_AUTH_TOKEN", "XXXXX-XXXXX-XXXXX-XXXXX"); | ||
| }); | ||
|
|
||
| it("should preserve existing NODE_AUTH_TOKEN", () => { | ||
| vi.stubEnv("NODE_AUTH_TOKEN", "my-real-token"); | ||
|
|
||
| configAuthentication("https://registry.npmjs.org/"); | ||
|
|
||
| expect(exportVariable).toHaveBeenCalledWith("NODE_AUTH_TOKEN", "my-real-token"); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| import { existsSync, readFileSync, writeFileSync } from "node:fs"; | ||
| import { EOL } from "node:os"; | ||
| import { resolve } from "node:path"; | ||
| import { debug, exportVariable } from "@actions/core"; | ||
|
|
||
| /** | ||
| * Configure npm registry authentication by writing a .npmrc file. | ||
| * Ported from actions/setup-node's authutil.ts. | ||
| */ | ||
| export function configAuthentication(registryUrl: string, scope?: string): void { | ||
| // Validate and normalize the registry URL | ||
| let url: URL; | ||
| try { | ||
| url = new URL(registryUrl); | ||
| } catch { | ||
| throw new Error(`Invalid registry-url: "${registryUrl}". Must be a valid URL.`); | ||
| } | ||
|
|
||
| // Ensure trailing slash | ||
| const normalizedUrl = url.href.endsWith("/") ? url.href : url.href + "/"; | ||
| const npmrc = resolve(process.env.RUNNER_TEMP || process.cwd(), ".npmrc"); | ||
|
|
||
| writeRegistryToFile(normalizedUrl, npmrc, scope); | ||
| } | ||
|
|
||
| function writeRegistryToFile(registryUrl: string, fileLocation: string, scope?: string): void { | ||
| // Auto-detect scope for GitHub Packages registry using exact host match | ||
| if (!scope) { | ||
| const url = new URL(registryUrl); | ||
| if (url.hostname === "npm.pkg.github.com") { | ||
| scope = process.env.GITHUB_REPOSITORY_OWNER; | ||
| } | ||
| } | ||
|
|
||
| let scopePrefix = ""; | ||
| if (scope) { | ||
| scopePrefix = (scope.startsWith("@") ? scope : "@" + scope).toLowerCase() + ":"; | ||
| } | ||
|
|
||
| debug(`Setting auth in ${fileLocation}`); | ||
|
|
||
| // Compute the auth line prefix for filtering existing entries | ||
| const authPrefix = registryUrl.replace(/^\w+:/, "").toLowerCase(); | ||
|
|
||
| const lines: string[] = []; | ||
| if (existsSync(fileLocation)) { | ||
| const curContents = readFileSync(fileLocation, "utf8"); | ||
| for (const line of curContents.split(/\r?\n/)) { | ||
| const lower = line.toLowerCase(); | ||
| // Remove existing registry and auth token lines for this scope/registry | ||
| if (lower.startsWith(`${scopePrefix}registry`)) continue; | ||
| if (lower.startsWith(authPrefix) && lower.includes("_authtoken")) continue; | ||
| lines.push(line); | ||
| } | ||
| } | ||
|
|
||
| // Auth token line: remove protocol prefix from registry URL | ||
| const authString = registryUrl.replace(/^\w+:/, "") + ":_authToken=${NODE_AUTH_TOKEN}"; | ||
| const registryString = `${scopePrefix}registry=${registryUrl}`; | ||
| lines.push(authString, registryString); | ||
|
|
||
| writeFileSync(fileLocation, lines.join(EOL)); | ||
|
|
||
| exportVariable("NPM_CONFIG_USERCONFIG", fileLocation); | ||
| // Export placeholder if NODE_AUTH_TOKEN is not set so npm doesn't error | ||
| exportVariable("NODE_AUTH_TOKEN", process.env.NODE_AUTH_TOKEN || "XXXXX-XXXXX-XXXXX-XXXXX"); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.