Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
37b51fe
Initial plan
Copilot Jul 17, 2025
670b25f
Implement dry-run, JSON output, and registry options for yarn npm pub…
Copilot Jul 17, 2025
28df538
Final implementation with tests and linting fixes
Copilot Jul 17, 2025
3118cef
Consolidate duplicate code between JSON and stream output paths in np…
Copilot Jul 17, 2025
c0b4e94
Rename report execution path to executeWithReportStream for clarity
Copilot Jul 17, 2025
0a6452a
Refactor streaming report checks into isStreamingReport helper method
Copilot Jul 17, 2025
596cf55
Use StreamReport's built-in JSON support instead of separate executio…
Copilot Jul 17, 2025
84781b2
Change directory option from positional argument to --directory flag
Copilot Jul 17, 2025
22b0854
Remove unnecessary executeCore method - consolidate into single execu…
Copilot Jul 17, 2025
ee34047
Fold extracted methods back into single execute method to eliminate a…
Copilot Jul 17, 2025
afd8de9
Remove redundant JSON conditionals where reportInfo handles JSON mode…
Copilot Jul 17, 2025
2b4682e
Undo directroy and registry flags
Saadnajmi Jul 18, 2025
4b1259b
more removal
Saadnajmi Jul 18, 2025
1dda19e
Simplify messages
Saadnajmi Jul 18, 2025
ef33377
version plan
Saadnajmi Jul 26, 2025
205ed41
JSON path reports same info as non JSON path, test passes
Saadnajmi Jul 27, 2025
44634b7
Update 4c28a2b4.yml
Saadnajmi Jul 27, 2025
1aee84b
update again
Saadnajmi Jul 27, 2025
d4f82af
Copy what pack.ts does and always call reportInfo with reportJson
Saadnajmi Jul 27, 2025
9811340
Don't report gitHead
Saadnajmi Jul 27, 2025
06961be
support -n for dry run, like pack
Saadnajmi Jul 27, 2025
7e52b9e
PR feedback
Saadnajmi Jul 30, 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
23 changes: 23 additions & 0 deletions .yarn/versions/4c28a2b4.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
releases:
"@yarnpkg/cli": patch
"@yarnpkg/plugin-npm-cli": minor

declined:
- "@yarnpkg/plugin-compat"
- "@yarnpkg/plugin-constraints"
- "@yarnpkg/plugin-dlx"
- "@yarnpkg/plugin-essentials"
- "@yarnpkg/plugin-init"
- "@yarnpkg/plugin-interactive-tools"
- "@yarnpkg/plugin-nm"
- "@yarnpkg/plugin-pack"
- "@yarnpkg/plugin-patch"
- "@yarnpkg/plugin-pnp"
- "@yarnpkg/plugin-pnpm"
- "@yarnpkg/plugin-stage"
- "@yarnpkg/plugin-typescript"
- "@yarnpkg/plugin-version"
- "@yarnpkg/plugin-workspace-tools"
- "@yarnpkg/builder"
- "@yarnpkg/core"
- "@yarnpkg/doctor"
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {npath, xfs} from '@yarnpkg/fslib';

const {
tests: {testIf},
misc,
} = require(`pkg-tests-core`);

const {
Expand Down Expand Up @@ -89,6 +90,41 @@ describe(`publish`, () => {
});
}));

test(`should support --dry-run flag`, makeTemporaryEnv({
name: `dry-run-test`,
version: `1.0.0`,
}, async ({path, run, source}) => {
await run(`install`);

const {stdout} = await run(`npm`, `publish`, `--dry-run`, `--tolerate-republish`);
expect(stdout).toContain(`[DRY RUN]`);
}));

test(`should support --json flag`, makeTemporaryEnv({
name: `json-test`,
version: `1.0.0`,
}, async ({path, run, source}) => {
await run(`install`);

const {stdout} = await run(`npm`, `publish`, `--json`, `--dry-run`, `--tolerate-republish`);
const jsonObjects = misc.parseJsonStream(stdout);
const result = jsonObjects.find((obj: any) => obj.name && obj.version);

expect(result).toBeDefined();
expect(result).toHaveProperty(`name`, `json-test`);
expect(result).toHaveProperty(`version`, `1.0.0`);
expect(result).toHaveProperty(`dryRun`, true);
expect(result).toHaveProperty(`registry`);
expect(result).toHaveProperty(`published`, false);
expect(result).toHaveProperty(`message`);

expect(result).toHaveProperty(`tag`);
expect(result).toHaveProperty(`provenance`);

expect(result).toHaveProperty(`files`);
expect(Array.isArray(result.files)).toBe(true);
}));

