circleci.js 5.7 KB

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