verify-public-main.mjs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. import { readFile } from 'node:fs/promises'
  2. import { dirname, join, resolve } from 'node:path'
  3. import { fileURLToPath } from 'node:url'
  4. import { loadEcosystemReleaseContext, readJson } from './lib/ecosystem-packages.mjs'
  5. import { validateJsonSchema } from './lib/simple-json-schema.mjs'
  6. import {
  7. assertDirectory,
  8. assertFile,
  9. assertOpenSourceMainRepoLayout
  10. } from './lib/public-main.mjs'
  11. import { entryFormatLabels } from './lib/wrapper-entry-formats.mjs'
  12. const scriptDir = dirname(fileURLToPath(import.meta.url))
  13. const sourceRoot = resolve(scriptDir, '..')
  14. const args = process.argv.slice(2)
  15. function readArg(name, fallback) {
  16. const index = args.indexOf(name)
  17. if (index >= 0) {
  18. return args[index + 1]
  19. }
  20. return fallback
  21. }
  22. const publicRepoDir = resolve(
  23. sourceRoot,
  24. readArg('--public-repo-dir', process.env.FILE_VIEWER_PUBLIC_REPO_DIR || '../file-viewer-public')
  25. )
  26. const { rootPackage, wrapperManifest, entries: ecosystemPackageEntries } =
  27. await loadEcosystemReleaseContext(sourceRoot)
  28. const readmeTemplate = await readJson(join(sourceRoot, 'ecosystem', 'wrapper-readme-template.json'))
  29. const version = rootPackage.version
  30. const requiredMetadataAssets = [
  31. {
  32. name: 'release-manifest.json',
  33. role: 'release-manifest'
  34. },
  35. {
  36. name: 'release-status.json',
  37. role: 'release-status'
  38. },
  39. {
  40. name: 'release-status.schema.json',
  41. role: 'release-status-schema'
  42. }
  43. ]
  44. function assert(condition, message) {
  45. if (!condition) {
  46. throw new Error(message)
  47. }
  48. }
  49. async function readText(path) {
  50. return readFile(path, 'utf8')
  51. }
  52. function assertIncludes(content, needle, label) {
  53. assert(content.includes(needle), `${label} is missing ${needle}`)
  54. }
  55. function assertPackageRecord(record, entry) {
  56. assert(record, `Release manifest is missing ${entry.packageName}`)
  57. assert(record.version === entry.version, `${entry.packageName} manifest version ${record.version} !== ${entry.version}`)
  58. assert(record.kind === entry.kind, `${entry.packageName} manifest kind ${record.kind} !== ${entry.kind}`)
  59. assert(record.packageDir === entry.packageDir, `${entry.packageName} manifest packageDir drifted`)
  60. }
  61. async function assertReleaseManifest(repoDir) {
  62. const manifestPath = join(repoDir, 'artifacts', 'release-manifest.json')
  63. await assertFile(manifestPath, 'artifacts/release-manifest.json')
  64. const manifest = await readJson(manifestPath)
  65. assert(manifest.version === version, `Open-source main repository manifest version ${manifest.version} !== ${version}`)
  66. assert(manifest.openSourceMain === true, 'Open-source main repository manifest must declare openSourceMain=true')
  67. assert(manifest.coreSourceIncluded === true, 'Open-source main repository manifest must declare coreSourceIncluded=true')
  68. assert(manifest.sourcePolicy === 'public-open-source-main-repository', 'Open-source main repository source policy drifted')
  69. assert(manifest.repositoryRole === 'open-source-main-repository', 'Open-source main repository role drifted')
  70. assert(manifest.corePackage?.packageName === wrapperManifest.corePackage.packageName, 'Core package metadata drifted')
  71. assert(manifest.corePackage?.visibility === wrapperManifest.corePackage.visibility, 'Core package visibility metadata drifted')
  72. const metadataAssets = new Map((manifest.metadataAssets || []).map(asset => [asset.name, asset]))
  73. for (const expected of requiredMetadataAssets) {
  74. const asset = metadataAssets.get(expected.name)
  75. assert(asset, `Release manifest is missing metadata asset ${expected.name}`)
  76. assert(asset.role === expected.role, `Release manifest metadata asset ${expected.name} role drifted`)
  77. assert(asset.required === true, `Release manifest metadata asset ${expected.name} must be required`)
  78. await assertFile(join(repoDir, 'artifacts', expected.name), `metadata asset ${expected.name}`)
  79. }
  80. const artifactRecords = new Map((manifest.ecosystemArtifacts || []).map(record => [record.name, record]))
  81. const ecosystemPackages = manifest.ecosystemPackages || {}
  82. for (const entry of ecosystemPackageEntries) {
  83. assert(ecosystemPackages[entry.packageName] === entry.version, `${entry.packageName} ecosystem package version missing from manifest`)
  84. const record = artifactRecords.get(entry.packageName)
  85. assertPackageRecord(record, entry)
  86. if (entry.wrapper) {
  87. assert(
  88. JSON.stringify(record.entryFormats || []) === JSON.stringify(entry.wrapper.entryFormats || []),
  89. `${entry.packageName} ecosystem artifact entry format mapping drifted`
  90. )
  91. }
  92. if (entry.releaseArtifact?.includeTarball === false) {
  93. assert(record.artifactIncluded === false, `${entry.packageName} duplicate artifact should not be included`)
  94. assert(record.artifactDuplicateOf === entry.releaseArtifact.duplicateOf, `${entry.packageName} duplicate artifact target drifted`)
  95. continue
  96. }
  97. assert(record.artifactIncluded === true, `${entry.packageName} artifact should be included`)
  98. assert(record.tarball === entry.tarballName, `${entry.packageName} tarball name drifted`)
  99. await assertFile(join(repoDir, 'artifacts', entry.tarballName), `${entry.packageName} tarball`)
  100. }
  101. const wrapperRecords = new Map((manifest.wrapperRepositories || []).map(record => [record.packageName, record]))
  102. for (const wrapper of wrapperManifest.wrappers) {
  103. const record = wrapperRecords.get(wrapper.packageName)
  104. assert(record, `Wrapper repository manifest is missing ${wrapper.packageName}`)
  105. for (const [key, expected] of Object.entries({
  106. framework: wrapper.framework,
  107. repository: wrapper.repository,
  108. github: wrapper.github,
  109. gitee: wrapper.gitee
  110. })) {
  111. assert(record[key] === expected, `${wrapper.packageName} wrapper repository ${key} drifted`)
  112. }
  113. assert(
  114. JSON.stringify(record.entryFormats || []) === JSON.stringify(wrapper.entryFormats || []),
  115. `${wrapper.packageName} wrapper repository entry format mapping drifted`
  116. )
  117. assert(
  118. JSON.stringify(record.historicalPackages || []) === JSON.stringify(wrapper.historicalPackages || []),
  119. `${wrapper.packageName} historical package mapping drifted`
  120. )
  121. }
  122. for (const requiredTarball of [
  123. `file-viewer-v2-${version}-demo.tar.gz`,
  124. `file-viewer-v2-${version}-component-demo.tar.gz`,
  125. `file-viewer-v2-${version}-lib-dist.tar.gz`,
  126. `file-viewer-v2-${version}-docs.tar.gz`
  127. ]) {
  128. await assertFile(join(repoDir, 'artifacts', requiredTarball), requiredTarball)
  129. }
  130. if (manifest.vue2Package) {
  131. assert(manifest.vue2Package.name === '@flyfish-group/file-viewer', 'Vue2 compatibility package name drifted')
  132. assert(manifest.vue2Package.version === version, `Vue2 compatibility package version ${manifest.vue2Package.version} !== ${version}`)
  133. assert(manifest.vue2Package.tarball, 'Vue2 compatibility package tarball is missing from manifest')
  134. await assertFile(join(repoDir, 'artifacts', manifest.vue2Package.tarball), 'Vue2 compatibility package tarball')
  135. }
  136. return manifest
  137. }
  138. async function assertReleaseStatus(repoDir) {
  139. const sourceSchemaPath = join(sourceRoot, 'ecosystem', 'release-status.schema.json')
  140. const publicSchemaPath = join(repoDir, 'artifacts', 'release-status.schema.json')
  141. await assertFile(publicSchemaPath, 'artifacts/release-status.schema.json')
  142. const sourceSchema = await readJson(sourceSchemaPath)
  143. const publicSchema = await readJson(publicSchemaPath)
  144. assert(
  145. JSON.stringify(publicSchema) === JSON.stringify(sourceSchema),
  146. 'Open-source main release status schema drifted from ecosystem/release-status.schema.json'
  147. )
  148. const statusPath = join(repoDir, 'artifacts', 'release-status.json')
  149. await assertFile(statusPath, 'artifacts/release-status.json')
  150. const status = await readJson(statusPath)
  151. const schemaFailures = validateJsonSchema(status, publicSchema)
  152. assert(!schemaFailures.length, `Release status schema validation failed:\n${schemaFailures.join('\n')}`)
  153. assert(status.schemaVersion === 1, 'Release status schemaVersion drifted')
  154. assert(status.version === version, `Release status version ${status.version} !== ${version}`)
  155. assert(status.sourcePolicy === 'private-complete-original-workspace', 'Release status source policy drifted')
  156. assert(status.openSourcePolicy === 'public-open-source-main-repository', 'Release status open-source policy drifted')
  157. assert(status.sourceBaseline?.branch === 'main', 'Release status source baseline branch must be private main')
  158. assert(
  159. status.sourceBaseline?.policy === 'private-complete-original-workspace',
  160. 'Release status source baseline policy drifted'
  161. )
  162. assert(
  163. typeof status.sourceBaseline?.note === 'string' &&
  164. status.sourceBaseline.note.includes('complete original aggregate workspace'),
  165. 'Release status must document that private main is the complete original aggregate workspace'
  166. )
  167. assert(
  168. status.sourceBaseline?.authoritativeCommit === status.sourceRemote?.hash,
  169. 'Release status source baseline commit must match source remote hash'
  170. )
  171. assert(
  172. status.sourceBaseline?.localCommit === status.local?.commit,
  173. 'Release status source baseline local commit must match local commit'
  174. )
  175. assert(
  176. typeof status.openSourceMain?.reportHashNote === 'string' && status.openSourceMain.reportHashNote.includes('public commit body'),
  177. 'Release status must document the public mirror commit traceability caveat'
  178. )
  179. assert(typeof status.githubRelease?.hasManifest === 'boolean', 'Release status GitHub Release manifest flag missing')
  180. assert(typeof status.githubRelease?.hasStatus === 'boolean', 'Release status GitHub Release status flag missing')
  181. assert(typeof status.githubRelease?.hasSchema === 'boolean', 'Release status GitHub Release schema flag missing')
  182. assert(Array.isArray(status.componentRepositories), 'Release status component repository rows missing')
  183. assert(Array.isArray(status.npmPackages), 'Release status npm package rows missing')
  184. assert(Array.isArray(status.gaps), 'Release status gaps list missing')
  185. assert(Array.isArray(status.gapDetails), 'Release status gap detail rows missing')
  186. assert(status.gapSummary?.total === status.gaps.length, 'Release status gap summary total drifted')
  187. assert(status.gapDetails.length === status.gaps.length, 'Release status gap detail count drifted')
  188. assert(
  189. status.gapSummary.byChannel && typeof status.gapSummary.byChannel === 'object',
  190. 'Release status gap summary channel map missing'
  191. )
  192. for (const detail of status.gapDetails) {
  193. assert(typeof detail.channel === 'string' && detail.channel, 'Release status gap detail channel missing')
  194. assert(typeof detail.message === 'string' && status.gaps.includes(detail.message), 'Release status gap detail message drifted')
  195. assert(typeof detail.externalBlocker === 'boolean', 'Release status gap detail externalBlocker flag missing')
  196. assert(typeof detail.nextAction === 'string' && detail.nextAction, 'Release status gap detail next action missing')
  197. }
  198. const packageRepositoryTargets = [
  199. wrapperManifest.corePackage,
  200. ...wrapperManifest.wrappers
  201. ]
  202. assert(
  203. status.componentRepositories.length === packageRepositoryTargets.length,
  204. `Release status package repository count ${status.componentRepositories.length} !== ${packageRepositoryTargets.length}`
  205. )
  206. const packageRepositoryRows = new Map(status.componentRepositories.map(row => [row.packageName, row]))
  207. for (const target of packageRepositoryTargets) {
  208. const row = packageRepositoryRows.get(target.packageName)
  209. assert(row, `Release status missing package repository row for ${target.packageName}`)
  210. assert(row.github?.url === target.github, `${target.packageName} GitHub repository URL drifted`)
  211. assert(row.github?.ok === true, `${target.packageName} GitHub repository is not reachable`)
  212. }
  213. assert(
  214. status.npmPackages.length === ecosystemPackageEntries.length,
  215. `Release status npm package count ${status.npmPackages.length} !== ${ecosystemPackageEntries.length}`
  216. )
  217. const npmRows = new Map(status.npmPackages.map(row => [row.packageName, row]))
  218. for (const entry of ecosystemPackageEntries) {
  219. const row = npmRows.get(entry.packageName)
  220. assert(row, `Release status missing npm row for ${entry.packageName}`)
  221. assert(row.expectedVersion === entry.version, `${entry.packageName} release status expected version drifted`)
  222. }
  223. }
  224. async function assertReadmes(repoDir) {
  225. const readme = await readText(join(repoDir, 'README.md'))
  226. const readmeEn = await readText(join(repoDir, 'README.en.md'))
  227. for (const [locale, content] of [['zh', readme], ['en', readmeEn]]) {
  228. const template = readmeTemplate.locales[locale]
  229. const entryLabels = entryFormatLabels(locale)
  230. assertIncludes(content, wrapperManifest.corePackage.packageName, 'public README')
  231. assertIncludes(content, readmeTemplate.markers.publicGenerated.start, 'public README')
  232. assertIncludes(content, readmeTemplate.markers.publicGenerated.end, 'public README')
  233. assertIncludes(content, template.publicEcosystemHeading, 'public README')
  234. for (const requiredLink of readmeTemplate.requiredLinks) {
  235. assertIncludes(content, requiredLink.replace(/\/$/, ''), 'public README')
  236. }
  237. for (const requiredTerm of readmeTemplate.requiredTerms) {
  238. assertIncludes(content, requiredTerm, 'public README')
  239. }
  240. for (const header of template.wrapperMatrixHeaders) {
  241. assertIncludes(content, header, 'public README')
  242. }
  243. for (const wrapper of wrapperManifest.wrappers) {
  244. assertIncludes(content, wrapper.packageName, 'public README')
  245. assertIncludes(content, wrapper.github, 'public README')
  246. assertIncludes(content, wrapper.gitee, 'public README')
  247. for (const format of wrapper.entryFormats || []) {
  248. assertIncludes(content, entryLabels[format] || format, 'public README')
  249. }
  250. for (const historicalPackage of wrapper.historicalPackages || []) {
  251. assertIncludes(content, historicalPackage, 'public README')
  252. }
  253. }
  254. }
  255. }
  256. await assertDirectory(publicRepoDir, 'open-source main repository')
  257. for (const requiredFile of ['README.md', 'README.en.md', 'BRANCHES.md', 'WRAPPER_ECOSYSTEM.md', 'LICENSE', 'package.json']) {
  258. await assertFile(join(publicRepoDir, requiredFile), requiredFile)
  259. }
  260. for (const requiredDirectory of ['apps', 'packages', 'docs', 'dist', 'artifacts']) {
  261. await assertDirectory(join(publicRepoDir, requiredDirectory), requiredDirectory)
  262. }
  263. await assertDirectory(join(publicRepoDir, 'packages', 'core'), 'packages/core')
  264. await assertDirectory(join(publicRepoDir, 'packages', 'renderers'), 'packages/renderers')
  265. await assertFile(join(publicRepoDir, 'pnpm-workspace.yaml'), 'pnpm-workspace.yaml')
  266. const manifest = await assertReleaseManifest(publicRepoDir)
  267. await assertReleaseStatus(publicRepoDir)
  268. await assertOpenSourceMainRepoLayout(publicRepoDir, { allowedRoots: manifest.allowedRoots })
  269. await assertReadmes(publicRepoDir)
  270. console.log(`Verified open-source main repository at ${publicRepoDir} for ${version}.`)