verify-branch-roles.mjs 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. import { spawnSync } from 'node:child_process'
  2. import { readFile } from 'node:fs/promises'
  3. import { dirname, join, resolve } from 'node:path'
  4. import { fileURLToPath } from 'node:url'
  5. const scriptDir = dirname(fileURLToPath(import.meta.url))
  6. const sourceRoot = resolve(scriptDir, '..')
  7. const branchRoles = await readJson(join(sourceRoot, 'ecosystem', 'branch-roles.json'))
  8. const wrapperManifest = await readJson(join(sourceRoot, 'ecosystem', 'wrappers.json'))
  9. const rootPackage = await readJson(join(sourceRoot, 'package.json'))
  10. function assert(condition, message) {
  11. if (!condition) {
  12. throw new Error(message)
  13. }
  14. }
  15. async function readJson(path) {
  16. return JSON.parse(await readFile(path, 'utf8'))
  17. }
  18. function runGit(args) {
  19. const result = spawnSync('git', args, {
  20. cwd: sourceRoot,
  21. encoding: 'utf8',
  22. stdio: 'pipe'
  23. })
  24. if (result.status !== 0) {
  25. throw new Error(`Command failed: git ${args.join(' ')}\n${result.stderr || result.stdout}`)
  26. }
  27. return result.stdout.trim()
  28. }
  29. function repositoryPath(url) {
  30. const normalized = url
  31. .replace(/^git\+/, '')
  32. .replace(/\.git$/, '')
  33. .replace(/^https?:\/\//, '')
  34. .replace(/^git@([^:]+):/, '$1/')
  35. const segments = normalized.split('/')
  36. return segments.slice(-2).join('/')
  37. }
  38. function assertPublicRepository(url, expectedOrg, label) {
  39. const path = repositoryPath(url)
  40. const [org] = path.split('/')
  41. assert(org === expectedOrg, `${label} must live under ${expectedOrg}, got ${url}`)
  42. }
  43. assert(branchRoles.schemaVersion === 1, 'branch-roles.json schemaVersion must be 1')
  44. assert(branchRoles.currentSourceBranch, 'branch-roles.json must declare currentSourceBranch')
  45. assert(branchRoles.sourceRemote?.name === 'origin', 'Private source remote must be named origin')
  46. assert(branchRoles.sourceRemote?.visibility === 'private', 'Source remote must be marked private')
  47. assert(
  48. branchRoles.sourceRemote?.url === wrapperManifest.corePackage.aggregateRepository,
  49. 'branch-roles source remote must match wrappers.json corePackage.aggregateRepository'
  50. )
  51. assert(
  52. wrapperManifest.corePackage.visibility === 'public-source',
  53. 'wrappers.json corePackage.visibility must be public-source'
  54. )
  55. const originUrl = runGit(['remote', 'get-url', branchRoles.sourceRemote.name])
  56. assert(
  57. originUrl.replace(/\.git$/, '') === branchRoles.sourceRemote.url.replace(/\.git$/, ''),
  58. `git origin must point to ${branchRoles.sourceRemote.url}, got ${originUrl}`
  59. )
  60. const currentBranch = runGit(['branch', '--show-current'])
  61. const rolesByName = new Map(branchRoles.branches.map(branch => [branch.name, branch]))
  62. assert(rolesByName.has('main'), 'branch roles must include main')
  63. assert(rolesByName.has('v2'), 'branch roles must include v2')
  64. assert(rolesByName.has('v3'), 'branch roles must include v3')
  65. assert(rolesByName.has(currentBranch), `current branch ${currentBranch} must be declared in branch roles`)
  66. const mainRole = rolesByName.get('main')
  67. assert(mainRole.role === 'private-aggregate-workspace', 'main branch role must be private-aggregate-workspace')
  68. assert(mainRole.packageName === rootPackage.name, `main branch package must be ${rootPackage.name}`)
  69. assert(
  70. mainRole.sourcePolicy === 'private-complete-original-workspace',
  71. 'main branch source policy must keep the complete original workspace private'
  72. )
  73. const wrappersById = new Map(wrapperManifest.wrappers.map(wrapper => [wrapper.id, wrapper]))
  74. const compatibilityPackagesByName = new Map(
  75. (wrapperManifest.compatibilityPackages || []).map(compatibilityPackage => [
  76. compatibilityPackage.packageName,
  77. compatibilityPackage
  78. ])
  79. )
  80. for (const branchName of ['v2', 'v3']) {
  81. const role = rolesByName.get(branchName)
  82. assert(role.wrapperId, `${branchName} branch must declare wrapperId`)
  83. const wrapper = wrappersById.get(role.wrapperId)
  84. assert(wrapper, `${branchName} wrapperId ${role.wrapperId} must exist in wrappers.json`)
  85. assert(role.packageName === wrapper.packageName, `${branchName} packageName must match wrappers.json`)
  86. assert(
  87. role.sourcePolicy === 'component-source-exported-publicly',
  88. `${branchName} source policy must export component source publicly`
  89. )
  90. for (const compatibilityPackage of role.compatibilityPackages || []) {
  91. assert(
  92. wrapper.historicalPackages.includes(compatibilityPackage),
  93. `${branchName} compatibility package ${compatibilityPackage} must be declared in wrappers.json`
  94. )
  95. const compatibilityPackageEntry = compatibilityPackagesByName.get(compatibilityPackage)
  96. assert(
  97. compatibilityPackageEntry,
  98. `${branchName} compatibility package ${compatibilityPackage} must have a compatibilityPackages entry`
  99. )
  100. assert(
  101. compatibilityPackageEntry.targetPackage === wrapper.packageName,
  102. `${branchName} compatibility package ${compatibilityPackage} must target ${wrapper.packageName}`
  103. )
  104. assert(
  105. compatibilityPackageEntry.packageDir,
  106. `${branchName} compatibility package ${compatibilityPackage} must declare packageDir`
  107. )
  108. }
  109. }
  110. for (const wrapper of wrapperManifest.wrappers) {
  111. for (const historicalPackage of wrapper.historicalPackages || []) {
  112. assert(
  113. compatibilityPackagesByName.has(historicalPackage),
  114. `Historical package ${historicalPackage} must have a compatibilityPackages entry`
  115. )
  116. }
  117. }
  118. assert(
  119. wrapperManifest.sourceBranch === branchRoles.currentSourceBranch,
  120. `wrappers.json sourceBranch must match branch-roles currentSourceBranch ${branchRoles.currentSourceBranch}`
  121. )
  122. assert(
  123. branchRoles.targetBranchModel?.main === mainRole.role,
  124. 'targetBranchModel.main must match the declared main branch role'
  125. )
  126. assert(
  127. branchRoles.targetBranchModel?.v2 === rolesByName.get('v2')?.role,
  128. 'targetBranchModel.v2 must match the declared v2 branch role'
  129. )
  130. assert(
  131. branchRoles.targetBranchModel?.v3 === rolesByName.get('v3')?.role,
  132. 'targetBranchModel.v3 must match the declared v3 branch role'
  133. )
  134. assert(branchRoles.publicOrganization === wrapperManifest.organization, 'Public organization must match wrappers.json')
  135. assert(
  136. branchRoles.publicMainRepository?.sourcePolicy === 'public-open-source-main-repository',
  137. 'Open-source main repository must publish source, demo code, documentation, and release artifacts'
  138. )
  139. assertPublicRepository(branchRoles.publicMainRepository.github, branchRoles.publicOrganization, 'Open-source main GitHub repository')
  140. assertPublicRepository(branchRoles.publicMainRepository.gitee, branchRoles.publicOrganization, 'Open-source main Gitee repository')
  141. const publicMainRepo = repositoryPath(branchRoles.publicMainRepository.github)
  142. assertPublicRepository(
  143. wrapperManifest.corePackage.github,
  144. wrapperManifest.organization,
  145. `${wrapperManifest.corePackage.packageName} GitHub repository`
  146. )
  147. assertPublicRepository(
  148. wrapperManifest.corePackage.gitee,
  149. wrapperManifest.organization,
  150. `${wrapperManifest.corePackage.packageName} Gitee repository`
  151. )
  152. assert(
  153. repositoryPath(wrapperManifest.corePackage.github) !== publicMainRepo,
  154. `${wrapperManifest.corePackage.packageName} must use a dedicated core repository, not the open-source main repository`
  155. )
  156. for (const wrapper of wrapperManifest.wrappers) {
  157. assertPublicRepository(wrapper.github, wrapperManifest.organization, `${wrapper.packageName} GitHub repository`)
  158. assertPublicRepository(wrapper.gitee, wrapperManifest.organization, `${wrapper.packageName} Gitee repository`)
  159. assert(
  160. repositoryPath(wrapper.github) !== publicMainRepo,
  161. `${wrapper.packageName} must use a dedicated component repository, not the open-source main repository`
  162. )
  163. }
  164. console.log(
  165. `Verified ${branchRoles.branches.length} branch roles, aggregate origin, and ${wrapperManifest.wrappers.length + 1} public core/component repositories.`
  166. )