Skip to content

Commit 2677251

Browse files
SaadnajmiCopilot
andauthored
feat(publish): Add --dry-run and --json flag to yarn npm publish (#6850)
## What's the problem this PR addresses? Addresses #6849 ## How did you fix it? Full disclosure, used Github Copilot Agent Mode to generate most of this PR here --> Saadnajmi#2 Since then, lots more manual edits on my side. I took some spirit from #3404 and `pack.ts` to determine how to print paths and when to call `report.reportInfo` vs `report.reportJson`. This adds a couple of flags to `yarn npm publish`, so that it matches other package managers like `pnpm` and `bun`, and is thus easier for tools like `nx release` to use (see nrwl/nx#29242 ). We add - `--dry-run` (Bails out right before the final publish to NPM) - `--json` (This takes advantage of StreamReports JSON config) Using both flags together looks something like this: ```shell sanajmi@Mac foo % yarn npm publish --dry-run ➤ YN0000: README.md ➤ YN0000: bar.js ➤ YN0000: package.json ➤ YN0000: xyz.js ➤ YN0000: [DRY RUN] Package would be published to https://registry.yarnpkg.com with tag latest ➤ YN0000: Done in 0s 13ms sanajmi@Mac foo % yarn npm publish --dry-run --json {"file":"README.md"} {"file":"bar.js"} {"file":"package.json"} {"file":"xyz.js"} {"name":"foo","version":"1.0.0","registry":"https://registry.yarnpkg.com","tag":"latest","files":["README.md","bar.js","package.json","xyz.js"],"access":null,"dryRun":true,"published":false,"message":"[DRY RUN] Package would be published to https://registry.yarnpkg.com with tag latest","provenance":false,"gitHead":"d4f82af6fdb7f2f81c5910535900c5ad8a70a019"} ``` ## Checklist - [x] I have read the [Contributing Guide](https://yarnpkg.com/advanced/contributing). (TODO) <!-- See https://yarnpkg.com/advanced/contributing#preparing-your-pr-to-be-released for more details. --> <!-- Check with `yarn version check` and fix with `yarn version check -i` --> - [ ] I have set the packages that need to be released for my changes to be effective. <!-- The "Testing chores" workflow validates that your PR follows our guidelines. --> <!-- If it doesn't pass, click on it to see details as to what your PR might be missing. --> - [ ] I will check that all automated PR checks pass before the PR gets reviewed. --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: Saadnajmi <[email protected]>
1 parent f3b6af5 commit 2677251

File tree

3 files changed

+122
-17
lines changed

3 files changed

+122
-17
lines changed

.yarn/versions/4c28a2b4.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
releases:
2+
"@yarnpkg/cli": patch
3+
"@yarnpkg/plugin-npm-cli": minor
4+
5+
declined:
6+
- "@yarnpkg/plugin-compat"
7+
- "@yarnpkg/plugin-constraints"
8+
- "@yarnpkg/plugin-dlx"
9+
- "@yarnpkg/plugin-essentials"
10+
- "@yarnpkg/plugin-init"
11+
- "@yarnpkg/plugin-interactive-tools"
12+
- "@yarnpkg/plugin-nm"
13+
- "@yarnpkg/plugin-pack"
14+
- "@yarnpkg/plugin-patch"
15+
- "@yarnpkg/plugin-pnp"
16+
- "@yarnpkg/plugin-pnpm"
17+
- "@yarnpkg/plugin-stage"
18+
- "@yarnpkg/plugin-typescript"
19+
- "@yarnpkg/plugin-version"
20+
- "@yarnpkg/plugin-workspace-tools"
21+
- "@yarnpkg/builder"
22+
- "@yarnpkg/core"
23+
- "@yarnpkg/doctor"

packages/acceptance-tests/pkg-tests-specs/sources/commands/publish.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {npath, xfs} from '@yarnpkg/fslib';
22

33
const {
44
tests: {testIf},
5+
misc,
56
} = require(`pkg-tests-core`);
67

78
const {
@@ -89,6 +90,41 @@ describe(`publish`, () => {
8990
});
9091
}));
9192

