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
3 changes: 1 addition & 2 deletions .github/workflows/ci-cd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ jobs:
if: ${{ github.ref == 'refs/heads/main' }}
name: Publish
runs-on: ubuntu-latest
environment: npm
timeout-minutes: 10

permissions:
Expand Down Expand Up @@ -259,5 +260,3 @@ jobs:

- name: Publish to NPM
uses: ./
with:
token: ${{ secrets.NPM_TOKEN }}
39 changes: 35 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,10 @@ jobs:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
```

See [GitHub's Node.js publishing guide](https://docs.github.com/en/actions/tutorials/publish-packages/publish-nodejs-packages) for more details and examples.
See GitHub's [Node.js publishing][] guide and npm's [trusted publishing][] docs for more details and examples.

[Node.js publishing]: https://docs.github.com/en/actions/tutorials/publish-packages/publish-nodejs-packages
[trusted publishing]: https://docs.npmjs.com/trusted-publishers#supported-cicd-providers

## Features

Expand Down Expand Up @@ -97,6 +100,30 @@ jobs:
token: ${{ secrets.NPM_TOKEN }}
```

If you have [trusted publishing][] configured for your package and use `npm@>=11.5.1`, you can omit the `token` input and use OIDC instead.

> [!IMPORTANT]
> If you're publishing a private package, you will still need to provide a read-only `token` so the action can read existing versions from the registry before publish.

```diff
jobs:
publish:
runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ id-token: write # required to use OIDC
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: "24" # includes npm@11.6.0
- run: npm ci
- run: npm test
- uses: JS-DevTools/npm-publish@v4
- with:
- token: ${{ secrets.NPM_TOKEN }}
```

You can also publish to third-party registries. For example, to publish to the [GitHub Package Registry][], set `token` to `secrets.GITHUB_TOKEN` and `registry` to `https://npm.pkg.github.com`:

```yaml
Expand Down Expand Up @@ -134,7 +161,7 @@ You can set any or all of the following input parameters using `with`:

| Name | Type | Default | Description |
| ---------------- | ---------------------- | ----------------------------- | -------------------------------------------------------------------------------- |
| `token` | string | **required** | Authentication token to use with the configured registry. |
| `token` | string | unspecified | Registry authentication token, not required if using [trusted publishing][]³ |
| `registry`¹ | string | `https://registry.npmjs.org/` | Registry URL to use. |
| `package` | string | Current working directory | Path to a package directory, a `package.json`, or a packed `.tgz` to publish. |
| `tag`¹ | string | `latest` | [Distribution tag][npm-tag] to publish to. |
Expand All @@ -146,6 +173,7 @@ You can set any or all of the following input parameters using `with`:

1. May be specified using `publishConfig` in `package.json`.
2. Provenance requires npm `>=9.5.0`.
3. Trusted publishing npm `>=11.5.1` and must be run from a supported cloud provider.

[npm-tag]: https://docs.npmjs.com/cli/v9/commands/npm-publish#tag
[npm-access]: https://docs.npmjs.com/cli/v9/commands/npm-publish#access
Expand Down Expand Up @@ -209,7 +237,7 @@ import type { Options } from "@jsdevtools/npm-publish";

| Name | Type | Default | Description |
| -------------------- | ---------------------- | ----------------------------- | -------------------------------------------------------------------------------- |
| `token` | string | **required** | Authentication token to use with the configured registry. |
| `token` | string | **required** | Registry authentication token, not required if using [trusted publishing][]³ |
| `registry`¹ | string, `URL` | `https://registry.npmjs.org/` | Registry URL to use. |
| `package` | string | Current working directory | Path to a package directory, a `package.json`, or a packed `.tgz` to publish. |
| `tag`¹ | string | `latest` | [Distribution tag][npm-tag] to publish to. |
Expand All @@ -223,6 +251,7 @@ import type { Options } from "@jsdevtools/npm-publish";

1. May be specified using `publishConfig` in `package.json`.
2. Provenance requires npm `>=9.5.0`.
3. Trusted publishing npm `>=11.5.1` and must be run from a supported cloud provider.

### API output

Expand Down Expand Up @@ -281,7 +310,9 @@ Arguments:

Options:

--token <token> (Required) npm authentication token.
--token <token> npm authentication token.
Not required if using trusted publishing.
See npm documentation for details.

--registry <url> Registry to read from and write to.
Defaults to "https://registry.npmjs.org/".
Expand Down
2 changes: 1 addition & 1 deletion action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ branding:
inputs:
token:
description: The NPM access token to use when publishing
required: true
required: false

registry:
description: The NPM registry URL to use
Expand Down
8 changes: 4 additions & 4 deletions dist/main.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 12 additions & 5 deletions src/__tests__/normalize-options.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,22 @@ describe("normalizeOptions", () => {
});

