mirror of
https://github.com/electron/electron.git
synced 2026-01-09 15:38:08 -05:00
It's guarunteed that `iojs-*` and `node-*` were the same origin file (we azcopy them) but this was missing in logs and it annoyed me
590 lines
18 KiB
JavaScript
Executable File
590 lines
18 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
|
|
import { BlobServiceClient } from '@azure/storage-blob';
|
|
import { Octokit } from '@octokit/rest';
|
|
import * as chalk from 'chalk';
|
|
import got from 'got';
|
|
import { gte } from 'semver';
|
|
import { track as trackTemp } from 'temp';
|
|
|
|
import { execSync, ExecSyncOptions } from 'node:child_process';
|
|
import { statSync, createReadStream, writeFileSync, close } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
|
|
import { getUrlHash } from './get-url-hash';
|
|
import { createGitHubTokenStrategy } from './github-token';
|
|
import { ELECTRON_ORG, ELECTRON_REPO, ElectronReleaseRepo, NIGHTLY_REPO } from './types';
|
|
import { getElectronVersion } from '../lib/get-version';
|
|
import { ELECTRON_DIR } from '../lib/utils';
|
|
|
|
const temp = trackTemp();
|
|
|
|
const pass = chalk.green('✓');
|
|
const fail = chalk.red('✗');
|
|
|
|
const pkgVersion = `v${getElectronVersion()}`;
|
|
|
|
function getRepo (): ElectronReleaseRepo {
|
|
return pkgVersion.indexOf('nightly') > 0 ? NIGHTLY_REPO : ELECTRON_REPO;
|
|
}
|
|
|
|
const targetRepo = getRepo();
|
|
let failureCount = 0;
|
|
|
|
const octokit = new Octokit({
|
|
authStrategy: createGitHubTokenStrategy(targetRepo)
|
|
});
|
|
|
|
async function getDraftRelease (
|
|
version?: string,
|
|
skipValidation: boolean = false
|
|
) {
|
|
const releaseInfo = await octokit.repos.listReleases({
|
|
owner: ELECTRON_ORG,
|
|
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.includes('beta')) {
|
|
check(draft.prerelease, 'draft is a prerelease');
|
|
}
|
|
check(
|
|
!!draft.body &&
|
|
draft.body.length > 50 &&
|
|
!draft.body.includes('(placeholder)'),
|
|
'draft has release notes'
|
|
);
|
|
check(failureCount === 0, 'Draft release looks good to go.', true);
|
|
}
|
|
return draft;
|
|
}
|
|
|
|
type MinimalRelease = {
|
|
id: number;
|
|
tag_name: string;
|
|
draft: boolean;
|
|
prerelease: boolean;
|
|
assets: {
|
|
name: string;
|
|
browser_download_url: string;
|
|
id: number;
|
|
}[];
|
|
};
|
|
|
|
async function validateReleaseAssets (
|
|
release: MinimalRelease,
|
|
validatingRelease: boolean = false
|
|
) {
|
|
const requiredAssets = assetsForVersion(
|
|
release.tag_name,
|
|
validatingRelease
|
|
).sort();
|
|
const extantAssets = release.assets.map((asset) => asset.name).sort();
|
|
const downloadUrls = release.assets
|
|
.map((asset) => ({ url: asset.browser_download_url, file: asset.name }))
|
|
.sort((a, b) => a.file.localeCompare(b.file));
|
|
|
|
failureCount = 0;
|
|
for (const asset of requiredAssets) {
|
|
check(extantAssets.includes(asset), asset);
|
|
}
|
|
check(
|
|
failureCount === 0,
|
|
'All required GitHub assets exist for release',
|
|
true
|
|
);
|
|
|
|
if (!validatingRelease || !release.draft) {
|
|
if (release.draft) {
|
|
await verifyDraftGitHubReleaseAssets(release);
|
|
} else {
|
|
await verifyShasumsForRemoteFiles(downloadUrls).catch((err) => {
|
|
console.error(`${fail} error verifyingShasums`, err);
|
|
});
|
|
}
|
|
const azRemoteFiles = azRemoteFilesForVersion(release.tag_name);
|
|
await verifyShasumsForRemoteFiles(azRemoteFiles, true);
|
|
}
|
|
}
|
|
|
|
function check (condition: boolean, statement: string, exitIfFail = false) {
|
|
if (condition) {
|
|
console.log(`${pass} ${statement}`);
|
|
} else {
|
|
failureCount++;
|
|
console.error(`${fail} ${statement}`);
|
|
if (exitIfFail) process.exit(1);
|
|
}
|
|
}
|
|
|
|
function assetsForVersion (version: string, validatingRelease: boolean) {
|
|
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-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-dsym-snapshot.zip`,
|
|
`electron-${version}-darwin-x64-symbols.zip`,
|
|
`electron-${version}-darwin-x64.zip`,
|
|
`electron-${version}-darwin-arm64-dsym.zip`,
|
|
`electron-${version}-darwin-arm64-dsym-snapshot.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-x64-debug.zip`,
|
|
`electron-${version}-linux-x64-symbols.zip`,
|
|
`electron-${version}-linux-x64.zip`,
|
|
`electron-${version}-mas-x64-dsym.zip`,
|
|
`electron-${version}-mas-x64-dsym-snapshot.zip`,
|
|
`electron-${version}-mas-x64-symbols.zip`,
|
|
`electron-${version}-mas-x64.zip`,
|
|
`electron-${version}-mas-arm64-dsym.zip`,
|
|
`electron-${version}-mas-arm64-dsym-snapshot.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',
|
|
'libcxx_headers.zip',
|
|
'libcxxabi_headers.zip',
|
|
`libcxx-objects-${version}-linux-arm64.zip`,
|
|
`libcxx-objects-${version}-linux-armv7l.zip`,
|
|
`libcxx-objects-${version}-linux-x64.zip`,
|
|
`ffmpeg-${version}-darwin-x64.zip`,
|
|
`ffmpeg-${version}-darwin-arm64.zip`,
|
|
`ffmpeg-${version}-linux-arm64.zip`,
|
|
`ffmpeg-${version}-linux-armv7l.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-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;
|
|
}
|
|
|
|
const cloudStoreFilePaths = (version: string) => [
|
|
`iojs-${version}-headers.tar.gz`,
|
|
`iojs-${version}.tar.gz`,
|
|
`node-${version}-headers.tar.gz`,
|
|
`node-${version}.tar.gz`,
|
|
'node.lib',
|
|
'arm64/node.lib',
|
|
'x64/node.lib',
|
|
'win-x64/iojs.lib',
|
|
'win-x86/iojs.lib',
|
|
'win-arm64/iojs.lib',
|
|
'win-x64/node.lib',
|
|
'win-x86/node.lib',
|
|
'win-arm64/node.lib',
|
|
'SHASUMS.txt',
|
|
'SHASUMS256.txt'
|
|
];
|
|
|
|
function azRemoteFilesForVersion (version: string) {
|
|
const azCDN = 'https://artifacts.electronjs.org/headers/';
|
|
const versionPrefix = `${azCDN}dist/${version}/`;
|
|
return cloudStoreFilePaths(version).map((filePath) => ({
|
|
file: filePath,
|
|
url: `${versionPrefix}${filePath}`
|
|
}));
|
|
}
|
|
|
|
function runScript (scriptName: string, scriptArgs: string[], cwd?: string) {
|
|
const scriptCommand = `${scriptName} ${scriptArgs.join(' ')}`;
|
|
const scriptOptions: ExecSyncOptions = {
|
|
encoding: 'utf-8'
|
|
};
|
|
if (cwd) scriptOptions.cwd = cwd;
|
|
try {
|
|
return execSync(scriptCommand, scriptOptions);
|
|
} catch (err) {
|
|
console.error(`${fail} Error running ${scriptName}`, err);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
function uploadNodeShasums () {
|
|
console.log('Uploading Node SHASUMS file to artifacts.electronjs.org.');
|
|
const scriptPath = join(
|
|
ELECTRON_DIR,
|
|
'script',
|
|
'release',
|
|
'uploaders',
|
|
'upload-node-checksums.py'
|
|
);
|
|
runScript(scriptPath, ['-v', pkgVersion]);
|
|
console.log(
|
|
`${pass} Done uploading Node SHASUMS file to artifacts.electronjs.org.`
|
|
);
|
|
}
|
|
|
|
function uploadIndexJson () {
|
|
console.log('Uploading index.json to artifacts.electronjs.org.');
|
|
const scriptPath = join(
|
|
ELECTRON_DIR,
|
|
'script',
|
|
'release',
|
|
'uploaders',
|
|
'upload-index-json.py'
|
|
);
|
|
runScript(scriptPath, [pkgVersion]);
|
|
console.log(`${pass} Done uploading index.json to artifacts.electronjs.org.`);
|
|
}
|
|
|
|
async function mergeShasums (pkgVersion: string) {
|
|
// Download individual checksum files for Electron zip files from artifact storage,
|
|
// concatenate them, and upload to GitHub.
|
|
|
|
const connectionString = process.env.ELECTRON_ARTIFACTS_BLOB_STORAGE;
|
|
if (!connectionString) {
|
|
throw new Error(
|
|
'Please set the $ELECTRON_ARTIFACTS_BLOB_STORAGE environment variable'
|
|
);
|
|
}
|
|
|
|
const blobServiceClient =
|
|
BlobServiceClient.fromConnectionString(connectionString);
|
|
const containerClient = blobServiceClient.getContainerClient(
|
|
'checksums-scratchpad'
|
|
);
|
|
const blobsIter = containerClient.listBlobsFlat({
|
|
prefix: `${pkgVersion}/`
|
|
});
|
|
const shasums = [];
|
|
for await (const blob of blobsIter) {
|
|
if (blob.name.endsWith('.sha256sum')) {
|
|
const blobClient = containerClient.getBlockBlobClient(blob.name);
|
|
const response = await blobClient.downloadToBuffer();
|
|
shasums.push(response.toString('ascii').trim());
|
|
}
|
|
}
|
|
return shasums.join('\n');
|
|
}
|
|
|
|
async function createReleaseShasums (release: MinimalRelease) {
|
|
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_ORG,
|
|
repo: targetRepo,
|
|
asset_id: existingAssets[0].id
|
|
})
|
|
.catch((err) => {
|
|
console.error(`${fail} Error deleting ${fileName} on GitHub:`, err);
|
|
process.exit(1);
|
|
});
|
|
}
|
|
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: string,
|
|
fileName: string,
|
|
releaseId: number
|
|
) {
|
|
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': statSync(filePath).size
|
|
},
|
|
data: createReadStream(filePath),
|
|
name: fileName
|
|
} as any)
|
|
.catch((err) => {
|
|
console.error(`${fail} Error uploading ${filePath} to GitHub:`, err);
|
|
process.exit(1);
|
|
});
|
|
}
|
|
|
|
function saveShaSumFile (checksums: string, fileName: string) {
|
|
return new Promise<string>((resolve) => {
|
|
temp.open(fileName, (err, info) => {
|
|
if (err) {
|
|
console.error(`${fail} Could not create ${fileName} file`);
|
|
process.exit(1);
|
|
} else {
|
|
writeFileSync(info.fd, checksums);
|
|
close(info.fd, (err) => {
|
|
if (err) {
|
|
console.error(`${fail} Could close ${fileName} file`);
|
|
process.exit(1);
|
|
}
|
|
resolve(info.path);
|
|
});
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
async function publishRelease (release: MinimalRelease) {
|
|
let makeLatest = false;
|
|
if (!release.prerelease) {
|
|
const currentLatest = await octokit.repos.getLatestRelease({
|
|
owner: ELECTRON_ORG,
|
|
repo: targetRepo
|
|
});
|
|
|
|
makeLatest = gte(release.tag_name, currentLatest.data.tag_name);
|
|
}
|
|
|
|
return octokit.repos
|
|
.updateRelease({
|
|
owner: ELECTRON_ORG,
|
|
repo: targetRepo,
|
|
release_id: release.id,
|
|
tag_name: release.tag_name,
|
|
draft: false,
|
|
make_latest: makeLatest ? 'true' : 'false'
|
|
})
|
|
.catch((err) => {
|
|
console.error(`${fail} Error publishing release:`, err);
|
|
process.exit(1);
|
|
});
|
|
}
|
|
|
|
export async function validateRelease () {
|
|
console.log(`Validating release ${pkgVersion}`);
|
|
const release = await getDraftRelease(pkgVersion);
|
|
await validateReleaseAssets(release, true);
|
|
}
|
|
|
|
export async function makeRelease () {
|
|
let draftRelease = await getDraftRelease();
|
|
uploadNodeShasums();
|
|
await createReleaseShasums(draftRelease);
|
|
|
|
// Fetch latest version of release before verifying
|
|
draftRelease = await getDraftRelease(pkgVersion, true);
|
|
await validateReleaseAssets(draftRelease);
|
|
// index.json goes live once uploaded so do these uploads as
|
|
// late as possible to reduce the chances it contains a release
|
|
// which fails to publish. It has to be done before the final
|
|
// publish to ensure there aren't published releases not contained
|
|
// in index.json, which causes other problems in downstream projects
|
|
uploadIndexJson();
|
|
await publishRelease(draftRelease);
|
|
console.log(
|
|
`${pass} SUCCESS!!! Release has been published. Please run ` +
|
|
'"npm run publish-to-npm" to publish release to npm.'
|
|
);
|
|
}
|
|
|
|
const SHASUM_256_FILENAME = 'SHASUMS256.txt';
|
|
const SHASUM_1_FILENAME = 'SHASUMS.txt';
|
|
|
|
async function verifyDraftGitHubReleaseAssets (release: MinimalRelease) {
|
|
console.log('Fetching authenticated GitHub artifact URLs to verify shasums');
|
|
|
|
const remoteFilesToHash = await Promise.all(
|
|
release.assets.map(async (asset) => {
|
|
const requestOptions = octokit.repos.getReleaseAsset.endpoint({
|
|
owner: ELECTRON_ORG,
|
|
repo: targetRepo,
|
|
asset_id: asset.id,
|
|
headers: {
|
|
Accept: 'application/octet-stream'
|
|
}
|
|
});
|
|
|
|
const { url, headers } = requestOptions;
|
|
headers.authorization = `token ${
|
|
((await octokit.auth()) as { token: string }).token
|
|
}`;
|
|
|
|
const response = await got(url, {
|
|
followRedirect: false,
|
|
method: 'HEAD',
|
|
headers: headers as any,
|
|
throwHttpErrors: false
|
|
});
|
|
|
|
if (response.statusCode !== 302 && response.statusCode !== 301) {
|
|
console.error('Failed to HEAD github asset: ' + url);
|
|
throw new Error(
|
|
"Unexpected status HEAD'ing github asset: " + response.statusCode
|
|
);
|
|
}
|
|
|
|
return { url: response.headers.location!, file: asset.name };
|
|
})
|
|
).catch((err) => {
|
|
console.error(`${fail} Error downloading files from GitHub`, err);
|
|
process.exit(1);
|
|
});
|
|
|
|
await verifyShasumsForRemoteFiles(remoteFilesToHash);
|
|
}
|
|
|
|
async function getShaSumMappingFromUrl (
|
|
shaSumFileUrl: string,
|
|
fileNamePrefix: string
|
|
) {
|
|
const response = await got(shaSumFileUrl, {
|
|
throwHttpErrors: false
|
|
});
|
|
|
|
if (response.statusCode !== 200) {
|
|
console.error('Failed to fetch SHASUM mapping: ' + shaSumFileUrl);
|
|
console.error('Bad SHASUM mapping response: ' + response.body.trim());
|
|
throw new Error(
|
|
'Unexpected status fetching SHASUM mapping: ' + response.statusCode
|
|
);
|
|
}
|
|
|
|
const raw = response.body;
|
|
return raw
|
|
.split('\n')
|
|
.map((line) => line.trim())
|
|
.filter(Boolean)
|
|
.reduce((map, line) => {
|
|
const [sha, file] = line.replace(' ', ' ').split(' ');
|
|
map[file.slice(fileNamePrefix.length)] = sha;
|
|
return map;
|
|
}, Object.create(null) as Record<string, string>);
|
|
}
|
|
|
|
type HashedFile = HashableFile & {
|
|
hash: string;
|
|
};
|
|
|
|
type HashableFile = {
|
|
file: string;
|
|
url: string;
|
|
};
|
|
|
|
async function validateFileHashesAgainstShaSumMapping (
|
|
remoteFilesWithHashes: HashedFile[],
|
|
mapping: Record<string, string>
|
|
) {
|
|
for (const remoteFileWithHash of remoteFilesWithHashes) {
|
|
check(
|
|
remoteFileWithHash.hash === mapping[remoteFileWithHash.file],
|
|
`Release asset ${remoteFileWithHash.file} should have hash of ${
|
|
mapping[remoteFileWithHash.file]
|
|
} but found ${remoteFileWithHash.hash}`,
|
|
true
|
|
);
|
|
}
|
|
}
|
|
|
|
async function verifyShasumsForRemoteFiles (
|
|
remoteFilesToHash: HashableFile[],
|
|
filesAreNodeJSArtifacts = false
|
|
) {
|
|
console.log(
|
|
`Generating SHAs for ${remoteFilesToHash.length} files to verify shasums`
|
|
);
|
|
|
|
// Only used for node.js artifact uploads
|
|
const shaSum1File = remoteFilesToHash.find(
|
|
({ file }) => file === SHASUM_1_FILENAME
|
|
)!;
|
|
// Used for both node.js artifact uploads and normal electron artifacts
|
|
const shaSum256File = remoteFilesToHash.find(
|
|
({ file }) => file === SHASUM_256_FILENAME
|
|
)!;
|
|
remoteFilesToHash = remoteFilesToHash.filter(
|
|
({ file }) => file !== SHASUM_1_FILENAME && file !== SHASUM_256_FILENAME
|
|
);
|
|
|
|
const remoteFilesWithHashes = await Promise.all(
|
|
remoteFilesToHash.map(async (file) => {
|
|
return {
|
|
hash: await getUrlHash(file.url, 'sha256'),
|
|
...file
|
|
};
|
|
})
|
|
);
|
|
|
|
await validateFileHashesAgainstShaSumMapping(
|
|
remoteFilesWithHashes,
|
|
await getShaSumMappingFromUrl(
|
|
shaSum256File.url,
|
|
filesAreNodeJSArtifacts ? '' : '*'
|
|
)
|
|
);
|
|
|
|
if (filesAreNodeJSArtifacts) {
|
|
const remoteFilesWithSha1Hashes = await Promise.all(
|
|
remoteFilesToHash.map(async (file) => {
|
|
return {
|
|
hash: await getUrlHash(file.url, 'sha1'),
|
|
...file
|
|
};
|
|
})
|
|
);
|
|
|
|
await validateFileHashesAgainstShaSumMapping(
|
|
remoteFilesWithSha1Hashes,
|
|
await getShaSumMappingFromUrl(
|
|
shaSum1File.url,
|
|
filesAreNodeJSArtifacts ? '' : '*'
|
|
)
|
|
);
|
|
}
|
|
}
|