verify-github-release-assets.mjs 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103
  1. import { createHash } from 'node:crypto'
  2. import { readdir, readFile, stat } from 'node:fs/promises'
  3. import { dirname, join, resolve } from 'node:path'
  4. import { spawnSync } from 'node:child_process'
  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. function readArg(name, fallback) {
  10. const index = args.indexOf(name)
  11. return index >= 0 ? args[index + 1] : fallback
  12. }
  13. const publicRepoDir = resolve(
  14. sourceRoot,
  15. readArg('--public-repo-dir', process.env.FILE_VIEWER_PUBLIC_REPO_DIR || '../file-viewer-public')
  16. )
  17. const artifactsDir = join(publicRepoDir, 'artifacts')
  18. const manifestPath = join(artifactsDir, 'release-manifest.json')
  19. const manifest = JSON.parse(await readFile(manifestPath, 'utf8'))
  20. const tag = readArg('--tag', `v${manifest.version}`)
  21. const repo = readArg('--repo', 'flyfish-dev/file-viewer')
  22. function runGh(commandArgs) {
  23. const result = spawnSync('gh', commandArgs, {
  24. cwd: sourceRoot,
  25. encoding: 'utf8',
  26. stdio: 'pipe',
  27. timeout: 30_000
  28. })
  29. if (result.status !== 0) {
  30. throw new Error((result.stderr || result.stdout || '').trim() || `gh ${commandArgs.join(' ')} failed`)
  31. }
  32. return result.stdout
  33. }
  34. async function sha256(path) {
  35. const data = await readFile(path)
  36. return createHash('sha256').update(data).digest('hex')
  37. }
  38. async function expectedAssets() {
  39. const names = (await readdir(artifactsDir))
  40. .filter(name => !name.startsWith('.'))
  41. .sort()
  42. const entries = []
  43. for (const name of names) {
  44. const path = join(artifactsDir, name)
  45. const info = await stat(path)
  46. if (!info.isFile()) {
  47. continue
  48. }
  49. entries.push({
  50. name,
  51. size: info.size,
  52. digest: `sha256:${await sha256(path)}`
  53. })
  54. }
  55. return entries
  56. }
  57. const release = JSON.parse(
  58. runGh(['release', 'view', tag, '--repo', repo, '--json', 'tagName,assets'])
  59. )
  60. const expected = await expectedAssets()
  61. const actual = new Map(release.assets.map(asset => [asset.name, asset]))
  62. const expectedNames = new Set(expected.map(asset => asset.name))
  63. const failures = []
  64. for (const asset of expected) {
  65. const uploaded = actual.get(asset.name)
  66. if (!uploaded) {
  67. failures.push(`missing asset ${asset.name}`)
  68. continue
  69. }
  70. if (uploaded.state !== 'uploaded') {
  71. failures.push(`${asset.name} state is ${uploaded.state}`)
  72. }
  73. if (uploaded.size !== asset.size) {
  74. failures.push(`${asset.name} size ${uploaded.size} !== local ${asset.size}`)
  75. }
  76. if (uploaded.digest && uploaded.digest !== asset.digest) {
  77. failures.push(`${asset.name} digest ${uploaded.digest} !== local ${asset.digest}`)
  78. }
  79. }
  80. for (const asset of release.assets) {
  81. if (!expectedNames.has(asset.name)) {
  82. failures.push(`unexpected asset ${asset.name}`)
  83. }
  84. }
  85. if (failures.length) {
  86. throw new Error(`GitHub Release asset verification failed for ${repo}@${tag}:\n${failures.join('\n')}`)
  87. }
  88. console.log(
  89. `Verified ${expected.length} GitHub Release assets for ${repo}@${tag} from ${artifactsDir}.`
  90. )