create-gitee-component-repos.mjs 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. import { spawnSync } from 'node:child_process'
  2. import { readFileSync } from 'node:fs'
  3. import { readFile } from 'node:fs/promises'
  4. import { dirname, join, resolve } from 'node:path'
  5. import { fileURLToPath } from 'node:url'
  6. const scriptDir = dirname(fileURLToPath(import.meta.url))
  7. const sourceRoot = resolve(scriptDir, '..')
  8. const args = process.argv.slice(2)
  9. const dryRun = args.includes('--dry-run') || process.env.FILE_VIEWER_GITEE_CREATE_DRY_RUN === '1'
  10. const preflight = args.includes('--preflight')
  11. const force = args.includes('--force')
  12. const explicitToken =
  13. process.env.FILE_VIEWER_GITEE_TOKEN ||
  14. process.env.GITEE_TOKEN ||
  15. process.env.GITEE_ACCESS_TOKEN ||
  16. ''
  17. const selectedIds = new Set(
  18. args
  19. .filter(arg => arg.startsWith('--id='))
  20. .map(arg => arg.slice('--id='.length))
  21. )
  22. const selectedPackages = new Set(
  23. args
  24. .filter(arg => arg.startsWith('--package='))
  25. .map(arg => arg.slice('--package='.length))
  26. )
  27. function readArg(name, fallback) {
  28. const index = args.indexOf(name)
  29. return index >= 0 ? args[index + 1] : fallback
  30. }
  31. const apiBase = readArg('--api-base', process.env.GITEE_API_BASE || 'https://gitee.com/api/v5')
  32. const tokenFile = readArg('--token-file', process.env.FILE_VIEWER_GITEE_TOKEN_FILE || '')
  33. const useGitCredentialToken =
  34. args.includes('--use-git-credential') ||
  35. process.env.FILE_VIEWER_GITEE_USE_GIT_CREDENTIAL === '1'
  36. function readTokenFile(path) {
  37. if (!path) {
  38. return ''
  39. }
  40. return readFileSync(resolve(path), 'utf8').trim()
  41. }
  42. function readGiteeCredentialToken() {
  43. const result = spawnSync('git', ['credential', 'fill'], {
  44. cwd: sourceRoot,
  45. input: 'protocol=https\nhost=gitee.com\n\n',
  46. encoding: 'utf8',
  47. stdio: ['pipe', 'pipe', 'pipe']
  48. })
  49. if (result.status !== 0) {
  50. return ''
  51. }
  52. const passwordLine = result.stdout
  53. .split('\n')
  54. .find(line => line.startsWith('password='))
  55. return passwordLine?.slice('password='.length).trim() || ''
  56. }
  57. const tokenFromFile = !explicitToken ? readTokenFile(tokenFile) : ''
  58. const credentialToken = !explicitToken && !tokenFromFile && useGitCredentialToken ? readGiteeCredentialToken() : ''
  59. const token = explicitToken || tokenFromFile || credentialToken
  60. const tokenSource = explicitToken
  61. ? 'environment'
  62. : tokenFromFile
  63. ? 'token-file'
  64. : credentialToken
  65. ? 'git-credential'
  66. : 'missing'
  67. const missingTokenMessage =
  68. 'Missing Gitee token. Set FILE_VIEWER_GITEE_TOKEN, GITEE_TOKEN, GITEE_ACCESS_TOKEN, FILE_VIEWER_GITEE_TOKEN_FILE, or pass --use-git-credential when the stored gitee.com password is an API token.'
  69. async function readJson(path) {
  70. return JSON.parse(await readFile(path, 'utf8'))
  71. }
  72. function repoNameFromUrl(url) {
  73. const path = new URL(url).pathname.replace(/^\/+|\.git$/g, '')
  74. return path.split('/').at(-1)
  75. }
  76. function repoOwnerFromUrl(url) {
  77. const path = new URL(url).pathname.replace(/^\/+|\.git$/g, '')
  78. return path.split('/')[0]
  79. }
  80. function targetDescription(target) {
  81. if (target.id === 'core') {
  82. return 'Framework-neutral TypeScript core for Flyfish File Viewer.'
  83. }
  84. return `${target.framework} standard component package for Flyfish File Viewer.`
  85. }
  86. function targetHomepage(target) {
  87. return target.github || 'https://github.com/flyfish-dev/file-viewer'
  88. }
  89. async function giteeRequest(method, path, body = undefined) {
  90. if (!token) {
  91. throw new Error(missingTokenMessage)
  92. }
  93. const url = new URL(path, `${apiBase.replace(/\/+$/, '')}/`)
  94. const init = {
  95. method,
  96. headers: {
  97. Accept: 'application/json'
  98. }
  99. }
  100. if (method === 'GET') {
  101. url.searchParams.set('access_token', token)
  102. } else {
  103. const form = new URLSearchParams()
  104. form.set('access_token', token)
  105. for (const [key, value] of Object.entries(body || {})) {
  106. if (value !== undefined && value !== null) {
  107. form.set(key, String(value))
  108. }
  109. }
  110. init.headers['Content-Type'] = 'application/x-www-form-urlencoded'
  111. init.body = form
  112. }
  113. const response = await fetch(url, init)
  114. const text = await response.text()
  115. let payload = null
  116. try {
  117. payload = text ? JSON.parse(text) : null
  118. } catch {
  119. payload = text
  120. }
  121. return {
  122. ok: response.ok,
  123. status: response.status,
  124. payload
  125. }
  126. }
  127. function authHint(response) {
  128. if (response.status !== 401) {
  129. return ''
  130. }
  131. if (tokenSource === 'git-credential') {
  132. return ' The stored gitee.com git credential was used as the API token and was rejected; store a Gitee API access token in FILE_VIEWER_GITEE_TOKEN_FILE or an environment variable instead.'
  133. }
  134. return ' Check that the Gitee API token is valid and has organization repository permissions.'
  135. }
  136. async function verifyGiteeToken() {
  137. const response = await giteeRequest('GET', 'user')
  138. if (!response.ok) {
  139. throw new Error(
  140. `Gitee token preflight failed: HTTP ${response.status} ${JSON.stringify(response.payload)}${authHint(response)}`
  141. )
  142. }
  143. const user = response.payload?.login || response.payload?.name || response.payload?.id || 'unknown'
  144. console.log(
  145. `Gitee token preflight ok: source=${tokenSource}, user=${user}, selectedRepositories=${targets.length}.`
  146. )
  147. }
  148. async function repoExists(owner, repo) {
  149. if (!token) {
  150. return false
  151. }
  152. const response = await giteeRequest('GET', `repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`)
  153. if (response.ok) {
  154. return true
  155. }
  156. if (response.status === 404) {
  157. return false
  158. }
  159. throw new Error(
  160. `Failed to check Gitee repository ${owner}/${repo}: HTTP ${response.status} ${JSON.stringify(response.payload)}${authHint(response)}`
  161. )
  162. }
  163. async function createRepo(target) {
  164. const owner = repoOwnerFromUrl(target.gitee)
  165. const repo = repoNameFromUrl(target.gitee)
  166. if (!force && await repoExists(owner, repo)) {
  167. console.log(`exists\t${owner}/${repo}`)
  168. return 'exists'
  169. }
  170. if (dryRun) {
  171. console.log(`[dry-run]\tcreate\t${owner}/${repo}\t${target.packageName}`)
  172. return 'dry-run'
  173. }
  174. const response = await giteeRequest('POST', `orgs/${encodeURIComponent(owner)}/repos`, {
  175. name: repo,
  176. path: repo,
  177. description: targetDescription(target),
  178. homepage: targetHomepage(target),
  179. private: false,
  180. has_issues: true,
  181. has_wiki: false,
  182. can_comment: true,
  183. auto_init: false
  184. })
  185. if (response.ok || response.status === 201) {
  186. console.log(`created\t${owner}/${repo}`)
  187. return 'created'
  188. }
  189. if (response.status === 400 || response.status === 409) {
  190. if (await repoExists(owner, repo)) {
  191. console.log(`exists\t${owner}/${repo}`)
  192. return 'exists'
  193. }
  194. }
  195. throw new Error(
  196. `Failed to create Gitee repository ${owner}/${repo}: HTTP ${response.status} ${JSON.stringify(response.payload)}${authHint(response)}`
  197. )
  198. }
  199. const wrapperManifest = await readJson(join(sourceRoot, 'ecosystem', 'wrappers.json'))
  200. const core = wrapperManifest.corePackage
  201. const targets = [
  202. {
  203. id: 'core',
  204. kind: 'core',
  205. framework: 'Core',
  206. packageName: core.packageName,
  207. repository: core.repository,
  208. github: core.github,
  209. gitee: core.gitee
  210. },
  211. ...wrapperManifest.wrappers.map(wrapper => ({
  212. id: wrapper.id,
  213. kind: 'component',
  214. framework: wrapper.framework,
  215. packageName: wrapper.packageName,
  216. repository: wrapper.repository,
  217. github: wrapper.github,
  218. gitee: wrapper.gitee
  219. }))
  220. ].filter(target => {
  221. if (selectedIds.size && !selectedIds.has(target.id)) {
  222. return false
  223. }
  224. if (selectedPackages.size && !selectedPackages.has(target.packageName)) {
  225. return false
  226. }
  227. return true
  228. })
  229. if (!targets.length) {
  230. throw new Error('No Gitee core/component repositories selected.')
  231. }
  232. if ((preflight || !dryRun) && !token) {
  233. throw new Error(missingTokenMessage)
  234. }
  235. if (preflight) {
  236. await verifyGiteeToken()
  237. process.exit(0)
  238. }
  239. const results = new Map()
  240. for (const target of targets) {
  241. const result = await createRepo(target)
  242. results.set(result, (results.get(result) || 0) + 1)
  243. }
  244. console.log(
  245. `Checked ${targets.length} Gitee repositories: ${[...results.entries()]
  246. .map(([name, count]) => `${name}=${count}`)
  247. .join(', ')}.`
  248. )