release.js 15 KB

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