build: cleanup release scripts, separate cli entrypoints from logic (#44082)

* build: cleanup release scripts, separate cli entrypoints from logic

Co-authored-by: Samuel Attard <samuel.r.attard@gmail.com>

* build: use repo/org constants

Co-authored-by: Samuel Attard <samuel.r.attard@gmail.com>

---------

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Samuel Attard <samuel.r.attard@gmail.com>
This commit is contained in:
trop[bot] 2024-10-01 14:08:15 -07:00 committed by GitHub
parent 7b14a305f8
commit 6074f7ed31
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 410 additions and 300 deletions

View file

@ -0,0 +1,61 @@
# Release Scripts
> These ancient artifacts date back to the early days of Electron, they have been modified
> over the years but in reality still very much look how they did at the beginning. You
> have been warned.
None of these scripts are called manually, they are each called by Sudowoodo at various points
in the Electron release process. What each script does though is loosely documented below,
however this documentation is a best effort so please be careful when modifying the scripts
as there still may be unknown or undocumented effects / intentions.
## What scripts do we have?
### `cleanup-release`
This script completely reverts a failed or otherwise unreleasable version. It does this by:
* Deleting the draft release if it exists
* Deleting the git tag if it exists
> [!NOTE]
> This is the only script / case where an existing tag will be deleted. Tags are only considered immutable after the release is published.
### `print-next-version`
This script just outputs the theoretical "next" version that a release would use.
### `prepare-for-release`
This script creates all the requisite tags and CI builds that will populate required release assets.
* Creates the git tag
* Kicks off all release builds on AppVeyor and GitHub Actions
### `run-release-build`
This script is used to re-kick specific release builds after they fail. Sudowoodo is responsible for prompting the release team as to whether or not to run this script. It's currently only used for AppVeyor builds.
> [!IMPORTANT]
> This script should be removed and the "rerun" logic for AppVeyor be implemented in Sudowoodo specifically in the same way that GitHub Actions' rerun logic is.
### `validate-before-publish`
This script ensures that a release is in a valid state before publishing it anywhere. Specifically it checks:
* All assets exist
* All checksums match uploaded assets
* Headers have been uploaded to the header CDN
* Symbols have been uploaded to the symbol CDN
### `publish-to-github`
This script finalizes the GitHub release, in the process it:
* Uploads the header SHASUMs to the CDN
* Updates the `index.json` file on the assets CDN with the new version via metadumper
* Publishes the actual GitHub release
### `publish-to-npm`
This script finishes the release process by publishing a new `npm` package.

View file

@ -0,0 +1,30 @@
import { parseArgs } from 'node:util';
import { cleanReleaseArtifacts } from '../release-artifact-cleanup';
const { values: { tag: _tag, releaseID } } = parseArgs({
options: {
tag: {
type: 'string'
},
releaseID: {
type: 'string',
default: ''
}
}
});
if (!_tag) {
console.error('Missing --tag argument');
process.exit(1);
}
const tag = _tag;
cleanReleaseArtifacts({
releaseID,
tag
})
.catch((err) => {
console.error(err);
process.exit(1);
});

View file

@ -0,0 +1,32 @@
import { parseArgs } from 'node:util';
import { prepareRelease } from '../prepare-release';
import { ELECTRON_REPO, isVersionBumpType, NIGHTLY_REPO } from '../types';
const { values: { branch }, positionals } = parseArgs({
options: {
branch: {
type: 'string'
}
},
allowPositionals: true
});
const bumpType = positionals[0];
if (!bumpType || !isVersionBumpType(bumpType)) {
console.log('Usage: prepare-for-release [stable | minor | beta | alpha | nightly]' +
' (--branch=branch)');
process.exit(1);
}
prepareRelease({
isPreRelease: bumpType !== 'stable' && bumpType !== 'minor',
targetRepo: bumpType === 'nightly' ? NIGHTLY_REPO : ELECTRON_REPO,
targetBranch: branch,
bumpType
})
.catch((err) => {
console.error(err);
process.exit(1);
});

View file

@ -0,0 +1,32 @@
import { parseArgs } from 'node:util';
import { printNextVersion } from '../prepare-release';
import { ELECTRON_REPO, isVersionBumpType, NIGHTLY_REPO } from '../types';
const { values: { branch }, positionals } = parseArgs({
options: {
branch: {
type: 'string'
}
},
allowPositionals: true
});
const bumpType = positionals[0];
if (!bumpType || !isVersionBumpType(bumpType)) {
console.log('Usage: print-next-version [stable | minor | beta | alpha | nightly]' +
' (--branch=branch)');
process.exit(1);
}
printNextVersion({
isPreRelease: bumpType !== 'stable' && bumpType !== 'minor',
targetRepo: bumpType === 'nightly' ? NIGHTLY_REPO : ELECTRON_REPO,
targetBranch: branch,
bumpType
})
.catch((err) => {
console.error(err);
process.exit(1);
});

View file

@ -0,0 +1,6 @@
import { makeRelease } from '../release';
makeRelease().catch((err) => {
console.error('Error occurred while making release:', err);
process.exit(1);
});

View file

@ -0,0 +1,229 @@
import { Octokit } from '@octokit/rest';
import * as childProcess from 'node:child_process';
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as semver from 'semver';
import * as temp from 'temp';
import { getCurrentBranch, ELECTRON_DIR } from '../../lib/utils';
import { getElectronVersion } from '../../lib/get-version';
import { getAssetContents } from '../get-asset';
import { createGitHubTokenStrategy } from '../github-token';
import { ELECTRON_ORG, ELECTRON_REPO, ElectronReleaseRepo, NIGHTLY_REPO } from '../types';
const rootPackageJson = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../../package.json'), 'utf-8'));
if (!process.env.ELECTRON_NPM_OTP) {
console.error('Please set ELECTRON_NPM_OTP');
process.exit(1);
}
let tempDir: string;
temp.track(); // track and cleanup files at exit
const files = [
'cli.js',
'index.js',
'install.js',
'package.json',
'README.md',
'LICENSE'
];
const jsonFields = [
'name',
'repository',
'description',
'license',
'author',
'keywords'
];
let npmTag = '';
const currentElectronVersion = getElectronVersion();
const isNightlyElectronVersion = currentElectronVersion.includes('nightly');
const targetRepo = getRepo();
const octokit = new Octokit({
userAgent: 'electron-npm-publisher',
authStrategy: createGitHubTokenStrategy(targetRepo)
});
function getRepo (): ElectronReleaseRepo {
return isNightlyElectronVersion ? NIGHTLY_REPO : ELECTRON_REPO;
}
new Promise<string>((resolve, reject) => {
temp.mkdir('electron-npm', (err, dirPath) => {
if (err) {
reject(err);
} else {
resolve(dirPath);
}
});
})
.then((dirPath) => {
tempDir = dirPath;
// copy files from `/npm` to temp directory
for (const name of files) {
const noThirdSegment = name === 'README.md' || name === 'LICENSE';
fs.writeFileSync(
path.join(tempDir, name),
fs.readFileSync(path.join(ELECTRON_DIR, noThirdSegment ? '' : 'npm', name))
);
}
// copy from root package.json to temp/package.json
const packageJson = require(path.join(tempDir, 'package.json'));
for (const fieldName of jsonFields) {
packageJson[fieldName] = rootPackageJson[fieldName];
}
packageJson.version = currentElectronVersion;
fs.writeFileSync(
path.join(tempDir, 'package.json'),
JSON.stringify(packageJson, null, 2)
);
return octokit.repos.listReleases({
owner: ELECTRON_ORG,
repo: targetRepo
});
})
.then((releases) => {
// download electron.d.ts from release
const release = releases.data.find(
(release) => release.tag_name === `v${currentElectronVersion}`
);
if (!release) {
throw new Error(`cannot find release with tag v${currentElectronVersion}`);
}
return release;
})
.then(async (release) => {
const tsdAsset = release.assets.find((asset) => asset.name === 'electron.d.ts');
if (!tsdAsset) {
throw new Error(`cannot find electron.d.ts from v${currentElectronVersion} release assets`);
}
const typingsContent = await getAssetContents(
targetRepo,
tsdAsset.id
);
fs.writeFileSync(path.join(tempDir, 'electron.d.ts'), typingsContent);
return release;
})
.then(async (release) => {
const checksumsAsset = release.assets.find((asset) => asset.name === 'SHASUMS256.txt');
if (!checksumsAsset) {
throw new Error(`cannot find SHASUMS256.txt from v${currentElectronVersion} release assets`);
}
const checksumsContent = await getAssetContents(
targetRepo,
checksumsAsset.id
);
const checksumsObject: Record<string, string> = Object.create(null);
for (const line of checksumsContent.trim().split('\n')) {
const [checksum, file] = line.split(' *');
checksumsObject[file] = checksum;
}
fs.writeFileSync(path.join(tempDir, 'checksums.json'), JSON.stringify(checksumsObject, null, 2));
return release;
})
.then(async (release) => {
const currentBranch = await getCurrentBranch();
if (isNightlyElectronVersion) {
// Nightlies get published to their own module, so they should be tagged as latest
npmTag = currentBranch === 'main' ? 'latest' : `nightly-${currentBranch}`;
const currentJson = JSON.parse(fs.readFileSync(path.join(tempDir, 'package.json'), 'utf8'));
currentJson.name = 'electron-nightly';
rootPackageJson.name = 'electron-nightly';
fs.writeFileSync(
path.join(tempDir, 'package.json'),
JSON.stringify(currentJson, null, 2)
);
} else {
if (currentBranch === 'main') {
// This should never happen, main releases should be nightly releases
// this is here just-in-case
throw new Error('Unreachable release phase, can\'t tag a non-nightly release on the main branch');
} else if (!release.prerelease) {
// Tag the release with a `2-0-x` style tag
npmTag = currentBranch;
} else if (release.tag_name.indexOf('alpha') > 0) {
// Tag the release with an `alpha-3-0-x` style tag
npmTag = `alpha-${currentBranch}`;
} else {
// Tag the release with a `beta-3-0-x` style tag
npmTag = `beta-${currentBranch}`;
}
}
})
.then(() => childProcess.execSync('npm pack', { cwd: tempDir }))
.then(() => {
// test that the package can install electron prebuilt from github release
const tarballPath = path.join(tempDir, `${rootPackageJson.name}-${currentElectronVersion}.tgz`);
return new Promise((resolve, reject) => {
const result = childProcess.spawnSync('npm', ['install', tarballPath, '--force', '--silent'], {
env: { ...process.env, electron_config_cache: tempDir },
cwd: tempDir,
stdio: 'inherit'
});
if (result.status !== 0) {
return reject(new Error(`npm install failed with status ${result.status}`));
}
try {
const electronPath = require(path.resolve(tempDir, 'node_modules', rootPackageJson.name));
if (typeof electronPath !== 'string') {
return reject(new Error(`path to electron binary (${electronPath}) returned by the ${rootPackageJson.name} module is not a string`));
}
if (!fs.existsSync(electronPath)) {
return reject(new Error(`path to electron binary (${electronPath}) returned by the ${rootPackageJson.name} module does not exist on disk`));
}
} catch (e) {
console.error(e);
return reject(new Error(`loading the generated ${rootPackageJson.name} module failed with an error`));
}
resolve(tarballPath);
});
})
.then((tarballPath) => {
const existingVersionJSON = childProcess.execSync(`npx npm@7 view ${rootPackageJson.name}@${currentElectronVersion} --json`).toString('utf-8');
// It's possible this is a re-run and we already have published the package, if not we just publish like normal
if (!existingVersionJSON) {
childProcess.execSync(`npm publish ${tarballPath} --tag ${npmTag} --otp=${process.env.ELECTRON_NPM_OTP}`);
}
})
.then(() => {
const currentTags = JSON.parse(childProcess.execSync('npm show electron dist-tags --json').toString());
const parsedLocalVersion = semver.parse(currentElectronVersion)!;
if (rootPackageJson.name === 'electron') {
// We should only customly add dist tags for non-nightly releases where the package name is still
// "electron"
if (parsedLocalVersion.prerelease.length === 0 &&
semver.gt(currentElectronVersion, currentTags.latest)) {
childProcess.execSync(`npm dist-tag add electron@${currentElectronVersion} latest --otp=${process.env.ELECTRON_NPM_OTP}`);
}
if (parsedLocalVersion.prerelease[0] === 'beta' &&
semver.gt(currentElectronVersion, currentTags.beta)) {
childProcess.execSync(`npm dist-tag add electron@${currentElectronVersion} beta --otp=${process.env.ELECTRON_NPM_OTP}`);
}
if (parsedLocalVersion.prerelease[0] === 'alpha' &&
semver.gt(currentElectronVersion, currentTags.alpha)) {
childProcess.execSync(`npm dist-tag add electron@${currentElectronVersion} alpha --otp=${process.env.ELECTRON_NPM_OTP}`);
}
}
})
.catch((err) => {
console.error('Error:', err);
process.exit(1);
});

View file

@ -0,0 +1,53 @@
import { parseArgs } from 'node:util';
import { runReleaseCIJobs } from '../run-release-ci-jobs';
const { values: { ghRelease, job, arch, ci, commit, newVersion }, positionals } = parseArgs({
options: {
ghRelease: {
type: 'boolean'
},
job: {
type: 'string'
},
arch: {
type: 'string'
},
ci: {
type: 'string'
},
commit: {
type: 'string'
},
newVersion: {
type: 'string'
}
},
allowPositionals: true
});
const targetBranch = positionals[0];
if (positionals.length < 1) {
console.log(`Trigger CI to build release builds of electron.
Usage: ci-release-build.js [--job=CI_JOB_NAME] [--arch=INDIVIDUAL_ARCH] [--ci=AppVeyor|GitHubActions]
[--ghRelease] [--commit=sha] [--newVersion=version_tag] TARGET_BRANCH
`);
process.exit(0);
}
if (ci === 'GitHubActions' || !ci) {
if (!newVersion) {
console.error('--newVersion is required for GitHubActions');
process.exit(1);
}
}
runReleaseCIJobs(targetBranch, {
ci: ci as 'GitHubActions' | 'AppVeyor',
ghRelease,
job: job as any,
arch,
newVersion: newVersion!,
commit
});

View file

@ -0,0 +1,6 @@
import { validateRelease } from '../release';
validateRelease().catch((err) => {
console.error('Error occurred while validating release:', err);
process.exit(1);
});