diff --git a/lib/commands/trust/circleci.js b/lib/commands/trust/circleci.js index 34d25b8018268..1d4db3831f74a 100644 --- a/lib/commands/trust/circleci.js +++ b/lib/commands/trust/circleci.js @@ -1,6 +1,7 @@ const Definition = require('@npmcli/config/lib/definitions/definition.js') const globalDefinitions = require('@npmcli/config/lib/definitions/definitions.js') const TrustCommand = require('../../trust-cmd.js') +const { trustDefinitions } = require('../../trust-cmd.js') // UUID validation regex const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i @@ -46,6 +47,8 @@ class TrustCircleCI extends TrustCommand { type: [null, String, Array], description: 'CircleCI context UUID to match', }), + trustDefinitions['allow-publish'], + trustDefinitions['allow-stage-publish'], // globals are alphabetical globalDefinitions['dry-run'], globalDefinitions.json, diff --git a/lib/commands/trust/github.js b/lib/commands/trust/github.js index 870314b717a75..9bdbb43074355 100644 --- a/lib/commands/trust/github.js +++ b/lib/commands/trust/github.js @@ -1,6 +1,7 @@ const Definition = require('@npmcli/config/lib/definitions/definition.js') const globalDefinitions = require('@npmcli/config/lib/definitions/definitions.js') const TrustCommand = require('../../trust-cmd.js') +const { trustDefinitions } = require('../../trust-cmd.js') const path = require('node:path') class TrustGitHub extends TrustCommand { @@ -38,6 +39,8 @@ class TrustGitHub extends TrustCommand { description: 'CI environment name', alias: ['env'], }), + trustDefinitions['allow-publish'], + trustDefinitions['allow-stage-publish'], // globals are alphabetical globalDefinitions['dry-run'], globalDefinitions.json, diff --git a/lib/commands/trust/gitlab.js b/lib/commands/trust/gitlab.js index e6456244ea185..63cc468864d65 100644 --- a/lib/commands/trust/gitlab.js +++ b/lib/commands/trust/gitlab.js @@ -1,6 +1,7 @@ const Definition = require('@npmcli/config/lib/definitions/definition.js') const globalDefinitions = require('@npmcli/config/lib/definitions/definitions.js') const TrustCommand = require('../../trust-cmd.js') +const { trustDefinitions } = require('../../trust-cmd.js') const path = require('node:path') class TrustGitLab extends TrustCommand { @@ -37,6 +38,8 @@ class TrustGitLab extends TrustCommand { description: 'CI environment name', alias: ['env'], }), + trustDefinitions['allow-publish'], + trustDefinitions['allow-stage-publish'], // globals are alphabetical globalDefinitions['dry-run'], globalDefinitions.json, diff --git a/lib/trust-cmd.js b/lib/trust-cmd.js index 5fab8df1d21aa..9102ba25308ad 100644 --- a/lib/trust-cmd.js +++ b/lib/trust-cmd.js @@ -6,9 +6,29 @@ const { read: _read } = require('read') const { input, output, log, META } = require('proc-log') const gitinfo = require('hosted-git-info') const pkgJson = require('@npmcli/package-json') +const Definition = require('@npmcli/config/lib/definitions/definition.js') const NPM_FRONTEND = 'https://www.npmjs.com' +const PERMISSIONS = { + CREATE_PACKAGE: 'createPackage', + CREATE_STAGED_PACKAGE: 'createStagedPackage', +} + +const trustDefinitions = { + 'allow-publish': new Definition('allow-publish', { + default: false, + type: Boolean, + description: 'Allow npm publish for this trusted publisher configuration', + }), + 'allow-stage-publish': new Definition('allow-stage-publish', { + default: false, + type: Boolean, + description: 'Allow npm stage publish for this trusted publisher configuration', + alias: ['allow-staged-publish'], + }), +} + class TrustCommand extends BaseCommand { // Helper to format template strings with color // Blue text with reset color for interpolated values @@ -45,8 +65,22 @@ class TrustCommand extends BaseCommand { })) } + static permissionLabels = { + [PERMISSIONS.CREATE_PACKAGE]: 'publish', + [PERMISSIONS.CREATE_STAGED_PACKAGE]: 'stage publish', + } + + static formatPermissions (permissions) { + if (!Array.isArray(permissions) || permissions.length === 0) { + return null + } + return permissions + .map(p => TrustCommand.permissionLabels[p] || p) + .join(', ') + } + logOptions (options, pad = true) { - const { values, warnings, fromPackageJson, urls } = { warnings: [], ...options } + const { values, warnings, fromPackageJson, urls, permissions } = { warnings: [], ...options } if (warnings && warnings.length > 0) { for (const warningMsg of warnings) { log.warn('trust', warningMsg) @@ -55,8 +89,12 @@ class TrustCommand extends BaseCommand { const json = this.config.get('json') if (json) { + const jsonValues = { ...options.values } + if (permissions) { + jsonValues.permissions = permissions + } // Disable redaction: trust config values (e.g. CircleCI UUIDs) are not secrets - output.standard(JSON.stringify(options.values, null, 2), { [META]: true, redact: false }) + output.standard(JSON.stringify(jsonValues, null, 2), { [META]: true, redact: false }) return } @@ -82,6 +120,10 @@ class TrustCommand extends BaseCommand { lines.push(parts.join(' ')) } } + const formattedPermissions = TrustCommand.formatPermissions(permissions) + if (formattedPermissions) { + lines.push(`${chalk.reset('permissions')}: ${chalk.green(formattedPermissions)}`) + } if (pad) { output.standard() } @@ -165,6 +207,22 @@ class TrustCommand extends BaseCommand { const { providerName, providerEntity, providerHostname } = this.constructor const dryRun = this.config.get('dry-run') const yes = this.config.get('yes') // deep-lore this allows for --no-yes + + const allowPublish = flags['allow-publish'] + const allowStagePublish = flags['allow-stage-publish'] || flags['allow-staged-publish'] + + if (!allowPublish && !allowStagePublish) { + throw new Error('Trust Relationships require permission access to run specific commands such as `npm stage` and `npm publish` please provide `--allow-stage-publish` or `--allow-publish` to proceed.') + } + + const permissions = [] + if (allowPublish) { + permissions.push(PERMISSIONS.CREATE_PACKAGE) + } + if (allowStagePublish) { + permissions.push(PERMISSIONS.CREATE_STAGED_PACKAGE) + } + const options = await this.flagsToOptions({ positionalArgs, flags, providerHostname }) this.dialogue`Establishing trust between ${options.values.package} package and ${providerName}` this.dialogue`Anyone with ${providerEntity} write access can publish to ${options.values.package}` @@ -172,12 +230,13 @@ class TrustCommand extends BaseCommand { if (!this.registryIsDefault) { this.warn`Registry ${this.npm.config.get('registry')} may not support trusted publishing` } - this.logOptions(options) + this.logOptions({ ...options, permissions }) if (dryRun) { return } await this.confirmOperation(yes) const trustConfig = this.constructor.optionsToBody(options.values) + trustConfig.permissions = permissions const response = await this.createConfig(options.values.package, [trustConfig]) const body = await response.json() this.dialogue`Trust configuration created successfully for ${options.values.package} with the following settings:` @@ -273,8 +332,9 @@ class TrustCommand extends BaseCommand { const items = Array.isArray(body) ? body : [body] for (const config of items) { const values = this.constructor.bodyToOptions(config) + const permissions = config.permissions output.standard() - this.logOptions({ values }, false) + this.logOptions({ values, permissions }, false) } output.standard() } @@ -282,3 +342,5 @@ class TrustCommand extends BaseCommand { module.exports = TrustCommand module.exports.NPM_FRONTEND = NPM_FRONTEND +module.exports.trustDefinitions = trustDefinitions +module.exports.PERMISSIONS = PERMISSIONS diff --git a/tap-snapshots/test/lib/commands/completion.js.test.cjs b/tap-snapshots/test/lib/commands/completion.js.test.cjs index 43a99252d4200..a5f0f6748f8d7 100644 --- a/tap-snapshots/test/lib/commands/completion.js.test.cjs +++ b/tap-snapshots/test/lib/commands/completion.js.test.cjs @@ -105,10 +105,16 @@ Array [ --repo --environment --env + --allow-publish + --allow-stage-publish + --allow-staged-publish --dry-run --json --registry --yes + --no-allow-publish + --no-allow-stage-publish + --no-allow-staged-publish --no-dry-run --no-json --no-yes @@ -123,10 +129,16 @@ Array [ --project --environment --env + --allow-publish + --allow-stage-publish + --allow-staged-publish --dry-run --json --registry --yes + --no-allow-publish + --no-allow-stage-publish + --no-allow-staged-publish --no-dry-run --no-json --no-yes diff --git a/test/lib/commands/trust/circleci.js b/test/lib/commands/trust/circleci.js index 1ceec9a6e5845..51211785c5748 100644 --- a/test/lib/commands/trust/circleci.js +++ b/test/lib/commands/trust/circleci.js @@ -44,6 +44,7 @@ t.test('circleci with all options provided', async t => { '--pipeline-definition-id', '6ba7b810-9dad-11d1-80b4-00c04fd430c8', '--vcs-origin', 'github.com/owner/repo', '--context-id', '123e4567-e89b-12d3-a456-426614174000', + '--allow-publish', ]) }) @@ -85,6 +86,7 @@ t.test('circleci without optional context-id', async t => { '--project-id', '7c9e6679-7425-40de-944b-e07fc1f90ae7', '--pipeline-definition-id', '6ba7b810-9dad-11d1-80b4-00c04fd430c8', '--vcs-origin', 'github.com/owner/repo', + '--allow-publish', ]) }) @@ -128,6 +130,7 @@ t.test('circleci with multiple context-ids', async t => { '--vcs-origin', 'github.com/owner/repo', '--context-id', '123e4567-e89b-12d3-a456-426614174000', '--context-id', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', + '--allow-publish', ]) }) @@ -152,6 +155,7 @@ t.test('circleci missing required org-id', async t => { '--project-id', '7c9e6679-7425-40de-944b-e07fc1f90ae7', '--pipeline-definition-id', '6ba7b810-9dad-11d1-80b4-00c04fd430c8', '--vcs-origin', 'github.com/owner/repo', + '--allow-publish', ]), { message: /org-id is required/ } ) @@ -178,6 +182,7 @@ t.test('circleci missing required project-id', async t => { '--org-id', '550e8400-e29b-41d4-a716-446655440000', '--pipeline-definition-id', '6ba7b810-9dad-11d1-80b4-00c04fd430c8', '--vcs-origin', 'github.com/owner/repo', + '--allow-publish', ]), { message: /project-id is required/ } ) @@ -204,6 +209,7 @@ t.test('circleci missing required pipeline-definition-id', async t => { '--org-id', '550e8400-e29b-41d4-a716-446655440000', '--project-id', '7c9e6679-7425-40de-944b-e07fc1f90ae7', '--vcs-origin', 'github.com/owner/repo', + '--allow-publish', ]), { message: /pipeline-definition-id is required/ } ) @@ -230,6 +236,7 @@ t.test('circleci missing required vcs-origin', async t => { '--org-id', '550e8400-e29b-41d4-a716-446655440000', '--project-id', '7c9e6679-7425-40de-944b-e07fc1f90ae7', '--pipeline-definition-id', '6ba7b810-9dad-11d1-80b4-00c04fd430c8', + '--allow-publish', ]), { message: /vcs-origin is required/ } ) @@ -257,6 +264,7 @@ t.test('circleci with invalid org-id uuid format', async t => { '--project-id', '7c9e6679-7425-40de-944b-e07fc1f90ae7', '--pipeline-definition-id', '6ba7b810-9dad-11d1-80b4-00c04fd430c8', '--vcs-origin', 'github.com/owner/repo', + '--allow-publish', ]), { message: /org-id must be a valid UUID/ } ) @@ -284,6 +292,7 @@ t.test('circleci with invalid vcs-origin format', async t => { '--project-id', '7c9e6679-7425-40de-944b-e07fc1f90ae7', '--pipeline-definition-id', '6ba7b810-9dad-11d1-80b4-00c04fd430c8', '--vcs-origin', 'invalid-format', + '--allow-publish', ]), { message: /vcs-origin must be in format 'provider\/owner\/repo'/ } ) @@ -311,6 +320,7 @@ t.test('circleci with vcs-origin containing scheme prefix', async t => { '--project-id', '7c9e6679-7425-40de-944b-e07fc1f90ae7', '--pipeline-definition-id', '6ba7b810-9dad-11d1-80b4-00c04fd430c8', '--vcs-origin', 'https://github.com/owner/repo', + '--allow-publish', ]), { message: /vcs-origin must not include a scheme/ } ) @@ -336,6 +346,7 @@ t.test('circleci missing package name', async t => { '--project-id', '7c9e6679-7425-40de-944b-e07fc1f90ae7', '--pipeline-definition-id', '6ba7b810-9dad-11d1-80b4-00c04fd430c8', '--vcs-origin', 'github.com/owner/repo', + '--allow-publish', ]), { message: /Package name must be specified either as an argument or in package.json file/ } ) diff --git a/test/lib/commands/trust/github.js b/test/lib/commands/trust/github.js index a2b16d272bde1..6cc65bff354d0 100644 --- a/test/lib/commands/trust/github.js +++ b/test/lib/commands/trust/github.js @@ -35,7 +35,7 @@ t.test('github with all options provided', async t => { registry.trustCreate({ packageName }) - await npm.exec('trust', ['github', packageName, '--yes', '--file', 'workflow.yml', '--repository', 'owner/repo', '--environment', 'production']) + await npm.exec('trust', ['github', packageName, '--yes', '--file', 'workflow.yml', '--repository', 'owner/repo', '--environment', 'production', '--allow-publish']) }) t.test('github with invalid repository format', async t => { @@ -61,7 +61,7 @@ t.test('github with invalid repository format', async t => { }) await t.rejects( - npm.exec('trust', ['github', packageName, '--yes', '--file', 'workflow.yml', '--repository', 'invalid']), + npm.exec('trust', ['github', packageName, '--yes', '--file', 'workflow.yml', '--repository', 'invalid', '--allow-publish']), { message: /must be specified in the format owner\/repository/ } ) }) @@ -89,7 +89,7 @@ t.test('github with file as path', async t => { }) await t.rejects( - npm.exec('trust', ['github', packageName, '--yes', '--file', '.github/workflows/ci.yml', '--repository', 'owner/repo']), + npm.exec('trust', ['github', packageName, '--yes', '--file', '.github/workflows/ci.yml', '--repository', 'owner/repo', '--allow-publish']), { message: /must be just a file not a path/ } ) }) @@ -124,7 +124,7 @@ t.test('github without environment', async t => { registry.trustCreate({ packageName }) - await npm.exec('trust', ['github', packageName, '--yes', '--file', 'workflow.yml', '--repository', 'owner/repo']) + await npm.exec('trust', ['github', packageName, '--yes', '--file', 'workflow.yml', '--repository', 'owner/repo', '--allow-publish']) }) t.test('bodyToOptions with all fields', t => { diff --git a/test/lib/commands/trust/gitlab.js b/test/lib/commands/trust/gitlab.js index 0b60196830c5f..16f3804f97896 100644 --- a/test/lib/commands/trust/gitlab.js +++ b/test/lib/commands/trust/gitlab.js @@ -35,7 +35,7 @@ t.test('gitlab with all options provided', async t => { registry.trustCreate({ packageName }) - await npm.exec('trust', ['gitlab', packageName, '--yes', '--file', '.gitlab-ci.yml', '--project', 'group/subgroup/repo', '--environment', 'production']) + await npm.exec('trust', ['gitlab', packageName, '--yes', '--file', '.gitlab-ci.yml', '--project', 'group/subgroup/repo', '--environment', 'production', '--allow-publish']) }) t.test('gitlab with invalid project format', async t => { @@ -61,7 +61,7 @@ t.test('gitlab with invalid project format', async t => { }) await t.rejects( - npm.exec('trust', ['gitlab', packageName, '--yes', '--file', '.gitlab-ci.yml', '--project', 'invalid']), + npm.exec('trust', ['gitlab', packageName, '--yes', '--file', '.gitlab-ci.yml', '--project', 'invalid', '--allow-publish']), { message: /must be specified in the format/ } ) }) @@ -89,7 +89,7 @@ t.test('gitlab with file as path', async t => { }) await t.rejects( - npm.exec('trust', ['gitlab', packageName, '--yes', '--file', '.gitlab/ci.yml', '--project', 'group/repo']), + npm.exec('trust', ['gitlab', packageName, '--yes', '--file', '.gitlab/ci.yml', '--project', 'group/repo', '--allow-publish']), { message: /must be just a file not a path/ } ) }) @@ -124,7 +124,7 @@ t.test('gitlab without environment', async t => { registry.trustCreate({ packageName }) - await npm.exec('trust', ['gitlab', packageName, '--yes', '--file', '.gitlab-ci.yml', '--project', 'group/repo']) + await npm.exec('trust', ['gitlab', packageName, '--yes', '--file', '.gitlab-ci.yml', '--project', 'group/repo', '--allow-publish']) }) t.test('bodyToOptions with all fields', t => { diff --git a/test/lib/trust-cmd.js b/test/lib/trust-cmd.js index f0c52aadbd2c4..40a5b5211ddc4 100644 --- a/test/lib/trust-cmd.js +++ b/test/lib/trust-cmd.js @@ -32,7 +32,7 @@ t.test('trust-cmd via trust github with read function called', async t => { registry.trustCreate({ packageName }) - await npm.exec('trust', ['github', packageName, '--file', 'workflow.yml', '--repository', 'npm/cli']) + await npm.exec('trust', ['github', '--allow-publish', packageName, '--file', 'workflow.yml', '--repository', 'npm/cli']) }) t.test('trust-cmd via trust github with all options', async t => { @@ -57,7 +57,77 @@ t.test('trust-cmd via trust github with all options', async t => { registry.trustCreate({ packageName }) - await npm.exec('trust', ['github', packageName, '--file', 'workflow.yml', '--repository', 'npm/cli', '--environment', 'production']) + await npm.exec('trust', ['github', '--allow-publish', packageName, '--file', 'workflow.yml', '--repository', 'npm/cli', '--environment', 'production']) +}) + +t.test('trust-cmd via trust github with --allow-stage-publish', async t => { + const { npm } = await loadMockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ + name: packageName, + version: '1.0.0', + }), + }, + config: { + '//registry.npmjs.org/:_authToken': 'test-auth-token', + yes: true, + }, + }) + + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + authorization: 'test-auth-token', + }) + + registry.trustCreate({ packageName }) + + await npm.exec('trust', ['github', '--allow-stage-publish', packageName, '--file', 'workflow.yml', '--repository', 'npm/cli']) +}) + +t.test('trust-cmd via trust github with --allow-staged-publish alias', async t => { + const { npm } = await loadMockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ + name: packageName, + version: '1.0.0', + }), + }, + config: { + '//registry.npmjs.org/:_authToken': 'test-auth-token', + yes: true, + }, + }) + + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + authorization: 'test-auth-token', + }) + + registry.trustCreate({ packageName }) + + await npm.exec('trust', ['github', '--allow-staged-publish', packageName, '--file', 'workflow.yml', '--repository', 'npm/cli']) +}) + +t.test('trust-cmd via trust github missing permissions', async t => { + const { npm } = await loadMockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ + name: packageName, + version: '1.0.0', + }), + }, + config: { + '//registry.npmjs.org/:_authToken': 'test-auth-token', + yes: true, + }, + }) + + await t.rejects( + npm.exec('trust', ['github', packageName, '--file', 'workflow.yml', '--repository', 'npm/cli']), + { message: /--allow-stage-publish.*--allow-publish/ } + ) }) t.test('trust-cmd via trust github infers from package.json', async t => { @@ -91,7 +161,7 @@ t.test('trust-cmd via trust github infers from package.json', async t => { registry.trustCreate({ packageName }) - await npm.exec('trust', ['github', '--yes', '--file', 'workflow.yml']) + await npm.exec('trust', ['github', '--allow-publish', '--yes', '--file', 'workflow.yml']) }) t.test('trust-cmd via trust github with dry-run', async t => { @@ -108,7 +178,7 @@ t.test('trust-cmd via trust github with dry-run', async t => { }, }) - await npm.exec('trust', ['github', packageName, '--file', 'workflow.yml', '--repository', 'npm/cli']) + await npm.exec('trust', ['github', '--allow-publish', packageName, '--file', 'workflow.yml', '--repository', 'npm/cli']) t.ok(joinedOutput().includes('Establishing trust'), 'shows notice about establishing trust') }) @@ -122,7 +192,7 @@ t.test('trust-cmd via trust github missing package name', async t => { }) await t.rejects( - npm.exec('trust', ['github', '--file', 'workflow.yml', '--repository', 'npm/cli']), + npm.exec('trust', ['github', '--allow-publish', '--file', 'workflow.yml', '--repository', 'npm/cli']), { message: /Package name must be specified/ }, 'throws when no package name' ) @@ -141,7 +211,7 @@ t.test('trust-cmd via trust github missing file', async t => { }) await t.rejects( - npm.exec('trust', ['github', packageName, '--repository', 'npm/cli']), + npm.exec('trust', ['github', '--allow-publish', packageName, '--repository', 'npm/cli']), { message: /must be specified with the file option/ }, 'throws when no file' ) @@ -160,7 +230,7 @@ t.test('trust-cmd via trust github invalid file extension', async t => { }) await t.rejects( - npm.exec('trust', ['github', packageName, '--file', 'workflow.txt', '--repository', 'npm/cli']), + npm.exec('trust', ['github', '--allow-publish', packageName, '--file', 'workflow.txt', '--repository', 'npm/cli']), { message: /must end in \.yml or \.yaml/ }, 'throws when file has wrong extension' ) @@ -179,7 +249,7 @@ t.test('trust-cmd via trust github missing repository', async t => { }) await t.rejects( - npm.exec('trust', ['github', packageName, '--file', 'workflow.yml']), + npm.exec('trust', ['github', '--allow-publish', packageName, '--file', 'workflow.yml']), { message: /must be specified with repository option/ }, 'throws when no repository' ) @@ -200,7 +270,7 @@ t.test('trust-cmd via trust github with custom registry warning', async t => { }, }) - await npm.exec('trust', ['github', packageName, '--file', 'workflow.yml', '--repository', 'npm/cli']) + await npm.exec('trust', ['github', '--allow-publish', packageName, '--file', 'workflow.yml', '--repository', 'npm/cli']) t.ok(logs.warn.some(l => l.includes('may not support trusted publishing')), 'warns about custom registry') }) @@ -220,7 +290,7 @@ t.test('trust-cmd via trust github with --json', async t => { }, }) - await npm.exec('trust', ['github', packageName, '--file', 'workflow.yml', '--repository', 'npm/cli']) + await npm.exec('trust', ['github', '--allow-publish', packageName, '--file', 'workflow.yml', '--repository', 'npm/cli']) const output = joinedOutput() t.ok(output.includes(packageName), 'JSON output includes package name') @@ -250,7 +320,7 @@ t.test('trust-cmd via trust github with user confirmation no', async t => { }) await t.rejects( - npm.exec('trust', ['github', packageName, '--file', 'workflow.yml', '--repository', 'npm/cli']), + npm.exec('trust', ['github', '--allow-publish', packageName, '--file', 'workflow.yml', '--repository', 'npm/cli']), { message: 'User cancelled operation' }, 'throws when user declines' ) @@ -271,7 +341,7 @@ t.test('trust-cmd via trust github with --no-yes', async t => { }) await t.rejects( - npm.exec('trust', ['github', packageName, '--file', 'workflow.yml', '--repository', 'npm/cli']), + npm.exec('trust', ['github', '--allow-publish', packageName, '--file', 'workflow.yml', '--repository', 'npm/cli']), { message: 'User cancelled operation' }, 'throws when --no-yes flag is set' ) @@ -300,7 +370,7 @@ t.test('trust-cmd via trust github with invalid answer', async t => { }) await t.rejects( - npm.exec('trust', ['github', packageName, '--file', 'workflow.yml', '--repository', 'npm/cli']), + npm.exec('trust', ['github', '--allow-publish', packageName, '--file', 'workflow.yml', '--repository', 'npm/cli']), { message: 'User cancelled operation' }, 'throws when user gives invalid answer' ) @@ -336,7 +406,7 @@ t.test('trust-cmd via trust github with user confirmation Y uppercase', async t registry.trustCreate({ packageName }) - await npm.exec('trust', ['github', packageName, '--file', 'workflow.yml', '--repository', 'npm/cli']) + await npm.exec('trust', ['github', '--allow-publish', packageName, '--file', 'workflow.yml', '--repository', 'npm/cli']) }) t.test('trust-cmd via trust github with user enters empty string', async t => { @@ -362,7 +432,7 @@ t.test('trust-cmd via trust github with user enters empty string', async t => { }) await t.rejects( - npm.exec('trust', ['github', packageName, '--file', 'workflow.yml', '--repository', 'npm/cli']), + npm.exec('trust', ['github', '--allow-publish', packageName, '--file', 'workflow.yml', '--repository', 'npm/cli']), { message: 'User cancelled operation' }, 'throws when user enters empty string' ) @@ -383,7 +453,7 @@ t.test('trust-cmd via trust github with mismatched repo type', async t => { }) await t.rejects( - npm.exec('trust', ['github', '--file', 'workflow.yml']), + npm.exec('trust', ['github', '--allow-publish', '--file', 'workflow.yml']), { message: /Repository in package.json is not a GitHub repository/ }, 'throws when repository type does not match provider' ) @@ -404,7 +474,7 @@ t.test('trust-cmd via trust github with mismatched repo type but flag provided', }, }) - await npm.exec('trust', ['github', '--file', 'workflow.yml', '--repository', 'owner/new-repo']) + await npm.exec('trust', ['github', '--allow-publish', '--file', 'workflow.yml', '--repository', 'owner/new-repo']) t.ok(logs.warn.some(l => l.includes('Repository in package.json is not a GitHub repository')), 'warns about repository type mismatch') }) @@ -424,7 +494,7 @@ t.test('trust-cmd via trust github with different repo in package.json', async t }, }) - await npm.exec('trust', ['github', '--file', 'workflow.yml', '--repository', 'owner/new-repo']) + await npm.exec('trust', ['github', '--allow-publish', '--file', 'workflow.yml', '--repository', 'owner/new-repo']) t.ok(logs.warn.some(l => l.includes('differs from provided')), 'warns about repository mismatch') }) @@ -459,7 +529,7 @@ t.test('trust-cmd via trust github with user confirmation yes spelled out', asyn registry.trustCreate({ packageName }) - await npm.exec('trust', ['github', packageName, '--file', 'workflow.yml', '--repository', 'npm/cli']) + await npm.exec('trust', ['github', '--allow-publish', packageName, '--file', 'workflow.yml', '--repository', 'npm/cli']) }) t.test('trust-cmd via trust github showing response with id and type', async t => { @@ -500,7 +570,7 @@ t.test('trust-cmd via trust github showing response with id and type', async t = }, }) - await npm.exec('trust', ['github', packageName, '--file', 'workflow.yml', '--repository', 'npm/cli']) + await npm.exec('trust', ['github', '--allow-publish', packageName, '--file', 'workflow.yml', '--repository', 'npm/cli']) const output = joinedOutput() t.ok(output.includes('type:'), 'output shows type field') @@ -520,7 +590,7 @@ t.test('trust-cmd via trust github missing repository when package name differs' }) await t.rejects( - npm.exec('trust', ['github', packageName, '--file', 'workflow.yml']), + npm.exec('trust', ['github', '--allow-publish', packageName, '--file', 'workflow.yml']), { message: /must be specified with repository option/ }, 'throws when no repository and package name differs' ) @@ -619,7 +689,7 @@ t.test('trust-cmd via trust github showing fromPackageJson indicator', async t = }, }) - await npm.exec('trust', ['github', packageName, '--file', 'workflow.yml']) + await npm.exec('trust', ['github', '--allow-publish', packageName, '--file', 'workflow.yml']) const output = joinedOutput() t.ok(output.includes('from package.json'), 'output shows fromPackageJson indicator') @@ -663,7 +733,7 @@ t.test('trust-cmd via trust github showing URLs for fields', async t => { }, }) - await npm.exec('trust', ['github', packageName, '--file', 'workflow.yml', '--repository', 'npm/cli']) + await npm.exec('trust', ['github', '--allow-publish', packageName, '--file', 'workflow.yml', '--repository', 'npm/cli']) const output = joinedOutput() t.match(output, /https:\/\/github\.com\/npm\/cli\b/, 'output shows repository URL') @@ -684,7 +754,7 @@ t.test('trust-cmd via trust github with yes=false flag', async t => { }) await t.rejects( - npm.exec('trust', ['github', packageName, '--file', 'workflow.yml', '--repository', 'npm/cli']), + npm.exec('trust', ['github', '--allow-publish', packageName, '--file', 'workflow.yml', '--repository', 'npm/cli']), { message: /User cancelled operation/ }, 'throws when yes is explicitly false' ) @@ -846,3 +916,28 @@ t.test('TrustCommand - logOptions with urls but all values are null', async t => t.ok(output.includes('file'), 'shows file field') t.notOk(output.includes('Links to verify manually'), 'does not show links header when all urls are null') }) + +t.test('formatPermissions with unknown permission falls back to raw value', t => { + const result = TrustCommand.formatPermissions(['unknownPermission']) + t.equal(result, 'unknownPermission', 'returns raw value for unknown permission') + t.end() +}) + +t.test('displayResponseBody with empty body', async t => { + const { npm, joinedOutput } = await loadMockNpm(t, { + config: { + '//registry.npmjs.org/:_authToken': 'test-auth-token', + }, + }) + + class TestTrustCmd extends TrustCommand { + static name = 'test' + static description = 'Test command' + } + + const cmd = new TestTrustCmd(npm) + cmd.displayResponseBody({ body: [], packageName: '@npmcli/test-package' }) + + const output = joinedOutput() + t.match(output, /No trust configurations found/, 'shows no configurations message') +})