-
Notifications
You must be signed in to change notification settings - Fork 0
[Feature] Improve yarn npm publish to match other package managers #2
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
Changes from 4 commits
37b51fe
670b25f
28df538
3118cef
c0b4e94
0a6452a
596cf55
84781b2
22b0854
ee34047
afd8de9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 {ppath} from '@yarnpkg/fslib'; | ||
| import {npmConfigUtils, npmHttpUtils, npmPublishUtils} from '@yarnpkg/plugin-npm'; | ||
| import {packUtils} from '@yarnpkg/plugin-pack'; | ||
| import {Command, Option, Usage, UsageError} from 'clipanion'; | ||
|
|
@@ -46,12 +47,30 @@ 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(`--dry-run`, false, { | ||
| description: `Show what would be published without actually publishing`, | ||
| }); | ||
|
|
||
| json = Option.Boolean(`--json`, false, { | ||
| description: `Output the result in JSON format`, | ||
| }); | ||
|
|
||
| registry = Option.String(`--registry`, { | ||
| description: `The registry to publish to`, | ||
| }); | ||
|
|
||
| directory = Option.String({ | ||
| description: `The directory to publish (defaults to current directory)`, | ||
| required: false, | ||
| }); | ||
|
|
||
| async execute() { | ||
| const configuration = await Configuration.find(this.context.cwd, this.context.plugins); | ||
| const {project, workspace} = await Project.find(configuration, this.context.cwd); | ||
| const cwd = this.directory ? ppath.resolve(this.context.cwd, this.directory) : this.context.cwd; | ||
| const configuration = await Configuration.find(cwd, this.context.plugins); | ||
| const {project, workspace} = await Project.find(configuration, cwd); | ||
|
|
||
| if (!workspace) | ||
| throw new WorkspaceRequiredError(project.cwd, this.context.cwd); | ||
| throw new WorkspaceRequiredError(project.cwd, cwd); | ||
|
|
||
| if (workspace.manifest.private) | ||
| throw new UsageError(`Private workspaces cannot be published`); | ||
|
|
@@ -64,85 +83,180 @@ export default class NpmPublishCommand extends BaseCommand { | |
| const ident = workspace.manifest.name; | ||
| const version = workspace.manifest.version; | ||
|
|
||
| const registry = npmConfigUtils.getPublishRegistry(workspace.manifest, {configuration}); | ||
| const registry = this.registry || npmConfigUtils.getPublishRegistry(workspace.manifest, {configuration}); | ||
|
|
||
| // For JSON output, we collect data differently but use the same core logic | ||
| if (this.json) { | ||
| const result = await this.executeWithCollectedOutput(workspace, registry, configuration, ident, version); | ||
| this.context.stdout.write(`${JSON.stringify(result, null, 2)}\n`); | ||
| return 0; | ||
| } | ||
|
|
||
| const report = await StreamReport.start({ | ||
| configuration, | ||
| stdout: this.context.stdout, | ||
| }, async report => { | ||
| // Not an error if --tolerate-republish is set | ||
| if (this.tolerateRepublish) { | ||
| try { | ||
| const registryData = await npmHttpUtils.get(npmHttpUtils.getIdentUrl(ident), { | ||
| configuration, | ||
| registry, | ||
| ident, | ||
| jsonResponse: true, | ||
| }); | ||
|
|
||
| if (!Object.hasOwn(registryData, `versions`)) | ||
| 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.`); | ||
| return; | ||
| } | ||
| } catch (err) { | ||
| if (err.originalError?.response?.statusCode !== 404) { | ||
| throw err; | ||
| } | ||
| } | ||
| } | ||
| await this.executeCore(workspace, registry, configuration, ident, version, report); | ||
| }); | ||
|
|
||
| return report.exitCode(); | ||
| } | ||
Saadnajmi marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| await scriptUtils.maybeExecuteWorkspaceLifecycleScript(workspace, `prepublish`, {report}); | ||
| private async executeCore(workspace: any, registry: string, configuration: any, ident: any, version: string, report: any) { | ||
|
||
| // Check if we should skip republishing | ||
| const shouldSkip = await this.checkTolerateRepublish(ident, version, configuration, registry); | ||
| if (shouldSkip) { | ||
| report.reportWarning(MessageName.UNNAMED, `Registry already knows about version ${version}; skipping.`); | ||
| return; | ||
| } | ||
|
|
||
| await packUtils.prepareForPack(workspace, {report}, async () => { | ||
| const files = await packUtils.genPackList(workspace); | ||
| await scriptUtils.maybeExecuteWorkspaceLifecycleScript(workspace, `prepublish`, {report}); | ||
|
|
||
| for (const file of files) | ||
| report.reportInfo(null, file); | ||
| await packUtils.prepareForPack(workspace, {report}, async () => { | ||
| await this.performPackAndPublish(workspace, registry, configuration, ident, report); | ||
| }); | ||
|
|
||
| const pack = await packUtils.genPackStream(workspace, files); | ||
| const buffer = await miscUtils.bufferStream(pack); | ||
| const message = this.dryRun | ||
| ? `[DRY RUN] Package publication completed` | ||
| : `Package archive published`; | ||
| report.reportInfo(MessageName.UNNAMED, message); | ||
| } | ||
|
|
||
| const gitHead = await npmPublishUtils.getGitHead(workspace.cwd); | ||
| private async executeWithCollectedOutput(workspace: any, registry: string, configuration: any, ident: any, version: string) { | ||
| const result: any = { | ||
| name: ident.name, | ||
| version, | ||
| registry, | ||
| dryRun: this.dryRun, | ||
| }; | ||
|
|
||
| let provenance = false; | ||
| 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.`); | ||
| try { | ||
| // Check if we should skip republishing | ||
| const shouldSkip = await this.checkTolerateRepublish(ident, version, configuration, registry); | ||
| if (shouldSkip) { | ||
| result.warning = `Registry already knows about version ${version}; skipping.`; | ||
| return result; | ||
| } | ||
|
|
||
| // Create a mock report that collects data instead of logging | ||
| const dataCollector = { | ||
| files: [] as Array<string>, | ||
| reportInfo: (name: any, file: string) => { | ||
| if (file) { | ||
| result.files = result.files || []; | ||
| result.files.push(file); | ||
| } | ||
| } else if (this.provenance) { | ||
| provenance = true; | ||
| report.reportInfo(null, `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.`); | ||
| } | ||
|
|
||
| const body = await npmPublishUtils.makePublishBody(workspace, buffer, { | ||
| access: this.access, | ||
| tag: this.tag, | ||
| registry, | ||
| gitHead, | ||
| provenance, | ||
| }); | ||
|
|
||
| await npmHttpUtils.put(npmHttpUtils.getIdentUrl(ident), body, { | ||
| configuration, | ||
| registry, | ||
| ident, | ||
| otp: this.otp, | ||
| jsonResponse: true, | ||
| }); | ||
| }, | ||
| reportWarning: () => {}, | ||
| reportError: () => {}, | ||
| }; | ||
|
|
||
| await packUtils.prepareForPack(workspace, dataCollector, async () => { | ||
| const publishData = await this.performPackAndPublish(workspace, registry, configuration, ident, dataCollector); | ||
| Object.assign(result, publishData); | ||
| }); | ||
|
|
||
| report.reportInfo(MessageName.UNNAMED, `Package archive published`); | ||
| result.message = this.dryRun | ||
| ? `Package publication completed (dry run)` | ||
| : `Package archive published`; | ||
|
|
||
| return result; | ||
| } catch (error) { | ||
| result.error = error.message; | ||
| return result; | ||
| } | ||
| } | ||
|
|
||
| private async checkTolerateRepublish(ident: any, version: string, configuration: any, registry: string): Promise<boolean> { | ||
| if (!this.tolerateRepublish) return false; | ||
|
|
||
| try { | ||
| const registryData = await npmHttpUtils.get(npmHttpUtils.getIdentUrl(ident), { | ||
| configuration, | ||
| registry, | ||
| ident, | ||
| jsonResponse: true, | ||
| }); | ||
|
|
||
| if (!Object.hasOwn(registryData, `versions`)) | ||
| throw new ReportError(MessageName.REMOTE_INVALID, `Registry returned invalid data for - missing "versions" field`); | ||
|
|
||
| return Object.hasOwn(registryData.versions, version); | ||
| } catch (err) { | ||
| if (err.originalError?.response?.statusCode !== 404) | ||
| throw err; | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| private determineProvenance(workspace: any, configuration: any): boolean { | ||
| if (workspace.manifest.publishConfig && `provenance` in workspace.manifest.publishConfig) | ||
| return Boolean(workspace.manifest.publishConfig.provenance); | ||
| if (this.provenance) | ||
| return true; | ||
| if (configuration.get(`npmPublishProvenance`)) | ||
| return true; | ||
| return false; | ||
| } | ||
|
|
||
| private reportProvenanceDecision(provenance: boolean, workspace: any, report: any) { | ||
| if (workspace.manifest.publishConfig && `provenance` in workspace.manifest.publishConfig) { | ||
| const message = provenance | ||
| ? `Generating provenance statement because \`publishConfig.provenance\` field is set.` | ||
| : `Skipping provenance statement because \`publishConfig.provenance\` field is set to false.`; | ||
| report.reportInfo(null, message); | ||
| } else if (this.provenance) { | ||
| report.reportInfo(null, `Generating provenance statement because \`--provenance\` flag is set.`); | ||
| } else if (provenance) { | ||
| report.reportInfo(null, `Generating provenance statement because \`npmPublishProvenance\` setting is set.`); | ||
| } | ||
| } | ||
|
|
||
| private async performPackAndPublish(workspace: any, registry: string, configuration: any, ident: any, report: any) { | ||
| const files = await packUtils.genPackList(workspace); | ||
|
|
||
| // Report files if this is a streaming report | ||
| if (report.reportInfo && typeof report.reportInfo === `function`) { | ||
| for (const file of files) { | ||
| report.reportInfo(null, file); | ||
| } | ||
| } | ||
|
|
||
| const pack = await packUtils.genPackStream(workspace, files); | ||
| const buffer = await miscUtils.bufferStream(pack); | ||
| const gitHead = await npmPublishUtils.getGitHead(workspace.cwd); | ||
| const provenance = this.determineProvenance(workspace, configuration); | ||
|
|
||
| // Report provenance decision if this is a streaming report | ||
| if (report.reportInfo && typeof report.reportInfo === `function`) | ||
Saadnajmi marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| this.reportProvenanceDecision(provenance, workspace, report); | ||
|
|
||
| const body = await npmPublishUtils.makePublishBody(workspace, buffer, { | ||
| access: this.access, | ||
| tag: this.tag, | ||
| registry, | ||
| gitHead, | ||
| provenance, | ||
| }); | ||
|
|
||
| return report.exitCode(); | ||
| if (!this.dryRun) { | ||
| await npmHttpUtils.put(npmHttpUtils.getIdentUrl(ident), body, { | ||
| configuration, | ||
| registry, | ||
| ident, | ||
| otp: this.otp, | ||
| jsonResponse: true, | ||
| }); | ||
| } else if (report.reportInfo && typeof report.reportInfo === `function`) { | ||
| report.reportInfo(MessageName.UNNAMED, `[DRY RUN] Package would be published to ${registry}`); | ||
| } | ||
|
|
||
| // Return data for JSON output | ||
| return { | ||
| files, | ||
| gitHead, | ||
| provenance, | ||
| published: !this.dryRun, | ||
| }; | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.