preflight-release-channels.mjs 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. import { existsSync } from 'node:fs'
  2. import { dirname, resolve } from 'node:path'
  3. import { spawnSync } from 'node:child_process'
  4. import { fileURLToPath } from 'node:url'
  5. const scriptDir = dirname(fileURLToPath(import.meta.url))
  6. const sourceRoot = resolve(scriptDir, '..')
  7. const args = process.argv.slice(2)
  8. function readArg(name, fallback) {
  9. const index = args.indexOf(name)
  10. return index >= 0 ? args[index + 1] : fallback
  11. }
  12. const skipExternal = args.includes('--skip-external')
  13. const publicRepoDir = resolve(
  14. sourceRoot,
  15. readArg('--public-repo-dir', process.env.FILE_VIEWER_PUBLIC_REPO_DIR || '../file-viewer-public')
  16. )
  17. const steps = [
  18. {
  19. name: 'branch roles',
  20. command: ['node', 'scripts/verify-branch-roles.mjs']
  21. },
  22. {
  23. name: 'ecosystem README coverage',
  24. command: ['node', 'scripts/verify-ecosystem-readmes.mjs']
  25. },
  26. {
  27. name: 'offline runtime assets',
  28. command: ['pnpm', 'verify:offline-assets'],
  29. okDetail: 'runtime preview code has no public CDN or third-party online asset fallback'
  30. },
  31. {
  32. name: 'ecosystem package metadata',
  33. command: ['node', 'scripts/release-ecosystem-packages.mjs', '--list'],
  34. okDetail: 'release package metadata loaded'
  35. },
  36. {
  37. name: 'open-source main repository',
  38. command: ['node', 'scripts/verify-public-main.mjs', '--public-repo-dir', publicRepoDir],
  39. skip: !existsSync(publicRepoDir),
  40. skipReason: `missing ${publicRepoDir}`
  41. },
  42. {
  43. name: 'GitHub Release assets',
  44. command: ['node', 'scripts/verify-github-release-assets.mjs', '--public-repo-dir', publicRepoDir],
  45. external: true,
  46. skip: !existsSync(publicRepoDir),
  47. skipReason: `missing ${publicRepoDir}`
  48. },
  49. {
  50. name: 'GitHub core/component content',
  51. command: ['pnpm', 'verify:wrapper-github-content'],
  52. external: true,
  53. okDetail: 'GitHub core/component repositories match local exports'
  54. },
  55. {
  56. name: 'npm publish authentication',
  57. command: ['node', 'scripts/release-ecosystem-packages.mjs', '--publish', '--preflight'],
  58. external: true,
  59. nextActions: [
  60. 'Run `npm login --registry=https://registry.npmjs.org/` in an interactive terminal and complete MFA/passkey.',
  61. 'Then rerun `pnpm release:channels:preflight` and `pnpm release:ecosystem:publish`.'
  62. ]
  63. },
  64. {
  65. name: 'Gitee API token',
  66. command: ['node', 'scripts/create-gitee-component-repos.mjs', '--preflight'],
  67. external: true,
  68. nextActions: [
  69. 'Store a Gitee organization API token outside the repository, for example `~/.config/flyfish/gitee-token`.',
  70. 'Run `FILE_VIEWER_GITEE_TOKEN_FILE=<token-file> pnpm components:gitee:preflight` before publishing mirrors.',
  71. 'Then run `FILE_VIEWER_GITEE_TOKEN_FILE=<token-file> pnpm components:gitee:publish`.'
  72. ]
  73. }
  74. ]
  75. function runStep(step) {
  76. if (step.skip) {
  77. return {
  78. ...step,
  79. status: 'skipped',
  80. detail: step.skipReason || 'not required'
  81. }
  82. }
  83. if (skipExternal && step.external) {
  84. return {
  85. ...step,
  86. status: 'skipped',
  87. detail: 'external checks skipped'
  88. }
  89. }
  90. const [command, ...commandArgs] = step.command
  91. const result = spawnSync(command, commandArgs, {
  92. cwd: sourceRoot,
  93. encoding: 'utf8',
  94. stdio: 'pipe',
  95. timeout: step.timeout ?? 30_000
  96. })
  97. const output = `${result.stdout || ''}\n${result.stderr || ''}`
  98. const detailLines = output
  99. .split('\n')
  100. .map(line => line.trim())
  101. .filter(Boolean)
  102. const detail =
  103. result.status === 0 && step.okDetail
  104. ? step.okDetail
  105. : detailLines.find(line => line.startsWith('Error:')) ||
  106. detailLines.find(line => /authentication|missing|required|failed/i.test(line)) ||
  107. detailLines.at(-1) ||
  108. ''
  109. return {
  110. ...step,
  111. status: result.status === 0 ? 'ok' : 'failed',
  112. detail
  113. }
  114. }
  115. const results = steps.map(runStep)
  116. const failures = results.filter(result => result.status === 'failed')
  117. console.log('# Release Channels Preflight\n')
  118. for (const result of results) {
  119. const label = result.status === 'ok' ? 'ok' : result.status
  120. console.log(`- ${label}\t${result.name}${result.detail ? `\t${result.detail}` : ''}`)
  121. }
  122. if (failures.length) {
  123. console.error(
  124. `\nRelease channels preflight failed: ${failures.map(result => result.name).join(', ')}.`
  125. )
  126. const nextActions = [
  127. ...new Set(failures.flatMap(result => result.nextActions || []))
  128. ]
  129. if (nextActions.length) {
  130. console.error('\nNext actions:')
  131. for (const action of nextActions) {
  132. console.error(`- ${action}`)
  133. }
  134. }
  135. process.exit(1)
  136. }
  137. console.log('\nRelease channels preflight passed.')