it("should throw if token invalid", () => {
expect(() => {
subject.normalizeOptions(manifest, { token: "" });
}).toThrow(errors.InvalidTokenError);

expect(() => {
// @ts-expect-error: intentionally mistyped for validation testing
subject.normalizeOptions({ token: 42 }, manifest);
subject.normalizeOptions(manifest, { token: 0 });
}).toThrow(errors.InvalidTokenError);
});

// eslint-disable-next-line unicorn/no-null
it.each([undefined, null, ""])(
"should set unspecified token %j to undefined",
(token) => {
// @ts-expect-error: intentionally mistyped for validation testing
const result = subject.normalizeOptions(manifest, { token });

expect(result).toMatchObject({ token: undefined });
}
);
});

describe("publishConfig", () => {
Expand Down
4 changes: 3 additions & 1 deletion src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ Arguments:

Options:

--token <token> (Required) npm authentication token.
--token <token> npm authentication token.
Not required if using trusted publishing.
See npm documentation for details.

--registry <url> Registry to read from and write to.
Defaults to "https://registry.npmjs.org/".
Expand Down
12 changes: 6 additions & 6 deletions src/normalize-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const TAG_LATEST = "latest";
/** Normalized and sanitized auth, publish, and runtime configurations. */
export interface NormalizedOptions {
registry: URL;
token: string;
token: string | undefined;
tag: ConfigValue<string>;
access: ConfigValue<Access | undefined>;
provenance: ConfigValue<boolean>;
Expand Down Expand Up @@ -58,7 +58,7 @@ export function normalizeOptions(
const defaultProvenance = manifest.publishConfig?.provenance ?? false;

return {
token: validateToken(options.token),
token: validateToken(options.token ?? undefined),
registry: validateRegistry(options.registry ?? defaultRegistry),
tag: setValue(options.tag, defaultTag, validateTag),
access: setValue(options.access, defaultAccess, validateAccess),
Expand All @@ -80,12 +80,12 @@ const setValue = <TValue>(
isDefault: value === undefined,
});

const validateToken = (value: unknown): string => {
if (typeof value === "string" && value.length > 0) {
return value;
const validateToken = (value: unknown): string | undefined => {
if (typeof value !== "string" && value !== undefined && value !== null) {
throw new errors.InvalidTokenError();
}

throw new errors.InvalidTokenError();
return typeof value === "string" && value.length > 0 ? value : undefined;
};

const validateRegistry = (value: unknown): URL => {
Expand Down
20 changes: 20 additions & 0 deletions src/npm/__tests__/use-npm-environment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,24 @@ describe("useNpmEnvironment", () => {
await expect(fs.access(npmrcPath!)).rejects.toThrow(/ENOENT/);
}
);

it("allows unspecified token", async () => {
const inputManifest = { name: "fizzbuzz" } as PackageManifest;
const inputOptions = {
token: undefined,
registry: new URL("http://example.com/"),
temporaryDirectory: directory,
} as NormalizedOptions;

const result = await subject.useNpmEnvironment(
inputManifest,
inputOptions,
async (_manifest, _options, environment) => {
await Promise.resolve();
return environment;
}
);

expect(result).toMatchObject({ NODE_AUTH_TOKEN: "" });
});
});
1 change: 0 additions & 1 deletion src/npm/call-npm-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ export const VIEW = "view";
export const PUBLISH = "publish";

export const E404 = "E404";
export const E409 = "E409";
export const EPUBLISHCONFLICT = "EPUBLISHCONFLICT";

const IS_WINDOWS = os.platform() === "win32";
Expand Down
5 changes: 4 additions & 1 deletion src/npm/use-npm-environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@ export async function useNpmEnvironment<TReturn>(
path.join(temporaryDirectory, "npm-publish-")
);
const npmrc = path.join(npmrcDirectory, ".npmrc");
const environment = { NODE_AUTH_TOKEN: token, npm_config_userconfig: npmrc };
const environment = {
NODE_AUTH_TOKEN: token ?? "",
npm_config_userconfig: npmrc,
};

await fs.writeFile(npmrc, config, "utf8");

Expand Down
9 changes: 7 additions & 2 deletions src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,13 @@ export interface Logger {

/** Options that determine how/whether the package is published. */
export interface Options {
/** The NPM access token to use when publishing. */
token: string;
/**
* The NPM access token to use when publishing.
*
* May be left unspecified if `npm` is running in an environment with [trusted
* publishing](https://docs.npmjs.com/trusted-publishers#supported-cicd-providers)
*/
token?: string | undefined;

/**
* The absolute or relative path of your package.
Expand Down
Loading