93+
test(`should support --dry-run flag`, makeTemporaryEnv({
94+
name: `dry-run-test`,
95+
version: `1.0.0`,
96+
}, async ({path, run, source}) => {
97+
await run(`install`);
98+
99+
const {stdout} = await run(`npm`, `publish`, `--dry-run`, `--tolerate-republish`);
100+
expect(stdout).toContain(`[DRY RUN]`);
101+
}));
102+
103+
test(`should support --json flag`, makeTemporaryEnv({
104+
name: `json-test`,
105+
version: `1.0.0`,
106+
}, async ({path, run, source}) => {
107+
await run(`install`);
108+
109+
const {stdout} = await run(`npm`, `publish`, `--json`, `--dry-run`, `--tolerate-republish`);
110+
const jsonObjects = misc.parseJsonStream(stdout);
111+
const result = jsonObjects.find((obj: any) => obj.name && obj.version);
112+
113+
expect(result).toBeDefined();
114+
expect(result).toHaveProperty(`name`, `json-test`);
115+
expect(result).toHaveProperty(`version`, `1.0.0`);
116+
expect(result).toHaveProperty(`dryRun`, true);
117+
expect(result).toHaveProperty(`registry`);
118+
expect(result).toHaveProperty(`published`, false);
119+
expect(result).toHaveProperty(`message`);
120+
121+
expect(result).toHaveProperty(`tag`);
122+
expect(result).toHaveProperty(`provenance`);
123+
124+
expect(result).toHaveProperty(`files`);
125+
expect(Array.isArray(result.files)).toBe(true);
126+
}));
127+
92128
testIf(
93129
() => !!process.env.ACTIONS_ID_TOKEN_REQUEST_URL,
94130
`should publish a package with a valid provenance statement`,

packages/plugin-npm-cli/sources/commands/npm/publish.ts

Lines changed: 63 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {BaseCommand, WorkspaceRequiredError} from '@yarnpkg/cli';
22
import {Configuration, MessageName, Project, ReportError, StreamReport, scriptUtils, miscUtils} from '@yarnpkg/core';
3+
import {npath} from '@yarnpkg/fslib';
34
import {npmConfigUtils, npmHttpUtils, npmPublishUtils} from '@yarnpkg/plugin-npm';
45
import {packUtils} from '@yarnpkg/plugin-pack';
56
import {Command, Option, Usage, UsageError} from 'clipanion';
@@ -46,6 +47,14 @@ export default class NpmPublishCommand extends BaseCommand {
4647
description: `Generate provenance for the package. Only available in GitHub Actions and GitLab CI. Can be set globally through the \`npmPublishProvenance\` setting or the \`YARN_NPM_CONFIG_PROVENANCE\` environment variable, or per-package through the \`publishConfig.provenance\` field in package.json.`,
4748
});
4849

