-
Notifications
You must be signed in to change notification settings - Fork 4.3k
feat: npm stage #9201
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
Open
reggi
wants to merge
4
commits into
latest
Choose a base branch
from
reggi/stage
base: latest
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
feat: npm stage #9201
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,142 @@ | ||
| --- | ||
| title: npm-stage | ||
| section: 1 | ||
| description: Stage packages for publishing | ||
| --- | ||
|
|
||
| ### Synopsis | ||
|
|
||
| <!-- AUTOGENERATED USAGE DESCRIPTIONS --> | ||
|
|
||
| ### Description | ||
|
|
||
| Staged publishing allows package maintainers to require proof-of-presence | ||
| for all publishes. Proof-of-presence is where a human is involved, | ||
| interjects, and provides authentication (2FA) during an action β in this | ||
| case, publishing an npm package. | ||
|
|
||
| Typically when maintainers use automated workflows to publish, | ||
| proof-of-presence is lacking as there's no convenient way to interject the | ||
| process and provide 2FA, as is the case for publishing with a granular | ||
| access token with bypass and the trusted publishing flow. Staged publishing | ||
| allows users to have their automated workflows stage a package without a 2FA | ||
| prompt, deferring the act of 2FA, allowing the maintainer to approve the | ||
| staged package and publish at a later point. | ||
|
|
||
| The `npm stage publish` command packs the current working directory and | ||
| places that version of the package into the registry in a state where it's | ||
| not available for public access, allowing maintainers to approve the package | ||
| at a later point in time. The act of staging does not prompt for 2FA and can be done with any token | ||
| type, the act of approving will. | ||
|
|
||
| Key behaviors: | ||
|
|
||
| * Staged packages share the same semver version unique index as published | ||
| packages β you cannot publish a version that already exists as a staged | ||
| version for that package. | ||
| * You can still publish packages normally while you have staged packages | ||
| pending. | ||
| * You can stage multiple versions of the same package. | ||
| * `npm stage publish` has parity with `npm publish` and will respect | ||
| `"private": true` in `package.json`, refusing to stage the package. | ||
|
|
||
| ### Prerequisites | ||
|
|
||
| Before using `npm stage` commands, ensure the following requirements are met: | ||
|
|
||
| * **Write permissions on the package:** You must have write access to the | ||
| package you're configuring. | ||
| * **Package must exist:** The package you're configuring must already exist | ||
| on the npm registry. | ||
| * **2FA enabled on your account:** Commands that require 2FA will prompt you | ||
| to authenticate. If you don't already have 2FA enabled on your account, | ||
| you must enable it before using these commands. | ||
|
|
||
| ### Subcommands | ||
|
|
||
| * `npm stage publish [<package-spec>]` - Stage a package for publishing | ||
| * `npm stage list [<package-spec>]` - List all staged package versions | ||
| * `npm stage view <stage-id>` - View details of a specific staged package | ||
| * `npm stage approve <stage-id>` - Approve a staged package for publishing | ||
| * `npm stage reject <stage-id>` - Reject a staged package | ||
| * `npm stage download <stage-id>` - Download the tarball for inspection | ||
|
|
||
| ### 2FA Requirements by Subcommand | ||
|
|
||
| | Command | Requires 2FA | Notes | | ||
| | --- | --- | --- | | ||
| | `npm stage publish` | No | Designed for automated workflows; defers 2FA to approval | | ||
| | `npm stage list` | No | View staged packages | | ||
| | `npm stage view` | No | View staged package details | | ||
| | `npm stage approve` | Yes | Prompts for 2FA to publish the staged package | | ||
| | `npm stage reject` | Yes | Prompts for 2FA to permanently remove the staged package | | ||
| | `npm stage download` | No | Downloads the tarball for local inspection | | ||
|
|
||
| ### Tag Behavior | ||
|
|
||
| The `--tag` flag follows the same logic as `npm publish`. If no tag is | ||
| provided, the `latest` tag is used by default. For pre-release versions | ||
| (e.g., `1.0.0-beta.1`) and non-latest semver versions, the tag must be | ||
| explicitly provided β otherwise the CLI will error, just as `npm publish` | ||
| would. | ||
|
|
||
| The tag is an immutable property of the staged package. Once a package is | ||
| staged with a given tag, the tag cannot be changed. If you need to stage the | ||
| same version with a different tag, you must first reject the existing staged | ||
| package using `npm stage reject` and then re-stage it with the desired tag. | ||
|
|
||
| ### Token Behavior | ||
|
|
||
| The key difference with staged publishing is that `npm stage publish` never | ||
| requires a 2FA prompt, regardless of token type. This is what makes it | ||
| suitable for automated workflows. The goal of `npm stage publish` is | ||
| deferring proof-of-presence to a later point in time. | ||
|
|
||
| | Token Type | `npm stage publish` | `npm publish` | | ||
| | --- | --- | --- | | ||
| | GAT with bypass | Can stage | Can publish (if allowed by package publishing access) | | ||
| | GAT without bypass | Can stage | 2FA prompt (if allowed by package publishing access) | | ||
| | Session token | Can stage | 2FA prompt | | ||
| | Trust token (OIDC) | Can stage (if allowed) | Can publish (if allowed) | | ||
|
|
||
| ### Trust Relationship Permissions | ||
|
|
||
| With staged publishing, trust relationships now support granular command | ||
| permissions. Shortlived tokens issued through trust relationships can only be | ||
| used with `npm stage publish` and `npm publish`. Shortlived tokens cannot run | ||
| `npm stage` subcommands. | ||
|
|
||
| `npm trust <provider>` supports `--allow-publish` and `--allow-stage-publish` | ||
| to control which commands are available through each trust relationship. | ||
|
|
||
| ### Best Practices | ||
|
|
||
| **Note:** The addition of staged publishing does not make your account or org | ||
| more secure. Maintainers must still use the best practices listed below. | ||
|
|
||
| 1. **Delete Granular Access Tokens (GAT) with bypass 2FA enabled.** | ||
| Now with staged publishing, we've eliminated the need for a GAT token | ||
| that can bypass 2FA. We encourage you to delete all your tokens with | ||
| bypass enabled and switch to using a trust relationship in your automated | ||
| workflows, or create a GAT without bypass and use `npm stage publish`. | ||
|
|
||
| 2. **Disallow tokens from publishing at the package level.** | ||
| All packages have their own access controls under "package access" | ||
| allowing packages to be published with bypass tokens, which is no longer | ||
| a necessity. We encourage you to select "Require two-factor | ||
| authentication and disallow tokens (recommended)" for all your packages | ||
| on the package access page. | ||
|
|
||
| 3. **Configure trust relationship permissions to prevent `npm publish`.** | ||
| We encourage you to only enable `npm stage publish` on your trust | ||
| relationships and disable `npm publish`. | ||
|
|
||
| ### Configuration | ||
|
|
||
| <!-- AUTOGENERATED CONFIG DESCRIPTIONS --> | ||
|
|
||
| ### See Also | ||
|
|
||
| * [npm publish](/commands/npm-publish) | ||
| * [npm unpublish](/commands/npm-unpublish) | ||
| * [npm trust](/commands/npm-trust) |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| const { log, output, META } = require('proc-log') | ||
| const npmFetch = require('npm-registry-fetch') | ||
| const { otplease } = require('../../utils/auth.js') | ||
| const { validateUUID } = require('../../utils/validate-uuid.js') | ||
| const BaseCommand = require('../../base-cmd.js') | ||
|
|
||
| class StageApprove extends BaseCommand { | ||
| static description = 'Approve a staged package, publishing it to the npm registry' | ||
| static name = 'approve' | ||
| static usage = ['<stage-id>'] | ||
| static params = ['otp', 'registry'] | ||
| static positionals = 1 | ||
|
|
||
| async exec (args) { | ||
| if (!args[0]) { | ||
| throw this.usageError('Missing required <stage-id>') | ||
| } | ||
| const stageId = args[0] | ||
| validateUUID(stageId, 'stage-id') | ||
| const opts = { ...this.npm.flatOptions } | ||
|
|
||
| log.notice('', `Approving staged package ${stageId}`) | ||
|
|
||
| await otplease(this.npm, opts, o => | ||
| npmFetch.json(`/-/stage/${stageId}/approve`, { | ||
| ...o, | ||
| method: 'POST', | ||
| }) | ||
| ) | ||
|
|
||
| output.standard(`Staged package ${stageId} approved and published successfully.`, { [META]: true, redact: false }) | ||
| } | ||
| } | ||
|
|
||
| module.exports = StageApprove |
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,69 @@ | ||||||||
| const { log, output, META } = require('proc-log') | ||||||||
| const { writeFile } = require('node:fs/promises') | ||||||||
| const { resolve } = require('node:path') | ||||||||
| const tar = require('tar') | ||||||||
| const npmFetch = require('npm-registry-fetch') | ||||||||
| const { getContents, logTar } = require('../../utils/tar.js') | ||||||||
| const { validateUUID } = require('../../utils/validate-uuid.js') | ||||||||
| const BaseCommand = require('../../base-cmd.js') | ||||||||
|
|
||||||||
| class StageDownload extends BaseCommand { | ||||||||
| static description = 'Download the tarball of a staged package for inspection' | ||||||||
| static name = 'download' | ||||||||
| static usage = ['<stage-id>'] | ||||||||
| static params = ['json', 'registry'] | ||||||||
| static positionals = 1 | ||||||||
|
|
||||||||
| async exec (args) { | ||||||||
| if (!args[0]) { | ||||||||
| throw this.usageError('Missing required <stage-id>') | ||||||||
| } | ||||||||
| const stageId = args[0] | ||||||||
| validateUUID(stageId, 'stage-id') | ||||||||
| const opts = { ...this.npm.flatOptions } | ||||||||
| const unicode = this.npm.config.get('unicode') | ||||||||
| const json = this.npm.config.get('json') | ||||||||
|
|
||||||||
| log.notice('', `Downloading staged package ${stageId}`) | ||||||||
|
|
||||||||
| const res = await npmFetch(`/-/stage/${stageId}/tarball`, opts) | ||||||||
| const data = Buffer.from(await res.arrayBuffer()) | ||||||||
|
|
||||||||
| const manifest = await this.#readManifestFromTarball(data) | ||||||||
| const pkgContents = await getContents(manifest, data) | ||||||||
| logTar(pkgContents, { unicode, json }) | ||||||||
|
|
||||||||
| const safeName = manifest.name.replace('@', '').replace('/', '-') | ||||||||
| const filename = `${safeName}-${stageId}.tgz` | ||||||||
| const dest = resolve(process.cwd(), filename) | ||||||||
|
|
||||||||
| await writeFile(dest, data) | ||||||||
| if (!json) { | ||||||||
| output.standard(filename, { [META]: true, redact: false }) | ||||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
| async #readManifestFromTarball (tarballData) { | ||||||||
| let manifestJson | ||||||||
| const stream = tar.t({ | ||||||||
| onentry (entry) { | ||||||||
| if (entry.path === 'package/package.json') { | ||||||||
| const chunks = [] | ||||||||
| entry.on('data', c => chunks.push(c)) | ||||||||
| entry.on('end', () => { | ||||||||
| manifestJson = JSON.parse(Buffer.concat(chunks).toString()) | ||||||||
| }) | ||||||||
| } else { | ||||||||
| entry.resume() | ||||||||
| } | ||||||||
| }, | ||||||||
| }) | ||||||||
| stream.end(tarballData) | ||||||||
| if (!manifestJson) { | ||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This relies on
Suggested change
|
||||||||
| throw new Error('Could not read package.json from tarball') | ||||||||
| } | ||||||||
| return manifestJson | ||||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
| module.exports = StageDownload | ||||||||
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: mixed style β
#publishcapturesconst command = this.#commandand uses the local everywhere, but#skipped(above) readsthis.#commanddirectly. Pick one.