release.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464
  1. #!/usr/bin/env node
  2. if (!process.env.CI) require('dotenv-safe').load();
  3. const args = require('minimist')(process.argv.slice(2), {
  4. boolean: [
  5. 'validateRelease',
  6. 'verboseNugget'
  7. ],
  8. default: { verboseNugget: false }
  9. });
  10. const fs = require('fs');
  11. const { execSync } = require('child_process');
  12. const got = require('got');
  13. const pkg = require('../../package.json');
  14. const pkgVersion = `v${pkg.version}`;
  15. const path = require('path');
  16. const temp = require('temp').track();
  17. const { URL } = require('url');
  18. const { Octokit } = require('@octokit/rest');
  19. const AWS = require('aws-sdk');
  20. require('colors');
  21. const pass = '✓'.green;
  22. const fail = '✗'.red;
  23. const { ELECTRON_DIR } = require('../lib/utils');
  24. const getUrlHash = require('./get-url-hash');
  25. const octokit = new Octokit({
  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 => ({ url: asset.browser_download_url, file: asset.name })).sort((a, b) => a.file.localeCompare(b.file));
  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 verifyDraftGitHubReleaseAssets(release);
  63. } else {
  64. await verifyShasumsForRemoteFiles(downloadUrls)
  65. .catch(err => {
  66. console.log(`${fail} error verifyingShasums`, err);
  67. });
  68. }
  69. const s3RemoteFiles = s3RemoteFilesForVersion(release.tag_name);
  70. await verifyShasumsForRemoteFiles(s3RemoteFiles, 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}-darwin-arm64.zip`,
  86. `chromedriver-${version}-linux-arm64.zip`,
  87. `chromedriver-${version}-linux-armv7l.zip`,
  88. `chromedriver-${version}-linux-ia32.zip`,
  89. `chromedriver-${version}-linux-x64.zip`,
  90. `chromedriver-${version}-mas-x64.zip`,
  91. `chromedriver-${version}-mas-arm64.zip`,
  92. `chromedriver-${version}-win32-ia32.zip`,
  93. `chromedriver-${version}-win32-x64.zip`,
  94. `chromedriver-${version}-win32-arm64.zip`,
  95. `electron-${version}-darwin-x64-dsym.zip`,
  96. `electron-${version}-darwin-x64-symbols.zip`,
  97. `electron-${version}-darwin-x64.zip`,
  98. `electron-${version}-darwin-arm64-dsym.zip`,
  99. `electron-${version}-darwin-arm64-symbols.zip`,
  100. `electron-${version}-darwin-arm64.zip`,
  101. `electron-${version}-linux-arm64-symbols.zip`,
  102. `electron-${version}-linux-arm64.zip`,
  103. `electron-${version}-linux-armv7l-symbols.zip`,
  104. `electron-${version}-linux-armv7l.zip`,
  105. `electron-${version}-linux-ia32-symbols.zip`,
  106. `electron-${version}-linux-ia32.zip`,
  107. `electron-${version}-linux-x64-debug.zip`,
  108. `electron-${version}-linux-x64-symbols.zip`,
  109. `electron-${version}-linux-x64.zip`,
  110. `electron-${version}-mas-x64-dsym.zip`,
  111. `electron-${version}-mas-x64-symbols.zip`,
  112. `electron-${version}-mas-x64.zip`,
  113. `electron-${version}-mas-arm64-dsym.zip`,
  114. `electron-${version}-mas-arm64-symbols.zip`,
  115. `electron-${version}-mas-arm64.zip`,
  116. `electron-${version}-win32-ia32-pdb.zip`,
  117. `electron-${version}-win32-ia32-symbols.zip`,
  118. `electron-${version}-win32-ia32.zip`,
  119. `electron-${version}-win32-x64-pdb.zip`,
  120. `electron-${version}-win32-x64-symbols.zip`,
  121. `electron-${version}-win32-x64.zip`,
  122. `electron-${version}-win32-arm64-pdb.zip`,
  123. `electron-${version}-win32-arm64-symbols.zip`,
  124. `electron-${version}-win32-arm64.zip`,
  125. 'electron-api.json',
  126. 'electron.d.ts',
  127. 'hunspell_dictionaries.zip',
  128. 'libcxx_headers.zip',
  129. 'libcxxabi_headers.zip',
  130. `libcxx-objects-${version}-linux-arm64.zip`,
  131. `libcxx-objects-${version}-linux-armv7l.zip`,
  132. `libcxx-objects-${version}-linux-ia32.zip`,
  133. `libcxx-objects-${version}-linux-x64.zip`,
  134. `ffmpeg-${version}-darwin-x64.zip`,
  135. `ffmpeg-${version}-darwin-arm64.zip`,
  136. `ffmpeg-${version}-linux-arm64.zip`,
  137. `ffmpeg-${version}-linux-armv7l.zip`,
  138. `ffmpeg-${version}-linux-ia32.zip`,
  139. `ffmpeg-${version}-linux-x64.zip`,
  140. `ffmpeg-${version}-mas-x64.zip`,
  141. `ffmpeg-${version}-mas-arm64.zip`,
  142. `ffmpeg-${version}-win32-ia32.zip`,
  143. `ffmpeg-${version}-win32-x64.zip`,
  144. `ffmpeg-${version}-win32-arm64.zip`,
  145. `mksnapshot-${version}-darwin-x64.zip`,
  146. `mksnapshot-${version}-darwin-arm64.zip`,
  147. `mksnapshot-${version}-linux-arm64-x64.zip`,
  148. `mksnapshot-${version}-linux-armv7l-x64.zip`,
  149. `mksnapshot-${version}-linux-ia32.zip`,
  150. `mksnapshot-${version}-linux-x64.zip`,
  151. `mksnapshot-${version}-mas-x64.zip`,
  152. `mksnapshot-${version}-mas-arm64.zip`,
  153. `mksnapshot-${version}-win32-ia32.zip`,
  154. `mksnapshot-${version}-win32-x64.zip`,
  155. `mksnapshot-${version}-win32-arm64-x64.zip`,
  156. `electron-${version}-win32-ia32-toolchain-profile.zip`,
  157. `electron-${version}-win32-x64-toolchain-profile.zip`,
  158. `electron-${version}-win32-arm64-toolchain-profile.zip`
  159. ];
  160. if (!validatingRelease) {
  161. patterns.push('SHASUMS256.txt');
  162. }
  163. return patterns;
  164. }
  165. function s3RemoteFilesForVersion (version) {
  166. const bucket = 'https://gh-contractor-zcbenz.s3.amazonaws.com/';
  167. const versionPrefix = `${bucket}atom-shell/dist/${version}/`;
  168. const filePaths = [
  169. `iojs-${version}-headers.tar.gz`,
  170. `iojs-${version}.tar.gz`,
  171. `node-${version}.tar.gz`,
  172. 'node.lib',
  173. 'x64/node.lib',
  174. 'win-x64/iojs.lib',
  175. 'win-x86/iojs.lib',
  176. 'win-arm64/iojs.lib',
  177. 'win-x64/node.lib',
  178. 'win-x86/node.lib',
  179. 'win-arm64/node.lib',
  180. 'arm64/node.lib',
  181. 'SHASUMS.txt',
  182. 'SHASUMS256.txt'
  183. ];
  184. return filePaths.map((filePath) => ({
  185. file: filePath,
  186. url: `${versionPrefix}${filePath}`
  187. }));
  188. }
  189. function runScript (scriptName, scriptArgs, cwd) {
  190. const scriptCommand = `${scriptName} ${scriptArgs.join(' ')}`;
  191. const scriptOptions = {
  192. encoding: 'UTF-8'
  193. };
  194. if (cwd) scriptOptions.cwd = cwd;
  195. try {
  196. return execSync(scriptCommand, scriptOptions);
  197. } catch (err) {
  198. console.log(`${fail} Error running ${scriptName}`, err);
  199. process.exit(1);
  200. }
  201. }
  202. function uploadNodeShasums () {
  203. console.log('Uploading Node SHASUMS file to S3.');
  204. const scriptPath = path.join(ELECTRON_DIR, 'script', 'release', 'uploaders', 'upload-node-checksums.py');
  205. runScript(scriptPath, ['-v', pkgVersion]);
  206. console.log(`${pass} Done uploading Node SHASUMS file to S3.`);
  207. }
  208. function uploadIndexJson () {
  209. console.log('Uploading index.json to S3.');
  210. const scriptPath = path.join(ELECTRON_DIR, 'script', 'release', 'uploaders', 'upload-index-json.py');
  211. runScript(scriptPath, [pkgVersion]);
  212. console.log(`${pass} Done uploading index.json to S3.`);
  213. }
  214. async function mergeShasums (pkgVersion) {
  215. // Download individual checksum files for Electron zip files from S3,
  216. // concatenate them, and upload to GitHub.
  217. const bucket = process.env.ELECTRON_S3_BUCKET;
  218. const accessKeyId = process.env.ELECTRON_S3_ACCESS_KEY;
  219. const secretAccessKey = process.env.ELECTRON_S3_SECRET_KEY;
  220. if (!bucket || !accessKeyId || !secretAccessKey) {
  221. throw new Error('Please set the $ELECTRON_S3_BUCKET, $ELECTRON_S3_ACCESS_KEY, and $ELECTRON_S3_SECRET_KEY environment variables');
  222. }
  223. const s3 = new AWS.S3({
  224. apiVersion: '2006-03-01',
  225. accessKeyId,
  226. secretAccessKey,
  227. region: 'us-west-2'
  228. });
  229. const objects = await s3.listObjectsV2({
  230. Bucket: bucket,
  231. Prefix: `atom-shell/tmp/${pkgVersion}/`,
  232. Delimiter: '/'
  233. }).promise();
  234. const shasums = [];
  235. for (const obj of objects.Contents) {
  236. if (obj.Key.endsWith('.sha256sum')) {
  237. const data = await s3.getObject({
  238. Bucket: bucket,
  239. Key: obj.Key
  240. }).promise();
  241. shasums.push(data.Body.toString('ascii').trim());
  242. }
  243. }
  244. return shasums.join('\n');
  245. }
  246. async function createReleaseShasums (release) {
  247. const fileName = 'SHASUMS256.txt';
  248. const existingAssets = release.assets.filter(asset => asset.name === fileName);
  249. if (existingAssets.length > 0) {
  250. console.log(`${fileName} already exists on GitHub; deleting before creating new file.`);
  251. await octokit.repos.deleteReleaseAsset({
  252. owner: 'electron',
  253. repo: targetRepo,
  254. asset_id: existingAssets[0].id
  255. }).catch(err => {
  256. console.log(`${fail} Error deleting ${fileName} on GitHub:`, err);
  257. });
  258. }
  259. console.log(`Creating and uploading the release ${fileName}.`);
  260. const checksums = await mergeShasums(pkgVersion);
  261. console.log(`${pass} Generated release SHASUMS.`);
  262. const filePath = await saveShaSumFile(checksums, fileName);
  263. console.log(`${pass} Created ${fileName} file.`);
  264. await uploadShasumFile(filePath, fileName, release.id);
  265. console.log(`${pass} Successfully uploaded ${fileName} to GitHub.`);
  266. }
  267. async function uploadShasumFile (filePath, fileName, releaseId) {
  268. const uploadUrl = `https://uploads.github.com/repos/electron/${targetRepo}/releases/${releaseId}/assets{?name,label}`;
  269. return octokit.repos.uploadReleaseAsset({
  270. url: uploadUrl,
  271. headers: {
  272. 'content-type': 'text/plain',
  273. 'content-length': fs.statSync(filePath).size
  274. },
  275. data: fs.createReadStream(filePath),
  276. name: fileName
  277. }).catch(err => {
  278. console.log(`${fail} Error uploading ${filePath} to GitHub:`, err);
  279. process.exit(1);
  280. });
  281. }
  282. function saveShaSumFile (checksums, fileName) {
  283. return new Promise((resolve, reject) => {
  284. temp.open(fileName, (err, info) => {
  285. if (err) {
  286. console.log(`${fail} Could not create ${fileName} file`);
  287. process.exit(1);
  288. } else {
  289. fs.writeFileSync(info.fd, checksums);
  290. fs.close(info.fd, (err) => {
  291. if (err) {
  292. console.log(`${fail} Could close ${fileName} file`);
  293. process.exit(1);
  294. }
  295. resolve(info.path);
  296. });
  297. }
  298. });
  299. });
  300. }
  301. async function publishRelease (release) {
  302. return octokit.repos.updateRelease({
  303. owner: 'electron',
  304. repo: targetRepo,
  305. release_id: release.id,
  306. tag_name: release.tag_name,
  307. draft: false
  308. }).catch(err => {
  309. console.log(`${fail} Error publishing release:`, err);
  310. process.exit(1);
  311. });
  312. }
  313. async function makeRelease (releaseToValidate) {
  314. if (releaseToValidate) {
  315. if (releaseToValidate === true) {
  316. releaseToValidate = pkgVersion;
  317. } else {
  318. console.log('Release to validate !=== true');
  319. }
  320. console.log(`Validating release ${releaseToValidate}`);
  321. const release = await getDraftRelease(releaseToValidate);
  322. await validateReleaseAssets(release, true);
  323. } else {
  324. let draftRelease = await getDraftRelease();
  325. uploadNodeShasums();
  326. uploadIndexJson();
  327. await createReleaseShasums(draftRelease);
  328. // Fetch latest version of release before verifying
  329. draftRelease = await getDraftRelease(pkgVersion, true);
  330. await validateReleaseAssets(draftRelease);
  331. await publishRelease(draftRelease);
  332. console.log(`${pass} SUCCESS!!! Release has been published. Please run ` +
  333. '"npm run publish-to-npm" to publish release to npm.');
  334. }
  335. }
  336. async function makeTempDir () {
  337. return new Promise((resolve, reject) => {
  338. temp.mkdir('electron-publish', (err, dirPath) => {
  339. if (err) {
  340. reject(err);
  341. } else {
  342. resolve(dirPath);
  343. }
  344. });
  345. });
  346. }
  347. const SHASUM_256_FILENAME = 'SHASUMS256.txt';
  348. const SHASUM_1_FILENAME = 'SHASUMS.txt';
  349. async function verifyDraftGitHubReleaseAssets (release) {
  350. console.log('Fetching authenticated GitHub artifact URLs to verify shasums');
  351. const remoteFilesToHash = await Promise.all(release.assets.map(async asset => {
  352. const requestOptions = await octokit.repos.getReleaseAsset.endpoint({
  353. owner: 'electron',
  354. repo: targetRepo,
  355. asset_id: asset.id,
  356. headers: {
  357. Accept: 'application/octet-stream'
  358. }
  359. });
  360. const { url, headers } = requestOptions;
  361. headers.authorization = `token ${process.env.ELECTRON_GITHUB_TOKEN}`;
  362. const response = await got(url, {
  363. followRedirect: false,
  364. method: 'HEAD',
  365. headers
  366. });
  367. return { url: response.headers.location, file: asset.name };
  368. })).catch(err => {
  369. console.log(`${fail} Error downloading files from GitHub`, err);
  370. process.exit(1);
  371. });
  372. await verifyShasumsForRemoteFiles(remoteFilesToHash);
  373. }
  374. async function getShaSumMappingFromUrl (shaSumFileUrl, fileNamePrefix) {
  375. const response = await got(shaSumFileUrl);
  376. const raw = response.body;
  377. return raw.split('\n').map(line => line.trim()).filter(Boolean).reduce((map, line) => {
  378. const [sha, file] = line.replace(' ', ' ').split(' ');
  379. map[file.slice(fileNamePrefix.length)] = sha;
  380. return map;
  381. }, {});
  382. }
  383. async function validateFileHashesAgainstShaSumMapping (remoteFilesWithHashes, mapping) {
  384. for (const remoteFileWithHash of remoteFilesWithHashes) {
  385. check(remoteFileWithHash.hash === mapping[remoteFileWithHash.file], `Release asset ${remoteFileWithHash.file} should have hash of ${mapping[remoteFileWithHash.file]} but found ${remoteFileWithHash.hash}`, true);
  386. }
  387. }
  388. async function verifyShasumsForRemoteFiles (remoteFilesToHash, filesAreNodeJSArtifacts = false) {
  389. console.log(`Generating SHAs for ${remoteFilesToHash.length} files to verify shasums`);
  390. // Only used for node.js artifact uploads
  391. const shaSum1File = remoteFilesToHash.find(({ file }) => file === SHASUM_1_FILENAME);
  392. // Used for both node.js artifact uploads and normal electron artifacts
  393. const shaSum256File = remoteFilesToHash.find(({ file }) => file === SHASUM_256_FILENAME);
  394. remoteFilesToHash = remoteFilesToHash.filter(({ file }) => file !== SHASUM_1_FILENAME && file !== SHASUM_256_FILENAME);
  395. const remoteFilesWithHashes = await Promise.all(remoteFilesToHash.map(async (file) => {
  396. return {
  397. hash: await getUrlHash(file.url, 'sha256'),
  398. ...file
  399. };
  400. }));
  401. await validateFileHashesAgainstShaSumMapping(remoteFilesWithHashes, await getShaSumMappingFromUrl(shaSum256File.url, filesAreNodeJSArtifacts ? '' : '*'));
  402. if (filesAreNodeJSArtifacts) {
  403. const remoteFilesWithSha1Hashes = await Promise.all(remoteFilesToHash.map(async (file) => {
  404. return {
  405. hash: await getUrlHash(file.url, 'sha1'),
  406. ...file
  407. };
  408. }));
  409. await validateFileHashesAgainstShaSumMapping(remoteFilesWithSha1Hashes, await getShaSumMappingFromUrl(shaSum1File.url, filesAreNodeJSArtifacts ? '' : '*'));
  410. }
  411. }
  412. makeRelease(args.validateRelease)
  413. .catch((err) => {
  414. console.error('Error occurred while making release:', err);
  415. process.exit(1);
  416. });