#!/usr/bin/env node if (!process.env.CI) require('dotenv-safe').load(); const args = require('minimist')(process.argv.slice(2), { boolean: [ 'validateRelease', 'skipVersionCheck', 'automaticRelease', 'verboseNugget' ], default: { verboseNugget: false } }); const fs = require('fs'); const { execSync } = require('child_process'); const nugget = require('nugget'); const got = require('got'); const pkg = require('../../package.json'); const pkgVersion = `v${pkg.version}`; const path = require('path'); const sumchecker = require('sumchecker'); const temp = require('temp').track(); const { URL } = require('url'); const { Octokit } = require('@octokit/rest'); const AWS = require('aws-sdk'); require('colors'); const pass = '✓'.green; const fail = '✗'.red; const { ELECTRON_DIR } = require('../lib/utils'); const octokit = new Octokit({ auth: process.env.ELECTRON_GITHUB_TOKEN }); const targetRepo = pkgVersion.indexOf('nightly') > 0 ? 'nightlies' : 'electron'; let failureCount = 0; async function getDraftRelease (version, skipValidation) { const releaseInfo = await octokit.repos.listReleases({ owner: 'electron', repo: targetRepo }); const versionToCheck = version || pkgVersion; const drafts = releaseInfo.data.filter(release => { return release.tag_name === versionToCheck && release.draft === true; }); const draft = drafts[0]; if (!skipValidation) { failureCount = 0; check(drafts.length === 1, 'one draft exists', true); if (versionToCheck.indexOf('beta') > -1) { check(draft.prerelease, 'draft is a prerelease'); } check(draft.body.length > 50 && !draft.body.includes('(placeholder)'), 'draft has release notes'); check((failureCount === 0), 'Draft release looks good to go.', true); } return draft; } async function validateReleaseAssets (release, validatingRelease) { const requiredAssets = assetsForVersion(release.tag_name, validatingRelease).sort(); const extantAssets = release.assets.map(asset => asset.name).sort(); const downloadUrls = release.assets.map(asset => asset.browser_download_url).sort(); failureCount = 0; requiredAssets.forEach(asset => { check(extantAssets.includes(asset), asset); }); check((failureCount === 0), 'All required GitHub assets exist for release', true); if (!validatingRelease || !release.draft) { if (release.draft) { await verifyAssets(release); } else { await verifyShasums(downloadUrls) .catch(err => { console.log(`${fail} error verifyingShasums`, err); }); } const s3Urls = s3UrlsForVersion(release.tag_name); await verifyShasums(s3Urls, true); } } function check (condition, statement, exitIfFail = false) { if (condition) { console.log(`${pass} ${statement}`); } else { failureCount++; console.log(`${fail} ${statement}`); if (exitIfFail) process.exit(1); } } function assetsForVersion (version, validatingRelease) { const patterns = [ `chromedriver-${version}-darwin-x64.zip`, `chromedriver-${version}-darwin-arm64.zip`, `chromedriver-${version}-linux-arm64.zip`, `chromedriver-${version}-linux-armv7l.zip`, `chromedriver-${version}-linux-ia32.zip`, `chromedriver-${version}-linux-x64.zip`, `chromedriver-${version}-mas-x64.zip`, `chromedriver-${version}-mas-arm64.zip`, `chromedriver-${version}-win32-ia32.zip`, `chromedriver-${version}-win32-x64.zip`, `chromedriver-${version}-win32-arm64.zip`, `electron-${version}-darwin-x64-dsym.zip`, `electron-${version}-darwin-x64-symbols.zip`, `electron-${version}-darwin-x64.zip`, `electron-${version}-darwin-arm64-dsym.zip`, `electron-${version}-darwin-arm64-symbols.zip`, `electron-${version}-darwin-arm64.zip`, `electron-${version}-linux-arm64-symbols.zip`, `electron-${version}-linux-arm64.zip`, `electron-${version}-linux-armv7l-symbols.zip`, `electron-${version}-linux-armv7l.zip`, `electron-${version}-linux-ia32-symbols.zip`, `electron-${version}-linux-ia32.zip`, `electron-${version}-linux-x64-debug.zip`, `electron-${version}-linux-x64-symbols.zip`, `electron-${version}-linux-x64.zip`, `electron-${version}-mas-x64-dsym.zip`, `electron-${version}-mas-x64-symbols.zip`, `electron-${version}-mas-x64.zip`, `electron-${version}-mas-arm64-dsym.zip`, `electron-${version}-mas-arm64-symbols.zip`, `electron-${version}-mas-arm64.zip`, `electron-${version}-win32-ia32-pdb.zip`, `electron-${version}-win32-ia32-symbols.zip`, `electron-${version}-win32-ia32.zip`, `electron-${version}-win32-x64-pdb.zip`, `electron-${version}-win32-x64-symbols.zip`, `electron-${version}-win32-x64.zip`, `electron-${version}-win32-arm64-pdb.zip`, `electron-${version}-win32-arm64-symbols.zip`, `electron-${version}-win32-arm64.zip`, 'electron-api.json', 'electron.d.ts', 'hunspell_dictionaries.zip', `ffmpeg-${version}-darwin-x64.zip`, `ffmpeg-${version}-darwin-arm64.zip`, `ffmpeg-${version}-linux-arm64.zip`, `ffmpeg-${version}-linux-armv7l.zip`, `ffmpeg-${version}-linux-ia32.zip`, `ffmpeg-${version}-linux-x64.zip`, `ffmpeg-${version}-mas-x64.zip`, `ffmpeg-${version}-mas-arm64.zip`, `ffmpeg-${version}-win32-ia32.zip`, `ffmpeg-${version}-win32-x64.zip`, `ffmpeg-${version}-win32-arm64.zip`, `mksnapshot-${version}-darwin-x64.zip`, `mksnapshot-${version}-darwin-arm64.zip`, `mksnapshot-${version}-linux-arm64-x64.zip`, `mksnapshot-${version}-linux-armv7l-x64.zip`, `mksnapshot-${version}-linux-ia32.zip`, `mksnapshot-${version}-linux-x64.zip`, `mksnapshot-${version}-mas-x64.zip`, `mksnapshot-${version}-mas-arm64.zip`, `mksnapshot-${version}-win32-ia32.zip`, `mksnapshot-${version}-win32-x64.zip`, `mksnapshot-${version}-win32-arm64-x64.zip`, `electron-${version}-win32-ia32-toolchain-profile.zip`, `electron-${version}-win32-x64-toolchain-profile.zip`, `electron-${version}-win32-arm64-toolchain-profile.zip` ]; if (!validatingRelease) { patterns.push('SHASUMS256.txt'); } return patterns; } function s3UrlsForVersion (version) { const bucket = 'https://gh-contractor-zcbenz.s3.amazonaws.com/'; const patterns = [ `${bucket}atom-shell/dist/${version}/iojs-${version}-headers.tar.gz`, `${bucket}atom-shell/dist/${version}/iojs-${version}.tar.gz`, `${bucket}atom-shell/dist/${version}/node-${version}.tar.gz`, `${bucket}atom-shell/dist/${version}/node.lib`, `${bucket}atom-shell/dist/${version}/win-x64/iojs.lib`, `${bucket}atom-shell/dist/${version}/win-x86/iojs.lib`, `${bucket}atom-shell/dist/${version}/x64/node.lib`, `${bucket}atom-shell/dist/${version}/SHASUMS.txt`, `${bucket}atom-shell/dist/${version}/SHASUMS256.txt`, `${bucket}atom-shell/dist/index.json` ]; return patterns; } function runScript (scriptName, scriptArgs, cwd) { const scriptCommand = `${scriptName} ${scriptArgs.join(' ')}`; const scriptOptions = { encoding: 'UTF-8' }; if (cwd) scriptOptions.cwd = cwd; try { return execSync(scriptCommand, scriptOptions); } catch (err) { console.log(`${fail} Error running ${scriptName}`, err); process.exit(1); } } function uploadNodeShasums () { console.log('Uploading Node SHASUMS file to S3.'); const scriptPath = path.join(ELECTRON_DIR, 'script', 'release', 'uploaders', 'upload-node-checksums.py'); runScript(scriptPath, ['-v', pkgVersion]); console.log(`${pass} Done uploading Node SHASUMS file to S3.`); } function uploadIndexJson () { console.log('Uploading index.json to S3.'); const scriptPath = path.join(ELECTRON_DIR, 'script', 'release', 'uploaders', 'upload-index-json.py'); runScript(scriptPath, [pkgVersion]); console.log(`${pass} Done uploading index.json to S3.`); } async function mergeShasums (pkgVersion) { // Download individual checksum files for Electron zip files from S3, // concatenate them, and upload to GitHub. const bucket = process.env.ELECTRON_S3_BUCKET; const accessKeyId = process.env.ELECTRON_S3_ACCESS_KEY; const secretAccessKey = process.env.ELECTRON_S3_SECRET_KEY; if (!bucket || !accessKeyId || !secretAccessKey) { throw new Error('Please set the $ELECTRON_S3_BUCKET, $ELECTRON_S3_ACCESS_KEY, and $ELECTRON_S3_SECRET_KEY environment variables'); } const s3 = new AWS.S3({ apiVersion: '2006-03-01', accessKeyId, secretAccessKey, region: 'us-west-2' }); const objects = await s3.listObjectsV2({ Bucket: bucket, Prefix: `atom-shell/tmp/${pkgVersion}/`, Delimiter: '/' }).promise(); const shasums = []; for (const obj of objects.Contents) { if (obj.Key.endsWith('.sha256sum')) { const data = await s3.getObject({ Bucket: bucket, Key: obj.Key }).promise(); shasums.push(data.toString('ascii').trim()); } } return shasums.join('\n'); } async function createReleaseShasums (release) { const fileName = 'SHASUMS256.txt'; const existingAssets = release.assets.filter(asset => asset.name === fileName); if (existingAssets.length > 0) { console.log(`${fileName} already exists on GitHub; deleting before creating new file.`); await octokit.repos.deleteReleaseAsset({ owner: 'electron', repo: targetRepo, asset_id: existingAssets[0].id }).catch(err => { console.log(`${fail} Error deleting ${fileName} on GitHub:`, err); }); } console.log(`Creating and uploading the release ${fileName}.`); const checksums = await mergeShasums(pkgVersion); console.log(`${pass} Generated release SHASUMS.`); const filePath = await saveShaSumFile(checksums, fileName); console.log(`${pass} Created ${fileName} file.`); await uploadShasumFile(filePath, fileName, release.id); console.log(`${pass} Successfully uploaded ${fileName} to GitHub.`); } async function uploadShasumFile (filePath, fileName, releaseId) { const uploadUrl = `https://uploads.github.com/repos/electron/${targetRepo}/releases/${releaseId}/assets{?name,label}`; return octokit.repos.uploadReleaseAsset({ url: uploadUrl, headers: { 'content-type': 'text/plain', 'content-length': fs.statSync(filePath).size }, data: fs.createReadStream(filePath), name: fileName }).catch(err => { console.log(`${fail} Error uploading ${filePath} to GitHub:`, err); process.exit(1); }); } function saveShaSumFile (checksums, fileName) { return new Promise((resolve, reject) => { temp.open(fileName, (err, info) => { if (err) { console.log(`${fail} Could not create ${fileName} file`); process.exit(1); } else { fs.writeFileSync(info.fd, checksums); fs.close(info.fd, (err) => { if (err) { console.log(`${fail} Could close ${fileName} file`); process.exit(1); } resolve(info.path); }); } }); }); } async function publishRelease (release) { return octokit.repos.updateRelease({ owner: 'electron', repo: targetRepo, release_id: release.id, tag_name: release.tag_name, draft: false }).catch(err => { console.log(`${fail} Error publishing release:`, err); process.exit(1); }); } async function makeRelease (releaseToValidate) { if (releaseToValidate) { if (releaseToValidate === true) { releaseToValidate = pkgVersion; } else { console.log('Release to validate !=== true'); } console.log(`Validating release ${releaseToValidate}`); const release = await getDraftRelease(releaseToValidate); await validateReleaseAssets(release, true); } else { let draftRelease = await getDraftRelease(); uploadNodeShasums(); uploadIndexJson(); await createReleaseShasums(draftRelease); // Fetch latest version of release before verifying draftRelease = await getDraftRelease(pkgVersion, true); await validateReleaseAssets(draftRelease); await publishRelease(draftRelease); console.log(`${pass} SUCCESS!!! Release has been published. Please run ` + '"npm run publish-to-npm" to publish release to npm.'); } } async function makeTempDir () { return new Promise((resolve, reject) => { temp.mkdir('electron-publish', (err, dirPath) => { if (err) { reject(err); } else { resolve(dirPath); } }); }); } async function verifyAssets (release) { const downloadDir = await makeTempDir(); console.log('Downloading files from GitHub to verify shasums'); const shaSumFile = 'SHASUMS256.txt'; let filesToCheck = await Promise.all(release.assets.map(async asset => { const requestOptions = await octokit.repos.getReleaseAsset.endpoint({ owner: 'electron', repo: targetRepo, asset_id: asset.id, headers: { Accept: 'application/octet-stream' } }); const { url, headers } = requestOptions; headers.authorization = `token ${process.env.ELECTRON_GITHUB_TOKEN}`; const response = await got(url, { followRedirect: false, method: 'HEAD', headers }); await downloadFiles(response.headers.location, downloadDir, asset.name); return asset.name; })).catch(err => { console.log(`${fail} Error downloading files from GitHub`, err); process.exit(1); }); filesToCheck = filesToCheck.filter(fileName => fileName !== shaSumFile); let checkerOpts; await validateChecksums({ algorithm: 'sha256', filesToCheck, fileDirectory: downloadDir, shaSumFile, checkerOpts, fileSource: 'GitHub' }); } function downloadFiles (urls, directory, targetName) { return new Promise((resolve, reject) => { const nuggetOpts = { dir: directory }; nuggetOpts.quiet = !args.verboseNugget; if (targetName) nuggetOpts.target = targetName; nugget(urls, nuggetOpts, (err) => { if (err) { reject(err); } else { console.log(`${pass} all files downloaded successfully!`); resolve(); } }); }); } async function verifyShasums (urls, isS3) { const fileSource = isS3 ? 'S3' : 'GitHub'; console.log(`Downloading files from ${fileSource} to verify shasums`); const downloadDir = await makeTempDir(); let filesToCheck = []; try { if (!isS3) { await downloadFiles(urls, downloadDir); filesToCheck = urls.map(url => { const currentUrl = new URL(url); return path.basename(currentUrl.pathname); }).filter(file => file.indexOf('SHASUMS') === -1); } else { const s3VersionPath = `/atom-shell/dist/${pkgVersion}/`; await Promise.all(urls.map(async (url) => { const currentUrl = new URL(url); const dirname = path.dirname(currentUrl.pathname); const filename = path.basename(currentUrl.pathname); const s3VersionPathIdx = dirname.indexOf(s3VersionPath); if (s3VersionPathIdx === -1 || dirname === s3VersionPath) { if (s3VersionPathIdx !== -1 && filename.indexof('SHASUMS') === -1) { filesToCheck.push(filename); } await downloadFiles(url, downloadDir); } else { const subDirectory = dirname.substr(s3VersionPathIdx + s3VersionPath.length); const fileDirectory = path.join(downloadDir, subDirectory); try { fs.statSync(fileDirectory); } catch (err) { fs.mkdirSync(fileDirectory); } filesToCheck.push(path.join(subDirectory, filename)); await downloadFiles(url, fileDirectory); } })); } } catch (err) { console.log(`${fail} Error downloading files from ${fileSource}`, err); process.exit(1); } console.log(`${pass} Successfully downloaded the files from ${fileSource}.`); let checkerOpts; if (isS3) { checkerOpts = { defaultTextEncoding: 'binary' }; } await validateChecksums({ algorithm: 'sha256', filesToCheck, fileDirectory: downloadDir, shaSumFile: 'SHASUMS256.txt', checkerOpts, fileSource }); if (isS3) { await validateChecksums({ algorithm: 'sha1', filesToCheck, fileDirectory: downloadDir, shaSumFile: 'SHASUMS.txt', checkerOpts, fileSource }); } } async function validateChecksums (validationArgs) { console.log(`Validating checksums for files from ${validationArgs.fileSource} ` + `against ${validationArgs.shaSumFile}.`); const shaSumFilePath = path.join(validationArgs.fileDirectory, validationArgs.shaSumFile); const checker = new sumchecker.ChecksumValidator(validationArgs.algorithm, shaSumFilePath, validationArgs.checkerOpts); await checker.validate(validationArgs.fileDirectory, validationArgs.filesToCheck) .catch(err => { if (err instanceof sumchecker.ChecksumMismatchError) { console.error(`${fail} The checksum of ${err.filename} from ` + `${validationArgs.fileSource} did not match the shasum in ` + `${validationArgs.shaSumFile}`); } else if (err instanceof sumchecker.ChecksumParseError) { console.error(`${fail} The checksum file ${validationArgs.shaSumFile} ` + `from ${validationArgs.fileSource} could not be parsed.`, err); } else if (err instanceof sumchecker.NoChecksumFoundError) { console.error(`${fail} The file ${err.filename} from ` + `${validationArgs.fileSource} was not in the shasum file ` + `${validationArgs.shaSumFile}.`); } else { console.error(`${fail} Error matching files from ` + `${validationArgs.fileSource} shasums in ${validationArgs.shaSumFile}.`, err); } process.exit(1); }); console.log(`${pass} All files from ${validationArgs.fileSource} match ` + `shasums defined in ${validationArgs.shaSumFile}.`); } makeRelease(args.validateRelease);