Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
24 changes: 24 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,30 @@ jobs:
echo "Installed version: ${{ steps.setup.outputs.version }}"
echo "Cache hit: ${{ steps.setup.outputs.cache-hit }}"

test-registry-url:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6

- name: Setup Vite+ with registry-url
uses: ./
with:
run-install: false
cache: false
registry-url: "https://npm.pkg.github.com"
scope: "@voidzero-dev"

- name: Verify .npmrc was created
run: |
echo "NPM_CONFIG_USERCONFIG=$NPM_CONFIG_USERCONFIG"
cat "$NPM_CONFIG_USERCONFIG"
grep -q "@voidzero-dev:registry=https://npm.pkg.github.com/" "$NPM_CONFIG_USERCONFIG"
grep -q "_authToken=\${NODE_AUTH_TOKEN}" "$NPM_CONFIG_USERCONFIG"

- name: Verify NODE_AUTH_TOKEN is exported
run: |
echo "NODE_AUTH_TOKEN is set: ${NODE_AUTH_TOKEN:+yes}"

build:
runs-on: ubuntu-latest
steps:
Expand Down
33 changes: 25 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,21 @@ steps:
- cwd: ./packages/lib
```

### With Private Registry (GitHub Packages)

```yaml
steps:
- uses: actions/checkout@v6
- uses: voidzero-dev/setup-vp@v1
with:
node-version: "22"
registry-url: "https://npm.pkg.github.com"
scope: "@myorg"
- run: vp install
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
```

### Matrix Testing with Multiple Node.js Versions

```yaml
Expand All @@ -100,14 +115,16 @@ jobs:

## Inputs

| Input | Description | Required | Default |
| ----------------------- | ----------------------------------------------------------------------------------------------------- | -------- | ------------- |
| `version` | Version of Vite+ to install | No | `latest` |
| `node-version` | Node.js version to install via `vp env use` | No | Latest LTS |
| `node-version-file` | Path to file containing Node.js version (`.nvmrc`, `.node-version`, `.tool-versions`, `package.json`) | No | |
| `run-install` | Run `vp install` after setup. Accepts boolean or YAML object with `cwd`/`args` | No | `true` |
| `cache` | Enable caching of project dependencies | No | `false` |
| `cache-dependency-path` | Path to lock file for cache key generation | No | Auto-detected |
| Input | Description | Required | Default |
| ----------------------- | --------------------------------------------------------------------------------------------------------- | -------- | ------------- |
| `version` | Version of Vite+ to install | No | `latest` |
| `node-version` | Node.js version to install via `vp env use` | No | Latest LTS |
| `node-version-file` | Path to file containing Node.js version (`.nvmrc`, `.node-version`, `.tool-versions`, `package.json`) | No | |
| `run-install` | Run `vp install` after setup. Accepts boolean or YAML object with `cwd`/`args` | No | `true` |
| `cache` | Enable caching of project dependencies | No | `false` |
| `cache-dependency-path` | Path to lock file for cache key generation | No | Auto-detected |
| `registry-url` | Optional registry to set up for auth. Sets the registry in `.npmrc` and reads auth from `NODE_AUTH_TOKEN` | No | |
| `scope` | Optional scope for scoped registries. Falls back to repo owner for GitHub Packages | No | |

## Outputs

Expand Down
6 changes: 6 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ inputs:
cache-dependency-path:
description: "Path to lock file for cache key generation. Auto-detected if not specified."
required: false
registry-url:
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."
required: false
scope:
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/)."
required: false

outputs:
version:
Expand Down
150 changes: 75 additions & 75 deletions dist/index.mjs

Large diffs are not rendered by default.

133 changes: 133 additions & 0 deletions src/auth.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { describe, it, expect, beforeEach, afterEach, vi } from "vite-plus/test";
import { join } from "node:path";
import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { EOL } from "node:os";
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 lines", () => {
vi.mocked(existsSync).mockReturnValue(true);
vi.mocked(readFileSync).mockReturnValue(
`always-auth=true${EOL}registry=https://old.reg/${EOL}`,
);

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 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");
});
});
59 changes: 59 additions & 0 deletions src/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
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 {
const npmrc = resolve(process.env.RUNNER_TEMP || process.cwd(), ".npmrc");

if (!registryUrl.endsWith("/")) {
registryUrl += "/";
}

writeRegistryToFile(registryUrl, npmrc, scope);
}

function writeRegistryToFile(registryUrl: string, fileLocation: string, scope?: string): void {
// Auto-detect scope for GitHub Packages registry
if (!scope && registryUrl.includes("npm.pkg.github.com")) {
scope = process.env.GITHUB_REPOSITORY_OWNER;
}

if (scope && !scope.startsWith("@")) {
scope = "@" + scope;
}

if (scope) {
scope = scope.toLowerCase() + ":";
} else {
scope = "";
}

debug(`Setting auth in ${fileLocation}`);

let newContents = "";
if (existsSync(fileLocation)) {
const curContents = readFileSync(fileLocation, "utf8");
for (const line of curContents.split(EOL)) {
// Preserve lines that don't set the scoped registry
if (!line.toLowerCase().startsWith(`${scope}registry`)) {
newContents += line + EOL;
}
}
}

// Auth token line: remove protocol prefix from registry URL
const authString = registryUrl.replace(/^\w+:/, "") + ":_authToken=${NODE_AUTH_TOKEN}";
const registryString = `${scope}registry=${registryUrl}`;
newContents += `${authString}${EOL}${registryString}`;

writeFileSync(fileLocation, newContents);

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");
}
10 changes: 8 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { saveVpCache } from "./cache-vp.js";
import { State, Outputs } from "./types.js";
import type { Inputs } from "./types.js";
import { resolveNodeVersionFile } from "./node-version-file.js";
import { configAuthentication } from "./auth.js";

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

// Step 4: Restore cache if enabled
// Step 4: Configure registry authentication if specified
if (inputs.registryUrl) {
configAuthentication(inputs.registryUrl, inputs.scope);
}

// Step 5: Restore cache if enabled
if (inputs.cache) {
await restoreCache(inputs);
}

// Step 5: Run vp install if requested
// Step 6: Run vp install if requested
if (inputs.runInstall.length > 0) {
await runViteInstall(inputs);
}
Expand Down
2 changes: 2 additions & 0 deletions src/inputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export function getInputs(): Inputs {
runInstall: parseRunInstall(getInput("run-install")),
cache: getBooleanInput("cache"),
cacheDependencyPath: getInput("cache-dependency-path") || undefined,
registryUrl: getInput("registry-url") || undefined,
scope: getInput("scope") || undefined,
};
}

Expand Down
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ export interface Inputs {
readonly runInstall: RunInstall[];
readonly cache: boolean;
readonly cacheDependencyPath?: string;
readonly registryUrl?: string;
readonly scope?: string;
}

// Lock file types
Expand Down
Loading