|
| 1 | +const Definition = require('@npmcli/config/lib/definitions/definition.js') |
| 2 | +const globalDefinitions = require('@npmcli/config/lib/definitions/definitions.js') |
| 3 | +const TrustCommand = require('../../trust-cmd.js') |
| 4 | + |
| 5 | +// UUID validation regex |
| 6 | +const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i |
| 7 | + |
| 8 | +class TrustCircleCI extends TrustCommand { |
| 9 | + static description = 'Create a trusted relationship between a package and CircleCI' |
| 10 | + static name = 'circleci' |
| 11 | + static positionals = 1 // expects at most 1 positional (package name) |
| 12 | + static providerName = 'CircleCI' |
| 13 | + static providerEntity = 'CircleCI pipeline' |
| 14 | + |
| 15 | + static usage = [ |
| 16 | + '[package] --org-id <uuid> --pipeline-definition-id <uuid> --vcs-origin <origin> [--context-id <uuid>] [-y|--yes]', |
| 17 | + ] |
| 18 | + |
| 19 | + static definitions = { |
| 20 | + yes: globalDefinitions.yes, |
| 21 | + json: globalDefinitions.json, |
| 22 | + 'dry-run': globalDefinitions['dry-run'], |
| 23 | + 'org-id': new Definition('org-id', { |
| 24 | + default: null, |
| 25 | + type: String, |
| 26 | + description: 'CircleCI organization UUID', |
| 27 | + }), |
| 28 | + 'pipeline-definition-id': new Definition('pipeline-definition-id', { |
| 29 | + default: null, |
| 30 | + type: String, |
| 31 | + description: 'CircleCI pipeline definition UUID', |
| 32 | + }), |
| 33 | + 'vcs-origin': new Definition('vcs-origin', { |
| 34 | + default: null, |
| 35 | + type: String, |
| 36 | + description: "CircleCI repository origin in format 'provider/owner/repo'", |
| 37 | + }), |
| 38 | + 'context-id': new Definition('context-id', { |
| 39 | + default: null, |
| 40 | + type: String, |
| 41 | + description: 'CircleCI context UUID', |
| 42 | + }), |
| 43 | + } |
| 44 | + |
| 45 | + validateUuid (value, fieldName) { |
| 46 | + if (!UUID_REGEX.test(value)) { |
| 47 | + throw new Error(`${fieldName} must be a valid UUID`) |
| 48 | + } |
| 49 | + } |
| 50 | + |
| 51 | + validateVcsOrigin (value) { |
| 52 | + // Expected format: provider/owner/repo (e.g., github.com/owner/repo, bitbucket.org/owner/repo) |
| 53 | + const parts = value.split('/') |
| 54 | + if (parts.length < 3) { |
| 55 | + throw new Error("vcs-origin must be in format 'provider/owner/repo'") |
| 56 | + } |
| 57 | + } |
| 58 | + |
| 59 | + // Generate a URL from vcs-origin (e.g., github.com/npm/repo -> https://github.com/npm/repo) |
| 60 | + getVcsOriginUrl (vcsOrigin) { |
| 61 | + if (!vcsOrigin) { |
| 62 | + return null |
| 63 | + } |
| 64 | + // vcs-origin format: github.com/owner/repo or bitbucket.org/owner/repo |
| 65 | + return `https://${vcsOrigin}` |
| 66 | + } |
| 67 | + |
| 68 | + static optionsToBody (options) { |
| 69 | + const { orgId, pipelineDefinitionId, vcsOrigin, contextId } = options |
| 70 | + const trustConfig = { |
| 71 | + type: 'circleci', |
| 72 | + claims: { |
| 73 | + org_id: orgId, |
| 74 | + pipeline_definition_id: pipelineDefinitionId, |
| 75 | + vcs_origin: vcsOrigin, |
| 76 | + }, |
| 77 | + } |
| 78 | + if (contextId) { |
| 79 | + trustConfig.claims.context_id = contextId |
| 80 | + } |
| 81 | + return trustConfig |
| 82 | + } |
| 83 | + |
| 84 | + static bodyToOptions (body) { |
| 85 | + return { |
| 86 | + ...(body.id) && { id: body.id }, |
| 87 | + ...(body.type) && { type: body.type }, |
| 88 | + ...(body.claims?.org_id) && { orgId: body.claims.org_id }, |
| 89 | + ...(body.claims?.pipeline_definition_id) && { |
| 90 | + pipelineDefinitionId: body.claims.pipeline_definition_id, |
| 91 | + }, |
| 92 | + ...(body.claims?.vcs_origin) && { vcsOrigin: body.claims.vcs_origin }, |
| 93 | + ...(body.claims?.context_id) && { contextId: body.claims.context_id }, |
| 94 | + } |
| 95 | + } |
| 96 | + |
| 97 | + // Override flagsToOptions since CircleCI doesn't use file/entity pattern |
| 98 | + async flagsToOptions ({ positionalArgs, flags }) { |
| 99 | + const content = await this.optionalPkgJson() |
| 100 | + const pkgName = positionalArgs[0] || content.name |
| 101 | + |
| 102 | + if (!pkgName) { |
| 103 | + throw new Error('Package name must be specified either as an argument or in package.json file') |
| 104 | + } |
| 105 | + |
| 106 | + const orgId = flags['org-id'] |
| 107 | + const pipelineDefinitionId = flags['pipeline-definition-id'] |
| 108 | + const vcsOrigin = flags['vcs-origin'] |
| 109 | + const contextId = flags['context-id'] |
| 110 | + |
| 111 | + // Validate required flags |
| 112 | + if (!orgId) { |
| 113 | + throw new Error('org-id is required') |
| 114 | + } |
| 115 | + if (!pipelineDefinitionId) { |
| 116 | + throw new Error('pipeline-definition-id is required') |
| 117 | + } |
| 118 | + if (!vcsOrigin) { |
| 119 | + throw new Error('vcs-origin is required') |
| 120 | + } |
| 121 | + |
| 122 | + // Validate formats |
| 123 | + this.validateUuid(orgId, 'org-id') |
| 124 | + this.validateUuid(pipelineDefinitionId, 'pipeline-definition-id') |
| 125 | + this.validateVcsOrigin(vcsOrigin) |
| 126 | + if (contextId) { |
| 127 | + this.validateUuid(contextId, 'context-id') |
| 128 | + } |
| 129 | + |
| 130 | + return { |
| 131 | + values: { |
| 132 | + package: pkgName, |
| 133 | + orgId, |
| 134 | + pipelineDefinitionId, |
| 135 | + vcsOrigin, |
| 136 | + ...(contextId && { contextId }), |
| 137 | + }, |
| 138 | + fromPackageJson: {}, |
| 139 | + warnings: [], |
| 140 | + urls: { |
| 141 | + package: this.getFrontendUrl({ pkgName }), |
| 142 | + vcsOrigin: this.getVcsOriginUrl(vcsOrigin), |
| 143 | + }, |
| 144 | + } |
| 145 | + } |
| 146 | + |
| 147 | + async exec (positionalArgs, flags) { |
| 148 | + await this.createConfigCommand({ |
| 149 | + positionalArgs, |
| 150 | + flags, |
| 151 | + }) |
| 152 | + } |
| 153 | +} |
| 154 | + |
| 155 | +module.exports = TrustCircleCI |
0 commit comments