release.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450
  1. #!/usr/bin/env node
  2. if (!process.env.CI) require('dotenv-safe').load()
  3. require('colors')
  4. const args = require('minimist')(process.argv.slice(2), {
  5. boolean: [
  6. 'validateRelease',
  7. 'skipVersionCheck',
  8. 'automaticRelease',
  9. 'verboseNugget'
  10. ],
  11. default: { 'verboseNugget': false }
  12. })
  13. const fs = require('fs')
  14. const { execSync } = require('child_process')
  15. const GitHub = require('github')
  16. const nugget = require('nugget')
  17. const pkg = require('../package.json')
  18. const pkgVersion = `v${pkg.version}`
  19. const pass = '\u2713'.green
  20. const path = require('path')
  21. const fail = '\u2717'.red
  22. const sumchecker = require('sumchecker')
  23. const temp = require('temp').track()
  24. const { URL } = require('url')
  25. const targetRepo = pkgVersion.indexOf('nightly') > 0 ? 'nightlies' : 'electron'
  26. let failureCount = 0
  27. const github = new GitHub({
  28. followRedirects: false
  29. })
  30. github.authenticate({ type: 'token', token: process.env.ELECTRON_GITHUB_TOKEN })
  31. async function getDraftRelease (version, skipValidation) {
  32. const releaseInfo = await github.repos.getReleases({ owner: 'electron', repo: targetRepo })
  33. let versionToCheck
  34. if (version) {
  35. versionToCheck = version
  36. } else {
  37. versionToCheck = pkgVersion
  38. }
  39. const drafts = releaseInfo.data
  40. .filter(release => release.tag_name === versionToCheck &&
  41. release.draft === true)
  42. const draft = drafts[0]
  43. if (!skipValidation) {
  44. failureCount = 0
  45. check(drafts.length === 1, 'one draft exists', true)
  46. if (versionToCheck.indexOf('beta') > -1) {
  47. check(draft.prerelease, 'draft is a prerelease')
  48. }
  49. check(draft.body.length > 50 && !draft.body.includes('(placeholder)'), 'draft has release notes')
  50. check((failureCount === 0), `Draft release looks good to go.`, true)
  51. }
  52. return draft
  53. }
  54. async function validateReleaseAssets (release, validatingRelease) {
  55. const requiredAssets = assetsForVersion(release.tag_name, validatingRelease).sort()
  56. const extantAssets = release.assets.map(asset => asset.name).sort()
  57. const downloadUrls = release.assets.map(asset => asset.browser_download_url).sort()
  58. failureCount = 0
  59. requiredAssets.forEach(asset => {
  60. check(extantAssets.includes(asset), asset)
  61. })
  62. check((failureCount === 0), `All required GitHub assets exist for release`, true)
  63. if (!validatingRelease || !release.draft) {
  64. if (release.draft) {
  65. await verifyAssets(release)
  66. } else {
  67. await verifyShasums(downloadUrls)
  68. .catch(err => {
  69. console.log(`${fail} error verifyingShasums`, err)
  70. })
  71. }
  72. const s3Urls = s3UrlsForVersion(release.tag_name)
  73. await verifyShasums(s3Urls, true)
  74. }
  75. }
  76. function check (condition, statement, exitIfFail = false) {
  77. if (condition) {
  78. console.log(`${pass} ${statement}`)
  79. } else {
  80. failureCount++
  81. console.log(`${fail} ${statement}`)
  82. if (exitIfFail) process.exit(1)
  83. }
  84. }
  85. function assetsForVersion (version, validatingRelease) {
  86. const patterns = [
  87. `electron-${version}-darwin-x64-dsym.zip`,
  88. `electron-${version}-darwin-x64-symbols.zip`,
  89. `electron-${version}-darwin-x64.zip`,
  90. `electron-${version}-linux-arm64-symbols.zip`,
  91. `electron-${version}-linux-arm64.zip`,
  92. `electron-${version}-linux-armv7l-symbols.zip`,
  93. `electron-${version}-linux-armv7l.zip`,
  94. `electron-${version}-linux-ia32-symbols.zip`,
  95. `electron-${version}-linux-ia32.zip`,
  96. `electron-${version}-linux-x64-symbols.zip`,
  97. `electron-${version}-linux-x64.zip`,
  98. `electron-${version}-mas-x64-dsym.zip`,
  99. `electron-${version}-mas-x64-symbols.zip`,
  100. `electron-${version}-mas-x64.zip`,
  101. `electron-${version}-win32-ia32-pdb.zip`,
  102. `electron-${version}-win32-ia32-symbols.zip`,
  103. `electron-${version}-win32-ia32.zip`,
  104. `electron-${version}-win32-x64-pdb.zip`,
  105. `electron-${version}-win32-x64-symbols.zip`,
  106. `electron-${version}-win32-x64.zip`,
  107. `electron-api.json`,
  108. `electron.d.ts`,
  109. `ffmpeg-${version}-darwin-x64.zip`,
  110. `ffmpeg-${version}-linux-arm64.zip`,
  111. `ffmpeg-${version}-linux-armv7l.zip`,
  112. `ffmpeg-${version}-linux-ia32.zip`,
  113. `ffmpeg-${version}-linux-x64.zip`,
  114. `ffmpeg-${version}-mas-x64.zip`,
  115. `ffmpeg-${version}-win32-ia32.zip`,
  116. `ffmpeg-${version}-win32-x64.zip`
  117. ]
  118. if (!validatingRelease) {
  119. patterns.push('SHASUMS256.txt')
  120. }
  121. return patterns
  122. }
  123. function s3UrlsForVersion (version) {
  124. const bucket = `https://gh-contractor-zcbenz.s3.amazonaws.com/`
  125. const patterns = [
  126. `${bucket}atom-shell/dist/${version}/iojs-${version}-headers.tar.gz`,
  127. `${bucket}atom-shell/dist/${version}/iojs-${version}.tar.gz`,
  128. `${bucket}atom-shell/dist/${version}/node-${version}.tar.gz`,
  129. `${bucket}atom-shell/dist/${version}/node.lib`,
  130. `${bucket}atom-shell/dist/${version}/win-x64/iojs.lib`,
  131. `${bucket}atom-shell/dist/${version}/win-x86/iojs.lib`,
  132. `${bucket}atom-shell/dist/${version}/x64/node.lib`,
  133. `${bucket}atom-shell/dist/${version}/SHASUMS.txt`,
  134. `${bucket}atom-shell/dist/${version}/SHASUMS256.txt`,
  135. `${bucket}atom-shell/dist/index.json`
  136. ]
  137. return patterns
  138. }
  139. function checkVersion () {
  140. if (args.skipVersionCheck) return
  141. console.log(`Verifying that app version matches package version ${pkgVersion}.`)
  142. const startScript = path.join(__dirname, 'start.py')
  143. const scriptArgs = ['--version']
  144. if (args.automaticRelease) {
  145. scriptArgs.unshift('-R')
  146. }
  147. const appVersion = runScript(startScript, scriptArgs).trim()
  148. check((pkgVersion.indexOf(appVersion) === 0), `App version ${appVersion} matches ` +
  149. `package version ${pkgVersion}.`, true)
  150. }
  151. function runScript (scriptName, scriptArgs, cwd) {
  152. const scriptCommand = `${scriptName} ${scriptArgs.join(' ')}`
  153. const scriptOptions = {
  154. encoding: 'UTF-8'
  155. }
  156. if (cwd) {
  157. scriptOptions.cwd = cwd
  158. }
  159. try {
  160. return execSync(scriptCommand, scriptOptions)
  161. } catch (err) {
  162. console.log(`${fail} Error running ${scriptName}`, err)
  163. process.exit(1)
  164. }
  165. }
  166. function uploadNodeShasums () {
  167. console.log('Uploading Node SHASUMS file to S3.')
  168. const scriptPath = path.join(__dirname, 'upload-node-checksums.py')
  169. runScript(scriptPath, ['-v', pkgVersion])
  170. console.log(`${pass} Done uploading Node SHASUMS file to S3.`)
  171. }
  172. function uploadIndexJson () {
  173. console.log('Uploading index.json to S3.')
  174. const scriptPath = path.join(__dirname, 'upload-index-json.py')
  175. runScript(scriptPath, [pkgVersion])
  176. console.log(`${pass} Done uploading index.json to S3.`)
  177. }
  178. async function createReleaseShasums (release) {
  179. const fileName = 'SHASUMS256.txt'
  180. const existingAssets = release.assets.filter(asset => asset.name === fileName)
  181. if (existingAssets.length > 0) {
  182. console.log(`${fileName} already exists on GitHub; deleting before creating new file.`)
  183. await github.repos.deleteAsset({
  184. owner: 'electron',
  185. repo: targetRepo,
  186. id: existingAssets[0].id
  187. }).catch(err => {
  188. console.log(`${fail} Error deleting ${fileName} on GitHub:`, err)
  189. })
  190. }
  191. console.log(`Creating and uploading the release ${fileName}.`)
  192. const scriptPath = path.join(__dirname, 'merge-electron-checksums.py')
  193. const checksums = runScript(scriptPath, ['-v', pkgVersion])
  194. console.log(`${pass} Generated release SHASUMS.`)
  195. const filePath = await saveShaSumFile(checksums, fileName)
  196. console.log(`${pass} Created ${fileName} file.`)
  197. await uploadShasumFile(filePath, fileName, release)
  198. console.log(`${pass} Successfully uploaded ${fileName} to GitHub.`)
  199. }
  200. async function uploadShasumFile (filePath, fileName, release) {
  201. const githubOpts = {
  202. owner: 'electron',
  203. repo: targetRepo,
  204. id: release.id,
  205. filePath,
  206. name: fileName
  207. }
  208. return github.repos.uploadAsset(githubOpts)
  209. .catch(err => {
  210. console.log(`${fail} Error uploading ${filePath} to GitHub:`, err)
  211. process.exit(1)
  212. })
  213. }
  214. function saveShaSumFile (checksums, fileName) {
  215. return new Promise((resolve, reject) => {
  216. temp.open(fileName, (err, info) => {
  217. if (err) {
  218. console.log(`${fail} Could not create ${fileName} file`)
  219. process.exit(1)
  220. } else {
  221. fs.writeFileSync(info.fd, checksums)
  222. fs.close(info.fd, (err) => {
  223. if (err) {
  224. console.log(`${fail} Could close ${fileName} file`)
  225. process.exit(1)
  226. }
  227. resolve(info.path)
  228. })
  229. }
  230. })
  231. })
  232. }
  233. async function publishRelease (release) {
  234. const githubOpts = {
  235. owner: 'electron',
  236. repo: targetRepo,
  237. id: release.id,
  238. tag_name: release.tag_name,
  239. draft: false
  240. }
  241. return github.repos.editRelease(githubOpts)
  242. .catch(err => {
  243. console.log(`${fail} Error publishing release:`, err)
  244. process.exit(1)
  245. })
  246. }
  247. async function makeRelease (releaseToValidate) {
  248. if (releaseToValidate) {
  249. if (releaseToValidate === true) {
  250. releaseToValidate = pkgVersion
  251. } else {
  252. console.log('Release to validate !=== true')
  253. }
  254. console.log(`Validating release ${releaseToValidate}`)
  255. const release = await getDraftRelease(releaseToValidate)
  256. await validateReleaseAssets(release, true)
  257. } else {
  258. checkVersion()
  259. let draftRelease = await getDraftRelease()
  260. uploadNodeShasums()
  261. uploadIndexJson()
  262. await createReleaseShasums(draftRelease)
  263. // Fetch latest version of release before verifying
  264. draftRelease = await getDraftRelease(pkgVersion, true)
  265. await validateReleaseAssets(draftRelease)
  266. await publishRelease(draftRelease)
  267. console.log(`${pass} SUCCESS!!! Release has been published. Please run ` +
  268. `"npm run publish-to-npm" to publish release to npm.`)
  269. }
  270. }
  271. async function makeTempDir () {
  272. return new Promise((resolve, reject) => {
  273. temp.mkdir('electron-publish', (err, dirPath) => {
  274. if (err) {
  275. reject(err)
  276. } else {
  277. resolve(dirPath)
  278. }
  279. })
  280. })
  281. }
  282. async function verifyAssets (release) {
  283. const downloadDir = await makeTempDir()
  284. const githubOpts = {
  285. owner: 'electron',
  286. repo: targetRepo,
  287. headers: {
  288. Accept: 'application/octet-stream'
  289. }
  290. }
  291. console.log(`Downloading files from GitHub to verify shasums`)
  292. const shaSumFile = 'SHASUMS256.txt'
  293. let filesToCheck = await Promise.all(release.assets.map(async (asset) => {
  294. githubOpts.id = asset.id
  295. const assetDetails = await github.repos.getAsset(githubOpts)
  296. await downloadFiles(assetDetails.meta.location, downloadDir, asset.name)
  297. return asset.name
  298. })).catch(err => {
  299. console.log(`${fail} Error downloading files from GitHub`, err)
  300. process.exit(1)
  301. })
  302. filesToCheck = filesToCheck.filter(fileName => fileName !== shaSumFile)
  303. let checkerOpts
  304. await validateChecksums({
  305. algorithm: 'sha256',
  306. filesToCheck,
  307. fileDirectory: downloadDir,
  308. shaSumFile,
  309. checkerOpts,
  310. fileSource: 'GitHub'
  311. })
  312. }
  313. function downloadFiles (urls, directory, targetName) {
  314. return new Promise((resolve, reject) => {
  315. const nuggetOpts = { dir: directory }
  316. nuggetOpts.quiet = !args.verboseNugget
  317. if (targetName) nuggetOpts.target = targetName
  318. nugget(urls, nuggetOpts, (err) => {
  319. if (err) {
  320. reject(err)
  321. } else {
  322. console.log(`${pass} all files downloaded successfully!`)
  323. resolve()
  324. }
  325. })
  326. })
  327. }
  328. async function verifyShasums (urls, isS3) {
  329. const fileSource = isS3 ? 'S3' : 'GitHub'
  330. console.log(`Downloading files from ${fileSource} to verify shasums`)
  331. const downloadDir = await makeTempDir()
  332. let filesToCheck = []
  333. try {
  334. if (!isS3) {
  335. await downloadFiles(urls, downloadDir)
  336. filesToCheck = urls.map(url => {
  337. const currentUrl = new URL(url)
  338. return path.basename(currentUrl.pathname)
  339. }).filter(file => file.indexOf('SHASUMS') === -1)
  340. } else {
  341. const s3VersionPath = `/atom-shell/dist/${pkgVersion}/`
  342. await Promise.all(urls.map(async (url) => {
  343. const currentUrl = new URL(url)
  344. const dirname = path.dirname(currentUrl.pathname)
  345. const filename = path.basename(currentUrl.pathname)
  346. const s3VersionPathIdx = dirname.indexOf(s3VersionPath)
  347. if (s3VersionPathIdx === -1 || dirname === s3VersionPath) {
  348. if (s3VersionPathIdx !== -1 && filename.indexof('SHASUMS') === -1) {
  349. filesToCheck.push(filename)
  350. }
  351. await downloadFiles(url, downloadDir)
  352. } else {
  353. const subDirectory = dirname.substr(s3VersionPathIdx + s3VersionPath.length)
  354. const fileDirectory = path.join(downloadDir, subDirectory)
  355. try {
  356. fs.statSync(fileDirectory)
  357. } catch (err) {
  358. fs.mkdirSync(fileDirectory)
  359. }
  360. filesToCheck.push(path.join(subDirectory, filename))
  361. await downloadFiles(url, fileDirectory)
  362. }
  363. }))
  364. }
  365. } catch (err) {
  366. console.log(`${fail} Error downloading files from ${fileSource}`, err)
  367. process.exit(1)
  368. }
  369. console.log(`${pass} Successfully downloaded the files from ${fileSource}.`)
  370. let checkerOpts
  371. if (isS3) {
  372. checkerOpts = { defaultTextEncoding: 'binary' }
  373. }
  374. await validateChecksums({
  375. algorithm: 'sha256',
  376. filesToCheck,
  377. fileDirectory: downloadDir,
  378. shaSumFile: 'SHASUMS256.txt',
  379. checkerOpts,
  380. fileSource
  381. })
  382. if (isS3) {
  383. await validateChecksums({
  384. algorithm: 'sha1',
  385. filesToCheck,
  386. fileDirectory: downloadDir,
  387. shaSumFile: 'SHASUMS.txt',
  388. checkerOpts,
  389. fileSource
  390. })
  391. }
  392. }
  393. async function validateChecksums (validationArgs) {
  394. console.log(`Validating checksums for files from ${validationArgs.fileSource} ` +
  395. `against ${validationArgs.shaSumFile}.`)
  396. const shaSumFilePath = path.join(validationArgs.fileDirectory, validationArgs.shaSumFile)
  397. const checker = new sumchecker.ChecksumValidator(validationArgs.algorithm,
  398. shaSumFilePath, validationArgs.checkerOpts)
  399. await checker.validate(validationArgs.fileDirectory, validationArgs.filesToCheck)
  400. .catch(err => {
  401. if (err instanceof sumchecker.ChecksumMismatchError) {
  402. console.error(`${fail} The checksum of ${err.filename} from ` +
  403. `${validationArgs.fileSource} did not match the shasum in ` +
  404. `${validationArgs.shaSumFile}`)
  405. } else if (err instanceof sumchecker.ChecksumParseError) {
  406. console.error(`${fail} The checksum file ${validationArgs.shaSumFile} ` +
  407. `from ${validationArgs.fileSource} could not be parsed.`, err)
  408. } else if (err instanceof sumchecker.NoChecksumFoundError) {
  409. console.error(`${fail} The file ${err.filename} from ` +
  410. `${validationArgs.fileSource} was not in the shasum file ` +
  411. `${validationArgs.shaSumFile}.`)
  412. } else {
  413. console.error(`${fail} Error matching files from ` +
  414. `${validationArgs.fileSource} shasums in ${validationArgs.shaSumFile}.`, err)
  415. }
  416. process.exit(1)
  417. })
  418. console.log(`${pass} All files from ${validationArgs.fileSource} match ` +
  419. `shasums defined in ${validationArgs.shaSumFile}.`)
  420. }
  421. makeRelease(args.validateRelease)