codekingpro/portable-devtools
114k
1const Definition = require('@npmcli/config/lib/definitions/definition.js')
2const globalDefinitions = require('@npmcli/config/lib/definitions/definitions.js')
3const TrustCommand = require('../../trust-cmd.js')
4
5// UUID validation regex
6const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
7
8class 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> --project-id <uuid> --pipeline-definition-id <uuid> --vcs-origin <origin> [--context-id <uuid>...] [-y|--yes]',
17 ]
18
19 static definitions = [
20 new Definition('org-id', {
21 default: null,
22 type: String,
23 required: true,
24 description: 'CircleCI organization UUID',
25 }),
26 new Definition('project-id', {
27 default: null,
28 type: String,
29 required: true,
30 description: 'CircleCI project UUID',
31 }),
32 new Definition('pipeline-definition-id', {
33 default: null,
34 type: String,
35 required: true,
36 description: 'CircleCI pipeline definition UUID',
37 }),
38 new Definition('vcs-origin', {
39 default: null,
40 type: String,
41 required: true,
42 description: "CircleCI repository origin in format 'provider/owner/repo'",
43 }),
44 new Definition('context-id', {
45 default: null,
46 type: [null, String, Array],
47 description: 'CircleCI context UUID to match',
48 }),
49 // globals are alphabetical
50 globalDefinitions['dry-run'],
51 globalDefinitions.json,
52 globalDefinitions.registry,
53 globalDefinitions.yes,
54 ]
55
56 validateUuid (value, fieldName) {
57 if (!UUID_REGEX.test(value)) {
58 throw new Error(`${fieldName} must be a valid UUID`)
59 }
60 }
61
62 validateVcsOrigin (value) {
63 // Expected format: provider/owner/repo (e.g., github.com/owner/repo, bitbucket.org/owner/repo)
64 if (value.includes('://')) {
65 throw new Error("vcs-origin must not include a scheme (e.g., use 'github.com/owner/repo' not 'https://github.com/owner/repo')")
66 }
67 const parts = value.split('/')
68 if (parts.length < 3) {
69 throw new Error("vcs-origin must be in format 'provider/owner/repo'")
70 }
71 }
72
73 // Generate a URL from vcs-origin (e.g., github.com/npm/repo -> https://github.com/npm/repo)
74 getVcsOriginUrl (vcsOrigin) {
75 if (!vcsOrigin) {
76 return null
77 }
78 // vcs-origin format: github.com/owner/repo or bitbucket.org/owner/repo
79 return `https://${vcsOrigin}`
80 }
81
82 static optionsToBody (options) {
83 const { orgId, projectId, pipelineDefinitionId, vcsOrigin, contextIds } = options
84 const trustConfig = {
85 type: 'circleci',
86 claims: {
87 'oidc.circleci.com/org-id': orgId,
88 'oidc.circleci.com/project-id': projectId,
89 'oidc.circleci.com/pipeline-definition-id': pipelineDefinitionId,
90 'oidc.circleci.com/vcs-origin': vcsOrigin,
91 },
92 }
93 if (contextIds && contextIds.length > 0) {
94 trustConfig.claims['oidc.circleci.com/context-ids'] = contextIds
95 }
96 return trustConfig
97 }
98
99 static bodyToOptions (body) {
100 return {
101 ...(body.id) && { id: body.id },
102 ...(body.type) && { type: body.type },
103 ...(body.claims?.['oidc.circleci.com/org-id']) && { orgId: body.claims['oidc.circleci.com/org-id'] },
104 ...(body.claims?.['oidc.circleci.com/project-id']) && { projectId: body.claims['oidc.circleci.com/project-id'] },
105 ...(body.claims?.['oidc.circleci.com/pipeline-definition-id']) && {
106 pipelineDefinitionId: body.claims['oidc.circleci.com/pipeline-definition-id'],
107 },
108 ...(body.claims?.['oidc.circleci.com/vcs-origin']) && { vcsOrigin: body.claims['oidc.circleci.com/vcs-origin'] },
109 ...(body.claims?.['oidc.circleci.com/context-ids']) && { contextIds: body.claims['oidc.circleci.com/context-ids'] },
110 }
111 }
112
113 // Override flagsToOptions since CircleCI doesn't use file/entity pattern
114 async flagsToOptions ({ positionalArgs, flags }) {
115 const content = await this.optionalPkgJson()
116 const pkgName = positionalArgs[0] || content.name
117
118 if (!pkgName) {
119 throw new Error('Package name must be specified either as an argument or in package.json file')
120 }
121
122 const orgId = flags['org-id']
123 const projectId = flags['project-id']
124 const pipelineDefinitionId = flags['pipeline-definition-id']
125 const vcsOrigin = flags['vcs-origin']
126 const contextIds = flags['context-id']
127
128 // Validate required flags
129 if (!orgId) {
130 throw new Error('org-id is required')
131 }
132 if (!projectId) {
133 throw new Error('project-id is required')
134 }
135 if (!pipelineDefinitionId) {
136 throw new Error('pipeline-definition-id is required')
137 }
138 if (!vcsOrigin) {
139 throw new Error('vcs-origin is required')
140 }
141
142 // Validate formats
143 this.validateUuid(orgId, 'org-id')
144 this.validateUuid(projectId, 'project-id')
145 this.validateUuid(pipelineDefinitionId, 'pipeline-definition-id')
146 this.validateVcsOrigin(vcsOrigin)
147 if (contextIds?.length > 0) {
148 for (const contextId of contextIds) {
149 this.validateUuid(contextId, 'context-id')
150 }
151 }
152
153 return {
154 values: {
155 package: pkgName,
156 orgId,
157 projectId,
158 pipelineDefinitionId,
159 vcsOrigin,
160 ...(contextIds?.length > 0 && { contextIds }),
161 },
162 fromPackageJson: {},
163 warnings: [],
164 urls: {
165 package: this.getFrontendUrl({ pkgName }),
166 vcsOrigin: this.getVcsOriginUrl(vcsOrigin),
167 },
168 }
169 }
170
171 async exec (positionalArgs, flags) {
172 await this.createConfigCommand({
173 positionalArgs,
174 flags,
175 })
176 }
177}
178
179module.exports = TrustCircleCI
180 