Skip to content
Draft
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
9 changes: 9 additions & 0 deletions .changeset/staged-publishing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@changesets/cli": minor
"@changesets/config": minor
"@changesets/types": minor
---

Add staged npm publishing, including config and CLI overrides, native npm,
pnpm, and Yarn adapters, structured stage events, new-package preflight, and
bulk approve/reject commands.
26 changes: 25 additions & 1 deletion docs/command-line-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ The command line for changesets is the main way of interacting with it. There ar
- init
- add [--empty] [--open] [--since <ref>] [--message <text>]
- version [--ignore, --snapshot]
- publish [--otp=code, --tag]
- publish [--otp=code, --tag, --stage, --no-stage]

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- publish [--otp=code, --tag, --stage, --no-stage]
- publish [--otp=code, --tag, --stage]

- stage [approve|reject] <id...> [--otp=code, --registry=url]
- status [--since=master --verbose --output=JSON_FILE.json]
- pre [exit|enter {tag}]
- git-tag
Expand Down Expand Up @@ -116,6 +117,29 @@ Because this command assumes that the last commit is the release commit, you sho

`--tag TAGNAME` - for packages that are published, the chosen tag will be used instead of `latest`, allowing you to publish changes intended for testing and validation, not main consumption. This will most likely be used with [snapshot releases](./snapshot-releases.md).

`--stage` stages existing packages for later approval instead of publishing
them immediately. `--no-stage` disables a `stagedPublishing` config value for
one invocation. New packages cannot be staged, so Changesets checks for them
before staging any package.

Staged packages do not get Git tags by default. Explicit `--git-tag` still
creates them immediately; `--no-git-tag` disables tags. Private packages
configured for tag-only releases are tagged immediately because they do not
participate in npm staged publishing.

The staged package IDs are reported in dependency order. Approve or reject
them with:

```sh
changeset stage approve <id...>
changeset stage reject <id...>
```

IDs are processed sequentially and the command stops at the first failure.
`--otp` is forwarded to the package manager. `--registry` selects the registry
for that invocation; when a release spans multiple registries, split the IDs
into correctly scoped commands.

### Git Tags

It is useful to have git tags of a publish, to allow people looking for the code at that time to find them. We generate tags in git during publish, but you will need to push them back up if you want to make them available. We recommend after publish you run:
Expand Down
13 changes: 13 additions & 0 deletions docs/config-file-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Changesets has a minimal amount of configuration options. Mostly these are for w
"bumpVersionsWithWorkspaceProtocolOnly": false,
"changedFilePatterns": ["**"],
"format": "auto",
"stagedPublishing": false,
"privatePackages": { "version": true, "tag": false }
}
```
Expand Down Expand Up @@ -254,6 +255,18 @@ When `tag` is set to `true`, Changesets will create a tag for private packages.
}
```

## `stagedPublishing` (optional boolean)

Default value: `false`

When enabled, `changeset publish` stages existing package versions for later
approval instead of publishing them immediately. It can be overridden per
invocation with `--stage` or `--no-stage`.

New packages are rejected during preflight because npm staged publishing does
not support their first publication. Staged packages skip Git tags by default;
an explicit `--git-tag` is respected.

## `changedFilePatterns` (array of strings)

Glob patterns for changed files that should mark a package as changed. Useful to fine-tune what counts as a change (e.g. only source files, ignoring test files, etc).
Expand Down
40 changes: 37 additions & 3 deletions packages/cli/src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ import { pack } from "./commands/pack/index.ts";
import { pre } from "./commands/pre/index.ts";
import { publishPlan } from "./commands/publish-plan/index.ts";
import { publish } from "./commands/publish/index.ts";
import { stage } from "./commands/stage/index.ts";
import { status } from "./commands/status/index.ts";
import { version } from "./commands/version/index.ts";