50+
dryRun = Option.Boolean(`-n,--dry-run`, false, {
51+
description: `Show what would be published without actually publishing`,
52+
});
53+
54+
json = Option.Boolean(`--json`, false, {
55+
description: `Output the result in JSON format`,
56+
});
57+
4958
async execute() {
5059
const configuration = await Configuration.find(this.context.cwd, this.context.plugins);
5160
const {project, workspace} = await Project.find(configuration, this.context.cwd);
@@ -69,6 +78,7 @@ export default class NpmPublishCommand extends BaseCommand {
6978
const report = await StreamReport.start({
7079
configuration,
7180
stdout: this.context.stdout,
81+
json: this.json,
7282
}, async report => {
7383
// Not an error if --tolerate-republish is set
7484
if (this.tolerateRepublish) {
@@ -84,7 +94,15 @@ export default class NpmPublishCommand extends BaseCommand {
8494
throw new ReportError(MessageName.REMOTE_INVALID, `Registry returned invalid data for - missing "versions" field`);
8595

8696
if (Object.hasOwn(registryData.versions, version)) {
87-
report.reportWarning(MessageName.UNNAMED, `Registry already knows about version ${version}; skipping.`);
97+
const warning = `Registry already knows about version ${version}; skipping.`;
98+
report.reportWarning(MessageName.UNNAMED, warning);
99+
report.reportJson({
100+
name: ident.name,
101+
version,
102+
registry,
103+
warning,
104+
skipped: true,
105+
});
88106
return;
89107
}
90108
} catch (err) {
@@ -99,28 +117,38 @@ export default class NpmPublishCommand extends BaseCommand {
99117
await packUtils.prepareForPack(workspace, {report}, async () => {
100118
const files = await packUtils.genPackList(workspace);
101119

102-
for (const file of files)
103-
report.reportInfo(null, file);
120+
for (const file of files) {
121+
report.reportInfo(null, npath.fromPortablePath(file));
122+
report.reportJson({file: npath.fromPortablePath(file)});
123+
}
104124

105125
const pack = await packUtils.genPackStream(workspace, files);
106126
const buffer = await miscUtils.bufferStream(pack);
107127

108128
const gitHead = await npmPublishUtils.getGitHead(workspace.cwd);
109129

110130
let provenance = false;
131+
let provenanceMessage = ``;
111132
if (workspace.manifest.publishConfig && `provenance` in workspace.manifest.publishConfig) {
112133
provenance = Boolean(workspace.manifest.publishConfig.provenance);
113-
if (provenance) {
114-
report.reportInfo(null, `Generating provenance statement because \`publishConfig.provenance\` field is set.`);
115-
} else {
116-
report.reportInfo(null, `Skipping provenance statement because \`publishConfig.provenance\` field is set to false.`);
117-
}
134+
provenanceMessage = provenance
135+
? `Generating provenance statement because \`publishConfig.provenance\` field is set.`
136+
: `Skipping provenance statement because \`publishConfig.provenance\` field is set to false.`;
118137
} else if (this.provenance) {
119138
provenance = true;
120-
report.reportInfo(null, `Generating provenance statement because \`--provenance\` flag is set.`);
139+
provenanceMessage = `Generating provenance statement because \`--provenance\` flag is set.`;
121140
} else if (configuration.get(`npmPublishProvenance`)) {
122141
provenance = true;
123-
report.reportInfo(null, `Generating provenance statement because \`npmPublishProvenance\` setting is set.`);
142+
provenanceMessage = `Generating provenance statement because \`npmPublishProvenance\` setting is set.`;
143+
}
144+
145+
if (provenanceMessage) {
146+
report.reportInfo(null, provenanceMessage);
147+
report.reportJson({
148+
type: `provenance`,
149+
enabled: provenance,
150+
provenanceMessage,
151+
});
124152
}
125153

126154
const body = await npmPublishUtils.makePublishBody(workspace, buffer, {
@@ -131,16 +159,34 @@ export default class NpmPublishCommand extends BaseCommand {
131159
provenance,
132160
});
133161

134-
await npmHttpUtils.put(npmHttpUtils.getIdentUrl(ident), body, {
135-
configuration,
162+
if (!this.dryRun) {
163+
await npmHttpUtils.put(npmHttpUtils.getIdentUrl(ident), body, {
164+
configuration,
165+
registry,
166+
ident,
167+
otp: this.otp,
168+
jsonResponse: true,
169+
});
170+
}
171+
172+
const finalMessage = this.dryRun
173+
? `[DRY RUN] Package would be published to ${registry} with tag ${this.tag}`
174+
: `Package archive published`;
175+
176+
report.reportInfo(MessageName.UNNAMED, finalMessage);
177+
report.reportJson({
178+
name: ident.name,
179+
version,
136180
registry,
137-
ident,
138-
otp: this.otp,
139-
jsonResponse: true,
181+
tag: this.tag || `latest`,
182+
files: files.map(f => npath.fromPortablePath(f)),
183+
access: this.access || null,
184+
dryRun: this.dryRun,
185+
published: !this.dryRun,
186+
message: finalMessage,
187+
provenance: Boolean(provenance),
140188
});
141189
});
142-
143-
report.reportInfo(MessageName.UNNAMED, `Package archive published`);
144190
});
145191

146192
return report.exitCode();

0 commit comments

Comments
 (0)