Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
eba6518
ci(release): enable triggering for the alpha branch
travi Jul 9, 2025
733fe89
fix(deps): depend on the oidc branch for npm
travi Jul 9, 2025
73185a3
fix(token): temporarily disable configuring token into .npmrc
travi Jul 9, 2025
0fc050d
fix(whoami): temporarily disable verifying token validity
travi Jul 9, 2025
156b6c8
fix: raise the minimum semantic-release requirement to v25
travi Jul 9, 2025
65dffca
Revert "fix: raise the minimum semantic-release requirement to v25"
travi Jul 10, 2025
fc30c21
fix(deps): depend on the released version of the cli branch now that …
travi Jul 25, 2025
841dc67
feat(auth): attempt a dry-run publish to determine auth status
travi Jul 28, 2025
8f88e9d
fix(auth): throw error if dry-run publish determines lack of auth
travi Jul 28, 2025
1967d72
fix(error): throw an aggregate error rather than a simple error
travi Jul 28, 2025
4ab3e74
fix(stdout): fix the reference of stdout from execa
travi Jul 28, 2025
86f65a6
fix(dry-run): look for the warning in stderr output rather than stdout
travi Jul 28, 2025
bee5db6
fix(dry-run): stop searching for "warn" to avoid ANSI color complicat…
travi Jul 28, 2025
9bdfd06
docs(oidc): update the authentication details to include recommending…
travi Aug 1, 2025
e24967d
feat(auth-error): updated messaging for auth failure to be less token…
travi Aug 1, 2025
f5c8d85
fix(auth): throw appropriate error when auth context fails to enable …
travi Aug 1, 2025
bd7acfb
feat(verify-auth): configure npmrc details again
travi Aug 15, 2025
197693e
Merge branch 'beta' of github.com:semantic-release/npm into alpha
travi Oct 13, 2025
761d4db
Merge branch 'master' of github.com:semantic-release/npm into alpha
travi Oct 13, 2025
dd9ea86
refactor(registry): extract a constant rather than repeating the url …
travi Oct 14, 2025
18b5911
wip(trusted-publishing): capture rule about targeting the official re…
travi Oct 14, 2025
1e279d8
wip(trusted-publishing): detect whether running from a trusted provider
travi Oct 14, 2025
00ab5fc
wip(trusted-publishing): limit successful oidc context detection to s…
travi Oct 14, 2025
a2aa38f
feat(trusted-publishing): highlight in the error that a token is only…
travi Oct 14, 2025
c9f0da5
fix(errors): bring back the invalid token error
travi Oct 14, 2025
e3319f1
feat(trusted-publishing): verify auth, considering OIDC vs tokens fro…
travi Oct 14, 2025
d825403
fix(errors): resolve syntax problem
travi Oct 14, 2025
739f655
test(integration): adjust to align to updated auth verification logic
travi Oct 14, 2025
e75c620
refactor(verify-auth): extract token verification logic to intent-rev…
travi Oct 14, 2025
c80ecb0
feat(trusted-publishing): make request to verify if OIDC token exchan…
travi Oct 15, 2025
3dd95d0
fix(trusted-publishing): uri encode the package name for the token ex…
travi Oct 15, 2025
4a8cd7a
docs(trusted-publishing): further clarify scenarios for trusted publi…
travi Oct 15, 2025
66e2a44
docs(trusted-publsihing): combine information about configuring githu…
travi Oct 15, 2025
d83b727
feat(trusted-publishing): pass id-token as bearer header for github a…
travi Oct 15, 2025
701510e
refactor(token-exchange): rename the function to be verb focused
travi Oct 15, 2025
f063ab4
Merge branch 'master' of github.com:semantic-release/npm into alpha
travi Oct 15, 2025
b673257
feat(trusted-publishing): handle failure to retrieve id-token in the …
travi Oct 15, 2025
6d1c3cf
feat(trusted-publishing): pass id-token as bearer header for gitlab p…
travi Oct 15, 2025
cf9011b
refactor(trusted-publishing): remove redundant ci-providers check
travi Oct 15, 2025
67ee603
fix(verify-auth): stream output of the dry-run for custom registries
travi Oct 15, 2025
a4a5add
feat(trusted-publishing): log progression of token-exchange steps
travi Oct 15, 2025
23c8610
fix(trusted-publishing): properly await the check for trusted publish…
travi Oct 15, 2025
e5c9857
docs(trusted-publishing): add details to the readme for configuring w…
travi Oct 15, 2025
38b0466
refactor: resolve prettier issues
travi Oct 15, 2025
d351eca
ci(node-versions): disable node v24 from the verification matrix for now
travi Oct 16, 2025
a160d8d
Merge pull request #1015 from semantic-release/alpha
travi Oct 16, 2025
e7d684c
fix(verify-auth): enable the publish dry-run to work for projects pub…
travi Oct 18, 2025
cf14bd9
docs(trusted-publishing): expand further around details for publishin…
travi Oct 18, 2025
316ce21
feat(trusted-publishing): refine the messages for related errors
travi Oct 18, 2025
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
1 change: 1 addition & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ name: Release
- master
- next
- beta
- alpha
- "*.x"
permissions:
contents: read
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
node-version:
- 22.14.0
- 22
- 24
# - 24
os:
- ubuntu-latest
runs-on: "${{ matrix.os }}"
Expand Down
55 changes: 39 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,41 +38,63 @@ The plugin can be configured in the [**semantic-release** configuration file](ht

### npm registry authentication

The npm [token](https://docs.npmjs.com/about-access-tokens) authentication configuration is **required** and can be set via [environment variables](#environment-variables).
### Official Registry

Automation tokens are recommended since they can be used for an automated workflow, even when your account is configured to use the [`auth-and-writes` level of 2FA](https://docs.npmjs.com/about-two-factor-authentication#authorization-and-writes).
When publishing to the [official registry](https://registry.npmjs.org/), it is recommended to publish with authentication intended for automation:

### npm provenance
- For improved security, and since access tokens have recently had their [maximum lifetimes restricted](https://github.blog/changelog/2025-09-29-strengthening-npm-security-important-changes-to-authentication-and-token-management/),
[trusted publishing](https://docs.npmjs.com/trusted-publishers) is recommended when publishing from a [supported CI provider](https://docs.npmjs.com/trusted-publishers#supported-cicd-providers)
- [Granular access tokens](https://docs.npmjs.com/creating-and-viewing-access-tokens#creating-granular-access-tokens-on-the-website) are recommended when publishing from a CI provider that is not supported by npm for trusted publishing, and can be set via [environment variables](#environment-variables).
Because these access tokens expire, rotation will need to be accounted for in this scenario.

If you are publishing to the official registry and your pipeline is on a [provider that is supported by npm for provenance](https://docs.npmjs.com/generating-provenance-statements#provenance-limitations), npm can be configured to [publish with provenance](https://docs.npmjs.com/generating-provenance-statements).
> [!NOTE]
> When using trusted publishing, provenance attestations are automatically generated for your packages without requiring provenance to be explicitly enabled.

Since semantic-release wraps the npm publish command, configuring provenance is not exposed directly.
Instead, provenance can be configured through the [other configuration options exposed by npm](https://docs.npmjs.com/generating-provenance-statements#using-third-party-package-publishing-tools).
Provenance applies specifically to publishing, so our recommendation is to configure under `publishConfig` within the `package.json`.
#### Trusted publishing from GitHub Actions

#### npm provenance on GitHub Actions

For package provenance to be signed on the GitHub Actions CI the following permission is required
to be enabled on the job:
To leverage trusted publishing and publish with provenance from GitHub Actions, the `id-token: write` permission is required to be enabled on the job:

```yaml
permissions:
id-token: write # to enable use of OIDC for npm provenance
id-token: write # to enable use of OIDC for trusted publishing and npm provenance
```

It's worth noting that if you are using semantic-release to its fullest with a GitHub release, GitHub comments,
It's also worth noting that if you are using semantic-release to its fullest with a GitHub release, GitHub comments,
and other features, then [more permissions are required](https://github.com/semantic-release/github#github-authentication) to be enabled on this job:

```yaml
permissions:
contents: write # to be able to publish a GitHub release
issues: write # to be able to comment on released issues
pull-requests: write # to be able to comment on released pull requests
id-token: write # to enable use of OIDC for npm provenance
id-token: write # to enable use of OIDC for trusted publishing and npm provenance
```

Refer to the [GitHub Actions recipe for npm package provenance](https://semantic-release.gitbook.io/semantic-release/recipes/ci-configurations/github-actions#.github-workflows-release.yml-configuration-for-node-projects) for the full CI job's YAML code example.

#### Trusted publishing for GitLab Pipelines

To leverage trusted publishing and publish with provenance from GitLab Pipelines, `NPM_ID_TOKEN` needs to be added as an entry under `id_tokens` in the job definition with an audience of `npm:registry.npmjs.org`:

```yaml
id_tokens:
NPM_ID_TOKEN:
aud: "npm:registry.npmjs.org"
```

See the [npm documentation for more details about configuring pipeline details](https://docs.npmjs.com/trusted-publishers#gitlab-cicd-configuration)

#### Unsupported CI providers

Token authentication is **required** and can be set via [environment variables](#environment-variables).
[Granular access tokens](https://docs.npmjs.com/creating-and-viewing-access-tokens#creating-granular-access-tokens-on-the-website) are recommended in this scenario, since trusted publishing is not available from all CI providers.
Because these access tokens expire, rotation will need to be accounted for in your process.

### Alternative Registries

Token authentication is **required** and can be set via [environment variables](#environment-variables).
See the documentation for your registry for details on how to create a token for automation.

### Environment variables

| Variable | Description |
Expand All @@ -97,13 +119,14 @@ The plugin uses the [`npm` CLI](https://github.com/npm/cli) which will read the

The [`registry`](https://docs.npmjs.com/misc/registry) can be configured via the npm environment variable `NPM_CONFIG_REGISTRY` and will take precedence over the configuration in `.npmrc`.

The [`registry`](https://docs.npmjs.com/misc/registry) and [`dist-tag`](https://docs.npmjs.com/cli/dist-tag) can be configured under `publishConfig` in the `package.json`:
The [`registry`](https://docs.npmjs.com/misc/registry), [`dist-tag`](https://docs.npmjs.com/cli/dist-tag), and [`provenance`](https://docs.npmjs.com/generating-provenance-statements#using-third-party-package-publishing-tools) can be configured under `publishConfig` in the `package.json`:

```json
{
"publishConfig": {
"registry": "https://registry.npmjs.org/",
"tag": "latest"
"tag": "latest",
"provenance": true
}
}
```
Expand Down
8 changes: 4 additions & 4 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export async function verifyConditions(pluginConfig, context) {

// Verify the npm authentication only if `npmPublish` is not false and `pkg.private` is not `true`
if (pluginConfig.npmPublish !== false && pkg.private !== true) {
await verifyNpmAuth(npmrc, pkg, context);
await verifyNpmAuth(npmrc, pkg, pluginConfig, context);
}
} catch (error) {
errors.push(...error.errors);
Expand All @@ -50,7 +50,7 @@ export async function prepare(pluginConfig, context) {
// Reload package.json in case a previous external step updated it
const pkg = await getPkg(pluginConfig, context);
if (!verified && pluginConfig.npmPublish !== false && pkg.private !== true) {
await verifyNpmAuth(npmrc, pkg, context);
await verifyNpmAuth(npmrc, pkg, pluginConfig, context);
}
} catch (error) {
errors.push(...error.errors);
Expand All @@ -72,7 +72,7 @@ export async function publish(pluginConfig, context) {
// Reload package.json in case a previous external step updated it
pkg = await getPkg(pluginConfig, context);
if (!verified && pluginConfig.npmPublish !== false && pkg.private !== true) {
await verifyNpmAuth(npmrc, pkg, context);
await verifyNpmAuth(npmrc, pkg, pluginConfig, context);
}
} catch (error) {
errors.push(...error.errors);
Expand All @@ -97,7 +97,7 @@ export async function addChannel(pluginConfig, context) {
// Reload package.json in case a previous external step updated it
pkg = await getPkg(pluginConfig, context);
if (!verified && pluginConfig.npmPublish !== false && pkg.private !== true) {
await verifyNpmAuth(npmrc, pkg, context);
await verifyNpmAuth(npmrc, pkg, pluginConfig, context);
}
} catch (error) {
errors.push(...error.errors);
Expand Down
4 changes: 4 additions & 0 deletions lib/definitions/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const OFFICIAL_REGISTRY = "https://registry.npmjs.org/";

export const GITHUB_ACTIONS_PROVIDER_NAME = "GitHub Actions";
export const GITLAB_PIPELINES_PROVIDER_NAME = "GitLab CI/CD";
20 changes: 14 additions & 6 deletions lib/definitions/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,22 @@ Your configuration for the \`pkgRoot\` option is \`${pkgRoot}\`.`,
export function ENONPMTOKEN({ registry }) {
return {
message: "No npm token specified.",
details: `An [npm token](${linkify(
details: `When not publishing through [trusted publishing](https://docs.npmjs.com/trusted-publishers), an [npm token](${linkify(
"README.md#npm-registry-authentication"
)}) must be created and set in the \`NPM_TOKEN\` environment variable on your CI environment.

Please make sure to create an [npm token](https://docs.npmjs.com/getting-started/working_with_tokens#how-to-create-new-tokens) and to set it in the \`NPM_TOKEN\` environment variable on your CI environment. The token must allow to publish to the registry \`${registry}\`.`,
Please make sure to create an [npm token](https://docs.npmjs.com/getting-started/working_with_tokens#how-to-create-new-tokens) and set it in the \`NPM_TOKEN\` environment variable on your CI environment. The token must allow publishing to the registry \`${registry}\`.`,
};
}

export function EINVALIDNPMAUTH({ registry }) {
return {
message: "Invalid npm authentication.",
details: `The [authentication required to publish](${linkify(
"README.md#npm-registry-authentication"
)}) configured in the \`NPM_TOKEN\` environment variable must be a valid [token](https://docs.npmjs.com/getting-started/working_with_tokens) allowed to publish to the registry \`${registry}\`.

Please make sure to set the \`NPM_TOKEN\` environment variable in your CI with the exact value of the npm token.`,
};
}

Expand All @@ -52,10 +63,7 @@ export function EINVALIDNPMTOKEN({ registry }) {
"README.md#npm-registry-authentication"
)}) configured in the \`NPM_TOKEN\` environment variable must be a valid [token](https://docs.npmjs.com/getting-started/working_with_tokens) allowing to publish to the registry \`${registry}\`.

If you are using Two Factor Authentication for your account, set its level to ["Authorization only"](https://docs.npmjs.com/getting-started/using-two-factor-authentication#levels-of-authentication) in your account settings. **semantic-release** cannot publish with the default "
Authorization and writes" level.

Please make sure to set the \`NPM_TOKEN\` environment variable in your CI with the exact value of the npm token.`,
Please verify your authentication configuration.`,
};
}

Expand Down
7 changes: 2 additions & 5 deletions lib/get-registry.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
import path from "path";
import rc from "rc";
import getRegistryUrl from "registry-auth-token/registry-url.js";
import { OFFICIAL_REGISTRY } from "./definitions/constants.js";

export default function ({ publishConfig: { registry } = {}, name }, { cwd, env }) {
return (
registry ||
env.NPM_CONFIG_REGISTRY ||
getRegistryUrl(
name.split("/")[0],
rc(
"npm",
{ registry: "https://registry.npmjs.org/" },
{ config: env.NPM_CONFIG_USERCONFIG || path.resolve(cwd, ".npmrc") }
)
rc("npm", { registry: OFFICIAL_REGISTRY }, { config: env.NPM_CONFIG_USERCONFIG || path.resolve(cwd, ".npmrc") })
)
);
}
3 changes: 2 additions & 1 deletion lib/get-release-info.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import normalizeUrl from "normalize-url";
import { OFFICIAL_REGISTRY } from "./definitions/constants.js";

export default function (
{ name },
{ env: { DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org/" }, nextRelease: { version } },
{ env: { DEFAULT_NPM_REGISTRY = OFFICIAL_REGISTRY }, nextRelease: { version } },
distTag,
registry
) {
Expand Down
5 changes: 3 additions & 2 deletions lib/set-npmrc-auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import getAuthToken from "registry-auth-token";
import nerfDart from "nerf-dart";
import AggregateError from "aggregate-error";
import getError from "./get-error.js";
import { OFFICIAL_REGISTRY } from "./definitions/constants.js";

export default async function (npmrc, registry, { cwd, env: { NPM_TOKEN, NPM_CONFIG_USERCONFIG }, logger }) {
logger.log("Verify authentication for registry %s", registry);
const { configs, ...rcConfig } = rc(
const { configs, config, ...rcConfig } = rc(
"npm",
{ registry: "https://registry.npmjs.org/" },
{ registry: OFFICIAL_REGISTRY },
{ config: NPM_CONFIG_USERCONFIG || path.resolve(cwd, ".npmrc") }
);

Expand Down
6 changes: 6 additions & 0 deletions lib/trusted-publishing/oidc-context.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { OFFICIAL_REGISTRY } from "../definitions/constants.js";
import exchangeToken from "./token-exchange.js";

export default async function oidcContextEstablished(registry, pkg, context) {
return OFFICIAL_REGISTRY === registry && !!(await exchangeToken(pkg, context));
}
72 changes: 72 additions & 0 deletions lib/trusted-publishing/token-exchange.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { getIDToken } from "@actions/core";
import envCi from "env-ci";

import {
OFFICIAL_REGISTRY,
GITHUB_ACTIONS_PROVIDER_NAME,
GITLAB_PIPELINES_PROVIDER_NAME,
} from "../definitions/constants.js";

async function exchangeIdToken(idToken, packageName, logger) {
const response = await fetch(
`${OFFICIAL_REGISTRY}-/npm/v1/oidc/token/exchange/package/${encodeURIComponent(packageName)}`,
{
method: "POST",
headers: { Authorization: `Bearer ${idToken}` },
}
);
const responseBody = await response.json();

if (response.ok) {
logger.log("OIDC token exchange with the npm registry succeeded");

return responseBody.token;
}

logger.log(`OIDC token exchange with the npm registry failed: ${response.status} ${responseBody.message}`);

return undefined;
}

async function exchangeGithubActionsToken(packageName, logger) {
let idToken;

logger.log("Verifying OIDC context for publishing from GitHub Actions");

try {
idToken = await getIDToken("npm:registry.npmjs.org");
} catch (e) {
logger.log(`Retrieval of GitHub Actions OIDC token failed: ${e.message}`);
logger.log("Have you granted the `id-token: write` permission to this workflow?");

return undefined;
}

return exchangeIdToken(idToken, packageName, logger);
}

async function exchangeGitlabPipelinesToken(packageName, logger) {
const idToken = process.env.NPM_ID_TOKEN;

logger.log("Verifying OIDC context for publishing from GitLab Pipelines");

if (!idToken) {
return undefined;
}

return exchangeIdToken(idToken, packageName, logger);
}

export default function exchangeToken(pkg, { logger }) {
const { name: ciProviderName } = envCi();

if (GITHUB_ACTIONS_PROVIDER_NAME === ciProviderName) {
return exchangeGithubActionsToken(pkg.name, logger);
}

if (GITLAB_PIPELINES_PROVIDER_NAME === ciProviderName) {
return exchangeGitlabPipelinesToken(pkg.name, logger);
}

return undefined;
}
Loading