release.js 16 KB

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