Skip to content

Commit cac0ac3

Browse files
committed
feat: adds circleci to trust command
1 parent 4cae12b commit cac0ac3

5 files changed

Lines changed: 634 additions & 0 deletions

File tree

lib/commands/trust/circleci.js

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
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> --project-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+
'project-id': new Definition('project-id', {
29+
default: null,
30+
type: String,
31+
description: 'CircleCI project UUID',
32+
}),
33+
'pipeline-definition-id': new Definition('pipeline-definition-id', {
34+
default: null,
35+
type: String,
36+
description: 'CircleCI pipeline definition UUID',
37+
}),
38+
'vcs-origin': new Definition('vcs-origin', {
39+
default: null,
40+
type: String,
41+
description: "CircleCI repository origin in format 'provider/owner/repo'",
42+
}),
43+
'context-id': new Definition('context-id', {
44+
default: null,
45+
type: [null, String, Array],
46+
description: 'CircleCI context UUID to match',
47+
}),
48+
}
49+
50+
validateUuid (value, fieldName) {
51+
if (!UUID_REGEX.test(value)) {
52+
throw new Error(`${fieldName} must be a valid UUID`)
53+
}
54+
}
55+
56+
validateVcsOrigin (value) {
57+
// Expected format: provider/owner/repo (e.g., github.com/owner/repo, bitbucket.org/owner/repo)
58+
const parts = value.split('/')
59+
if (parts.length < 3) {
60+
throw new Error("vcs-origin must be in format 'provider/owner/repo'")
61+
}
62+
}
63+
64+
// Generate a URL from vcs-origin (e.g., github.com/npm/repo -> https://github.com/npm/repo)
65+
getVcsOriginUrl (vcsOrigin) {
66+
if (!vcsOrigin) {
67+
return null
68+
}
69+
// vcs-origin format: github.com/owner/repo or bitbucket.org/owner/repo
70+
return `https://${vcsOrigin}`
71+
}
72+
73+
static optionsToBody (options) {
74+
const { orgId, projectId, pipelineDefinitionId, vcsOrigin, contextIds } = options
75+
const trustConfig = {
76+
type: 'circleci',
77+
claims: {
78+
org_id: orgId,
79+
project_id: projectId,
80+
pipeline_definition_id: pipelineDefinitionId,
81+
vcs_origin: vcsOrigin,
82+
},
83+
}
84+
if (contextIds && contextIds.length > 0) {
85+
trustConfig.claims.context_ids = contextIds
86+
}
87+
return trustConfig
88+
}
89+
90+
static bodyToOptions (body) {
91+
return {
92+
...(body.id) && { id: body.id },
93+
...(body.type) && { type: body.type },
94+
...(body.claims?.org_id) && { orgId: body.claims.org_id },
95+
...(body.claims?.project_id) && { projectId: body.claims.project_id },
96+
...(body.claims?.pipeline_definition_id) && {
97+
pipelineDefinitionId: body.claims.pipeline_definition_id,
98+
},
99+
...(body.claims?.vcs_origin) && { vcsOrigin: body.claims.vcs_origin },
100+
...(body.claims?.context_ids) && { contextIds: body.claims.context_ids },
101+
}
102+
}
103+
104+
// Override flagsToOptions since CircleCI doesn't use file/entity pattern
105+
async flagsToOptions ({ positionalArgs, flags }) {
106+
const content = await this.optionalPkgJson()
107+
const pkgName = positionalArgs[0] || content.name
108+
109+
if (!pkgName) {
110+
throw new Error('Package name must be specified either as an argument or in package.json file')
111+
}
112+
113+
const orgId = flags['org-id']
114+
const projectId = flags['project-id']
115+
const pipelineDefinitionId = flags['pipeline-definition-id']
116+
const vcsOrigin = flags['vcs-origin']
117+
const contextIds = flags['context-id']
118+
119+
// Validate required flags
120+
if (!orgId) {
121+
throw new Error('org-id is required')
122+
}
123+
if (!projectId) {
124+
throw new Error('project-id is required')
125+
}
126+
if (!pipelineDefinitionId) {
127+
throw new Error('pipeline-definition-id is required')
128+
}
129+
if (!vcsOrigin) {
130+
throw new Error('vcs-origin is required')
131+
}
132+
133+
// Validate formats
134+
this.validateUuid(orgId, 'org-id')
135+
this.validateUuid(projectId, 'project-id')
136+
this.validateUuid(pipelineDefinitionId, 'pipeline-definition-id')
137+
this.validateVcsOrigin(vcsOrigin)
138+
if (contextIds?.length > 0) {
139+
for (const contextId of contextIds) {
140+
this.validateUuid(contextId, 'context-id')
141+
}
142+
}
143+
144+
return {
145+
values: {
146+
package: pkgName,
147+
orgId,
148+
projectId,
149+
pipelineDefinitionId,
150+
vcsOrigin,
151+
...(contextIds?.length > 0 && { contextIds }),
152+
},
153+
fromPackageJson: {},
154+
warnings: [],
155+
urls: {
156+
package: this.getFrontendUrl({ pkgName }),
157+
vcsOrigin: this.getVcsOriginUrl(vcsOrigin),
158+
},
159+
}
160+
}
161+
162+
async exec (positionalArgs, flags) {
163+
await this.createConfigCommand({
164+
positionalArgs,
165+
flags,
166+
})
167+
}
168+
}
169+
170+
module.exports = TrustCircleCI

lib/commands/trust/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ class Trust extends BaseCommand {
77
static subcommands = {
88
github: require('./github.js'),
99
gitlab: require('./gitlab.js'),
10+
circleci: require('./circleci.js'),
1011
list: require('./list.js'),
1112
revoke: require('./revoke.js'),
1213
}

tap-snapshots/test/lib/commands/completion.js.test.cjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ Array [
137137
String(
138138
github
139139
gitlab
140+
circleci
140141
list
141142
revoke
142143
),

tap-snapshots/test/lib/docs.js.test.cjs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5710,6 +5710,9 @@ Subcommands:
57105710
gitlab
57115711
Create a trusted relationship between a package and GitLab CI/CD
57125712
5713+
circleci
5714+
Create a trusted relationship between a package and CircleCI
5715+
57135716
list
57145717
List trusted relationships for a package
57155718
@@ -5743,6 +5746,16 @@ Note: This command is unaware of workspaces.
57435746
#### \`json\`
57445747
#### \`dry-run\`
57455748
#### Synopsis
5749+
#### Flags
5750+
#### \`org-id\`
5751+
#### \`project-id\`
5752+
#### \`pipeline-definition-id\`
5753+
#### \`vcs-origin\`
5754+
#### \`context-id\`
5755+
#### \`yes\`
5756+
#### \`json\`
5757+
#### \`dry-run\`
5758+
#### Synopsis
57465759
#### Configuration
57475760
#### \`json\`
57485761
#### Synopsis

0 commit comments

Comments
 (0)