Skip to content

Commit 612922f

Browse files
authored
feat: allow token to be optional for OIDC-based publish (#247)
Closes #242
1 parent ad69356 commit 612922f

File tree

11 files changed

+93
-27
lines changed

11 files changed

+93
-27
lines changed

.github/workflows/ci-cd.yaml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ jobs:
230230
if: ${{ github.ref == 'refs/heads/main' }}
231231
name: Publish
232232
runs-on: ubuntu-latest
233+
environment: npm
233234
timeout-minutes: 10
234235

235236
permissions:
@@ -259,5 +260,3 @@ jobs:
259260

260261
- name: Publish to NPM
261262
uses: ./
262-
with:
263-
token: ${{ secrets.NPM_TOKEN }}

README.md

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,10 @@ jobs:
4444
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
4545
```
4646
47-
See [GitHub's Node.js publishing guide](https://docs.github.com/en/actions/tutorials/publish-packages/publish-nodejs-packages) for more details and examples.
47+
See GitHub's [Node.js publishing][] guide and npm's [trusted publishing][] docs for more details and examples.
48+
49+
[Node.js publishing]: https://docs.github.com/en/actions/tutorials/publish-packages/publish-nodejs-packages
50+
[trusted publishing]: https://docs.npmjs.com/trusted-publishers#supported-cicd-providers
4851
4952
## Features
5053
@@ -97,6 +100,30 @@ jobs:
97100
token: ${{ secrets.NPM_TOKEN }}
98101
```
99102

103+
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.
104+
105+
> [!IMPORTANT]
106+
> 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.
107+
108+
```diff
109+
jobs:
110+
publish:
111+
runs-on: ubuntu-latest
112+
+ permissions:
113+
+ contents: read
114+
+ id-token: write # required to use OIDC
115+
steps:
116+
- uses: actions/checkout@v5
117+
- uses: actions/setup-node@v5
118+
with:
119+
node-version: "24" # includes [email protected]
120+
- run: npm ci
121+
- run: npm test
122+
- uses: JS-DevTools/npm-publish@v4
123+
- with:
124+
- token: ${{ secrets.NPM_TOKEN }}
125+
```
126+
100127
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`:
101128

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

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

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

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

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

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

227256
### API output
228257

@@ -281,7 +310,9 @@ Arguments:
281310
282311
Options:
283312
284-
--token <token> (Required) npm authentication token.
313+
--token <token> npm authentication token.
314+
Not required if using trusted publishing.
315+
See npm documentation for details.
285316
286317
--registry <url> Registry to read from and write to.
287318
Defaults to "https://registry.npmjs.org/".

action.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ branding:
99
inputs:
1010
token:
1111
description: The NPM access token to use when publishing
12-
required: true
12+
required: false
1313

1414
registry:
1515
description: The NPM registry URL to use

dist/main.js

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/__tests__/normalize-options.test.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,15 +57,22 @@ describe("normalizeOptions", () => {
5757
});
5858

5959
it("should throw if token invalid", () => {
60-
expect(() => {
61-
subject.normalizeOptions(manifest, { token: "" });
62-
}).toThrow(errors.InvalidTokenError);
63-
6460
expect(() => {
6561
// @ts-expect-error: intentionally mistyped for validation testing
66-
subject.normalizeOptions({ token: 42 }, manifest);
62+
subject.normalizeOptions(manifest, { token: 0 });
6763
}).toThrow(errors.InvalidTokenError);
6864
});
65+
66+
// eslint-disable-next-line unicorn/no-null
67+
it.each([undefined, null, ""])(
68+
"should set unspecified token %j to undefined",
69+
(token) => {
70+
// @ts-expect-error: intentionally mistyped for validation testing
71+
const result = subject.normalizeOptions(manifest, { token });
72+
73+
expect(result).toMatchObject({ token: undefined });
74+
}
75+
);
6976
});
7077

7178
describe("publishConfig", () => {

src/cli/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ Arguments:
1414
1515
Options:
1616
17-
--token <token> (Required) npm authentication token.
17+
--token <token> npm authentication token.
18+
Not required if using trusted publishing.
19+
See npm documentation for details.
1820
1921
--registry <url> Registry to read from and write to.
2022
Defaults to "https://registry.npmjs.org/".

src/normalize-options.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export const TAG_LATEST = "latest";
1919
/** Normalized and sanitized auth, publish, and runtime configurations. */
2020
export interface NormalizedOptions {
2121
registry: URL;
22-
token: string;
22+
token: string | undefined;
2323
tag: ConfigValue<string>;
2424
access: ConfigValue<Access | undefined>;
2525
provenance: ConfigValue<boolean>;
@@ -58,7 +58,7 @@ export function normalizeOptions(
5858
const defaultProvenance = manifest.publishConfig?.provenance ?? false;
5959

6060
return {
61-
token: validateToken(options.token),
61+
token: validateToken(options.token ?? undefined),
6262
registry: validateRegistry(options.registry ?? defaultRegistry),
6363
tag: setValue(options.tag, defaultTag, validateTag),
6464
access: setValue(options.access, defaultAccess, validateAccess),
@@ -80,12 +80,12 @@ const setValue = <TValue>(
8080
isDefault: value === undefined,
8181
});
8282

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

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

9191
const validateRegistry = (value: unknown): URL => {

src/npm/__tests__/use-npm-environment.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,4 +77,24 @@ describe("useNpmEnvironment", () => {
7777
await expect(fs.access(npmrcPath!)).rejects.toThrow(/ENOENT/);
7878
}
7979
);
80+
81+
it("allows unspecified token", async () => {
82+
const inputManifest = { name: "fizzbuzz" } as PackageManifest;
83+
const inputOptions = {
84+
token: undefined,
85+
registry: new URL("http://example.com/"),
86+
temporaryDirectory: directory,
87+
} as NormalizedOptions;
88+
89+
const result = await subject.useNpmEnvironment(
90+
inputManifest,
91+
inputOptions,
92+
async (_manifest, _options, environment) => {
93+
await Promise.resolve();
94+
return environment;
95+
}
96+
);
97+
98+
expect(result).toMatchObject({ NODE_AUTH_TOKEN: "" });
99+
});
80100
});

src/npm/call-npm-cli.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ export const VIEW = "view";
3636
export const PUBLISH = "publish";
3737

3838
export const E404 = "E404";
39-
export const E409 = "E409";
4039
export const EPUBLISHCONFLICT = "EPUBLISHCONFLICT";
4140

4241
const IS_WINDOWS = os.platform() === "win32";

src/npm/use-npm-environment.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,10 @@ export async function useNpmEnvironment<TReturn>(
4242
path.join(temporaryDirectory, "npm-publish-")
4343
);
4444
const npmrc = path.join(npmrcDirectory, ".npmrc");
45-
const environment = { NODE_AUTH_TOKEN: token, npm_config_userconfig: npmrc };
45+
const environment = {
46+
NODE_AUTH_TOKEN: token ?? "",
47+
npm_config_userconfig: npmrc,
48+
};
4649

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

0 commit comments

Comments
 (0)