testIf(
() => !!process.env.ACTIONS_ID_TOKEN_REQUEST_URL,
`should publish a package with a valid provenance statement`,
Expand Down
80 changes: 63 additions & 17 deletions packages/plugin-npm-cli/sources/commands/npm/publish.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {BaseCommand, WorkspaceRequiredError} from '@yarnpkg/cli';
import {Configuration, MessageName, Project, ReportError, StreamReport, scriptUtils, miscUtils} from '@yarnpkg/core';
import {npath} from '@yarnpkg/fslib';
import {npmConfigUtils, npmHttpUtils, npmPublishUtils} from '@yarnpkg/plugin-npm';
import {packUtils} from '@yarnpkg/plugin-pack';
import {Command, Option, Usage, UsageError} from 'clipanion';
Expand Down Expand Up @@ -46,6 +47,14 @@ export default class NpmPublishCommand extends BaseCommand {
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.`,
});

dryRun = Option.Boolean(`-n,--dry-run`, false, {
description: `Show what would be published without actually publishing`,
});

json = Option.Boolean(`--json`, false, {
description: `Output the result in JSON format`,
});

async execute() {
const configuration = await Configuration.find(this.context.cwd, this.context.plugins);
const {project, workspace} = await Project.find(configuration, this.context.cwd);
Expand All @@ -69,6 +78,7 @@ export default class NpmPublishCommand extends BaseCommand {
const report = await StreamReport.start({
configuration,
stdout: this.context.stdout,
json: this.json,
}, async report => {
// Not an error if --tolerate-republish is set
if (this.tolerateRepublish) {
Expand All @@ -84,7 +94,15 @@ export default class NpmPublishCommand extends BaseCommand {
throw new ReportError(MessageName.REMOTE_INVALID, `Registry returned invalid data for - missing "versions" field`);

if (Object.hasOwn(registryData.versions, version)) {
report.reportWarning(MessageName.UNNAMED, `Registry already knows about version ${version}; skipping.`);
const warning = `Registry already knows about version ${version}; skipping.`;
report.reportWarning(MessageName.UNNAMED, warning);
report.reportJson({
name: ident.name,
version,
registry,
warning,
skipped: true,
});
return;
}
} catch (err) {
Expand All @@ -99,28 +117,38 @@ export default class NpmPublishCommand extends BaseCommand {
await packUtils.prepareForPack(workspace, {report}, async () => {
const files = await packUtils.genPackList(workspace);

for (const file of files)
report.reportInfo(null, file);
for (const file of files) {
report.reportInfo(null, npath.fromPortablePath(file));
report.reportJson({file: npath.fromPortablePath(file)});
}

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

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

let provenance = false;
let provenanceMessage = ``;
if (workspace.manifest.publishConfig && `provenance` in workspace.manifest.publishConfig) {
provenance = Boolean(workspace.manifest.publishConfig.provenance);
if (provenance) {
report.reportInfo(null, `Generating provenance statement because \`publishConfig.provenance\` field is set.`);
} else {
report.reportInfo(null, `Skipping provenance statement because \`publishConfig.provenance\` field is set to false.`);
}
provenanceMessage = provenance
? `Generating provenance statement because \`publishConfig.provenance\` field is set.`
: `Skipping provenance statement because \`publishConfig.provenance\` field is set to false.`;
} else if (this.provenance) {
provenance = true;
report.reportInfo(null, `Generating provenance statement because \`--provenance\` flag is set.`);
provenanceMessage = `Generating provenance statement because \`--provenance\` flag is set.`;
} else if (configuration.get(`npmPublishProvenance`)) {
provenance = true;
report.reportInfo(null, `Generating provenance statement because \`npmPublishProvenance\` setting is set.`);
provenanceMessage = `Generating provenance statement because \`npmPublishProvenance\` setting is set.`;
}

if (provenanceMessage) {
report.reportInfo(null, provenanceMessage);
report.reportJson({
type: `provenance`,
enabled: provenance,
provenanceMessage,
});
}

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

await npmHttpUtils.put(npmHttpUtils.getIdentUrl(ident), body, {
configuration,
if (!this.dryRun) {
await npmHttpUtils.put(npmHttpUtils.getIdentUrl(ident), body, {
configuration,
registry,
ident,
otp: this.otp,
jsonResponse: true,
});
}

const finalMessage = this.dryRun
? `[DRY RUN] Package would be published to ${registry} with tag ${this.tag}`
: `Package archive published`;

report.reportInfo(MessageName.UNNAMED, finalMessage);
report.reportJson({
name: ident.name,
version,
registry,
ident,
otp: this.otp,
jsonResponse: true,
tag: this.tag || `latest`,
files: files.map(f => npath.fromPortablePath(f)),
access: this.access || null,
dryRun: this.dryRun,
published: !this.dryRun,
message: finalMessage,
provenance: Boolean(provenance),
});
});

report.reportInfo(MessageName.UNNAMED, `Package archive published`);
});

return report.exitCode();
Expand Down
Loading