vi.mock("./commands/init/index.ts");
vi.mock("./commands/add/index.ts");
vi.mock("./commands/version/index.ts");
vi.mock("./commands/publish/index.ts");
vi.mock("./commands/stage/index.ts");
vi.mock("./commands/publish-plan/index.ts");
vi.mock("./commands/pack/index.ts");
vi.mock("./commands/status/index.ts");
Expand Down Expand Up @@ -129,7 +131,7 @@ const tests: CommandTest[] = [
cases: [
{
args: [],
options: { gitTag: true },
options: {},
},
{
args: ["--no-git-tag"],
Expand All @@ -147,7 +149,6 @@ const tests: CommandTest[] = [
args: ["--from-pack-dir", ".packed"],
options: {
fromPackDir: ".packed",
gitTag: true,
},
},
{
Expand All @@ -156,10 +157,43 @@ const tests: CommandTest[] = [
CHANGESETS_OUTPUT: "output.ndjson",
},
options: {
gitTag: true,
output: "output.ndjson",
},
},
{
args: ["--stage"],
options: { stage: true },
},
{
args: ["--no-stage"],
options: { stage: false },
},
],
},
{
command: "stage",
fn: stage,
cases: [
{
args: [
"approve",
"1de6f3db-2ed9-4d72-b3dd-8f0e2b474a2f",
"2de6f3db-2ed9-4d72-b3dd-8f0e2b474a2f",
"--otp",
"123456",
"--registry",
"https://registry.example.com",
],
options: {
operation: "approve",
ids: [
"1de6f3db-2ed9-4d72-b3dd-8f0e2b474a2f",
"2de6f3db-2ed9-4d72-b3dd-8f0e2b474a2f",
],
otp: "123456",
registry: "https://registry.example.com",
},
},
],
},
{
Expand Down
19 changes: 18 additions & 1 deletion packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,13 +115,30 @@ cli
.option("--otp <code>", "One time password for npm publish")
.option("--tag <name>", "Publish with the given npm dist-tag")
.option("--from-pack-dir <dir>", "Publish from a packed output directory")
.option("--git-tag", "Create a git tag for the release", { default: true })
.option("--git-tag", "Create a git tag for the release")
.option("--stage", "Stage packages for approval instead of publishing")
.action(async (options) => {
withEnvOptions(normalizeOptions(options));
const { publish } = await import("./commands/publish/index.ts");
await publish(options);
});

cli
.command("stage <operation> [...ids]", "Approve or reject staged package IDs")
.option("--otp <code>", "One time password for the stage operation")
.option("--registry <url>", "Registry containing the staged packages")
.action(
async (
operation: string,
ids: string[],
options: Record<string, unknown>,
) => {
normalizeOptions(options);
const { stage } = await import("./commands/stage/index.ts");
await stage({ ...options, operation, ids });
},
);

cli
.command("publish-plan", "Show packages that are ready to publish or tag")
.option(
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/commands/init/__tests__/command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@ describe("init", () => {
"fixed",
"linked",
"updateInternalDependencies",
"stagedPublishing",
]);
});
});
2 changes: 2 additions & 0 deletions packages/cli/src/commands/pack/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ describe("pack", () => {
"version": "1.0.0",
"access": "restricted",
"tag": "latest",
"isNew": false,
"tarball": {
"path": "packages/pkg-a-1.0.0.tgz",
"integrity": "sha256-20tNDRy0gL+a7qJTdxwA/r5ifyNnZfo31qVhTweaOqA="
Expand Down Expand Up @@ -153,6 +154,7 @@ describe("pack", () => {
version: "1.0.0",
access: "public",
tag: "latest",
isNew: false,
},
{
kind: "tag-only",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ describe("getPublishPlan", () => {
version: "1.0.0",
access: "restricted",
tag: "latest",
isNew: true,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

most of these changes seem like snapshot changes but are actually normal equal checks (also, they're not using toStrictEqual?)
they should probably be changed to toMatchInlineSnapshot()

},
{
kind: "tag-only",
Expand Down Expand Up @@ -152,6 +153,7 @@ describe("getPublishPlan", () => {
version: "1.0.0",
access: "restricted",
tag: "latest",
isNew: true,
},
],
[
Expand All @@ -161,6 +163,7 @@ describe("getPublishPlan", () => {
version: "1.0.0",
access: "restricted",
tag: "latest",
isNew: true,
},
],
[
Expand Down Expand Up @@ -212,13 +215,15 @@ describe("getPublishPlan", () => {
version: "1.0.0",
access: "restricted",
tag: "latest",
isNew: true,
},
{
kind: "publish",
name: "pkg-b",
version: "1.0.0",
access: "restricted",
tag: "latest",
isNew: true,
},
],
]);
Expand Down Expand Up @@ -266,6 +271,7 @@ describe("getPublishPlan", () => {
version: "1.0.0-next.3",
access: "restricted",
tag: "latest",
isNew: false,
},
],
]);
Expand Down Expand Up @@ -305,6 +311,7 @@ describe("getPublishPlan", () => {
version: "1.1.0",
access: "restricted",
tag: "latest",
isNew: false,
},
],
]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ describe("publish-plan", () => {
version: "1.0.0",
access: "restricted",
tag: "latest",
isNew: true,
},
{
kind: "tag-only",
Expand Down Expand Up @@ -138,6 +139,7 @@ describe("publish-plan", () => {
version: "1.0.0",
access: "restricted",
tag: "latest",
isNew: true,
},
],
]);
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/commands/publish-plan/getPublishPlan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export type PublishReleaseEntry = BaseReleaseEntry & {
kind: "publish";
access: AccessType;
tag: string;
isNew?: boolean;
tarball?: TarballMetadata;
};

Expand Down Expand Up @@ -175,6 +176,7 @@ ${response.error.message || "Unknown error"}
version: localVersion,
access: pkg.packageJson.publishConfig?.access || access,
tag: getReleaseTag(publishedState, preState, options.tag),
isNew: publishedState === "never",
};
packagesToPublish.push(release);
previewLines.push(`${c.blue(release.name)}@${c.green(release.version)}`);
Expand Down
Loading