mirror of
https://github.com/ChainSafe/lodestar.git
synced 2026-05-02 03:01:08 -04:00
* Adopt gitflow for Lodestar releases * Apply feedback * Replace most mentions of `master` with `unstable` * Tweak various parts of RELEASE.md * Update CI * Tweak dev release version handling * Tweak workflow names * Replace `nightly` with `dev` * Replace `beta` with `rc` * Fix publish for rc release and stable release only * Remove stray "beta" * Add code examples to RELEASE.md * Clarify publish process * Collapse 'Alternatives considered' section * Move 'Details' header up one level * Clarify publishing a release * Tweak headings * Fix hotfix section * More clarifications * Update examples in RELEASE.md * More clarifications * Fix formatting * Run RELEASE.md through grammarly * Change script name to release:publish * Add create_rc script * Add workflow_dispatch for create RC * Add placeholder for other scripts * Update RELEASE.md * Update release create rc script * Update script instructions * create_rc script must not be run in CI * Update release flow * Add publish-rc workflow * Apply suggestions from code review Co-authored-by: Afr Schoe <58883403+q9f@users.noreply.github.com> Co-authored-by: Cayman <caymannava@gmail.com> Co-authored-by: Afr Schoe <58883403+q9f@users.noreply.github.com>
296 lines
7.8 KiB
JavaScript
296 lines
7.8 KiB
JavaScript
/* eslint-disable
|
|
no-console,
|
|
@typescript-eslint/explicit-function-return-type,
|
|
@typescript-eslint/no-unsafe-assignment,
|
|
@typescript-eslint/no-unsafe-member-access,
|
|
@typescript-eslint/no-unsafe-call,
|
|
@typescript-eslint/no-unsafe-return,
|
|
@typescript-eslint/explicit-module-boundary-types,
|
|
import/no-extraneous-dependencies
|
|
*/
|
|
|
|
import child_process from "node:child_process";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import semver from "semver";
|
|
import inquirer from "inquirer";
|
|
|
|
export const UNSTABLE_BRANCH = "unstable";
|
|
export const STABLE_BRANCH = "stable";
|
|
export const REPO_SLUG = "chainsafe/lodestar";
|
|
export const GIT_REPO_URL = `git@github.com:${REPO_SLUG}.git`;
|
|
export const MAIN_PACKAGE_PATH = "packages/cli";
|
|
|
|
/**
|
|
* @param {string} cmd
|
|
* @returns {string}
|
|
*/
|
|
export function shell(cmd) {
|
|
return child_process.execSync(cmd, {encoding: "utf8", stdio: "pipe"}).trim();
|
|
}
|
|
|
|
/**
|
|
* ```
|
|
* cmd <version> [commit]
|
|
* ```
|
|
* @typedef {Object} CliArgs
|
|
* @property {string} versionMMP - Major.Minor.Patch semver version to be released, eg: '1.1.0'
|
|
* @property {string} commit - Commit hash to be released (optional)
|
|
* @returns {CliArgs}
|
|
*/
|
|
export function parseCmdArgs() {
|
|
const versionArg = process.argv[2];
|
|
const commitArg = process.argv[3];
|
|
|
|
if (versionArg === undefined) {
|
|
throw Error("argv[2] undefined, must provide version");
|
|
}
|
|
|
|
let commit;
|
|
// optional arg, defaults to HEAD
|
|
try {
|
|
commit = shell(`git log -n 1 --pretty='%h' ${commitArg ?? "HEAD"}`);
|
|
} catch (e) {
|
|
throw Error(`Invalid commit ${commitArg}`);
|
|
}
|
|
|
|
const versionObj = semver.parse(versionArg);
|
|
|
|
// Re-format version to drop any prefixes or suffixes
|
|
const versionMMP = [versionObj.major, versionObj.minor, versionObj.patch].join(".");
|
|
|
|
try {
|
|
if (versionObj.includePrerelease) throw Error("Includes pre-release");
|
|
if (semver.clean(versionArg) !== versionMMP) throw Error("No clean major.minor.path version");
|
|
} catch (e) {
|
|
throw Error(`Bad argv[2] semver version '${versionArg}': ${e.message}`);
|
|
}
|
|
|
|
return {
|
|
versionMMP,
|
|
commit,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param {string} commit
|
|
* @param {string} branch
|
|
*/
|
|
export function assertCommitExistsInBranch(commit, branch) {
|
|
/** @type {string} */
|
|
let headCommit;
|
|
try {
|
|
// Also, ensure the branch exists first
|
|
headCommit = shell(`git rev-parse refs/heads/${branch}`);
|
|
} catch (e) {
|
|
throw Error(`Branch ${branch} does not exist: ${e.message}`);
|
|
}
|
|
|
|
// Best, safest strategy to assert ancestor-ship
|
|
// From https://stackoverflow.com/questions/43535132/given-a-commit-id-how-to-determine-if-current-branch-contains-the-commit
|
|
//
|
|
// git merge-base --is-ancestor parent child -> exit code 0 (YES)
|
|
// git merge-base --is-ancestor child parent -> exit code 1 (NO)
|
|
// git merge-base --is-ancestor child child -> exit code 0 (YES)
|
|
|
|
try {
|
|
shell(`git merge-base --is-ancestor ${commit} ${headCommit}`);
|
|
} catch (e) {
|
|
throw Error(`Commit ${commit} does not belong to branch ${branch}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generic confirm prompt
|
|
* @param {string} message
|
|
*/
|
|
export async function confirm(message) {
|
|
// CI is never interactive, skip checks
|
|
if (process.env.CI) {
|
|
return true;
|
|
}
|
|
|
|
const input = await inquirer.prompt([
|
|
{
|
|
name: "yes",
|
|
type: "confirm",
|
|
message,
|
|
},
|
|
]);
|
|
|
|
return Boolean(input.yes);
|
|
}
|
|
|
|
/**
|
|
* Returns branch head commit if exists, null if doesn't exists
|
|
* @param {string} branch
|
|
* @returns {string|null}
|
|
*/
|
|
export function checkBranchExistsLocal(branch) {
|
|
try {
|
|
return shell(`git show-ref refs/heads/${branch}`);
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns branch head commit if exists, null if doesn't exists
|
|
* @param {string} branch
|
|
* @returns {string|null}
|
|
*/
|
|
export function checkBranchExistsRemote(branch) {
|
|
// From https://stackoverflow.com/questions/8223906/how-to-check-if-remote-branch-exists-on-a-given-remote-repository
|
|
|
|
// If branch is found returns:
|
|
// ```
|
|
// b523c9000c4df1afbd8371324083fef218669108 refs/heads/branch-name
|
|
// ```
|
|
// If not found, returns empty
|
|
try {
|
|
console.log(`Checking if branch ${branch} exists in remote`); // Log as this action is slow
|
|
const out = shell(`git ls-remote --heads ${GIT_REPO_URL} refs/heads/${branch}`);
|
|
|
|
// Empty means not found
|
|
if (!out) return null;
|
|
|
|
// Return the first part of the first line
|
|
return out.split(/\s+/)[0];
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns tag commit if exists, null if doesn't exists
|
|
* @param {string} tag
|
|
* @returns {string|null}
|
|
*/
|
|
export function checkTagExistsLocal(tag) {
|
|
try {
|
|
return shell(`git show-ref refs/tags/${tag}`);
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns tag commit if exists, null if doesn't exists
|
|
* @param {string} tag
|
|
* @returns {string|null}
|
|
*/
|
|
export function checkTagExistsRemote(tag) {
|
|
// Returns list of tags
|
|
// bb944682f7f65272137de74ed18605e49257356c refs/tags/v0.1.6
|
|
// 771a930dc0ba86769d6862bc4dc100acc50170fa refs/tags/v0.1.6^{}
|
|
//
|
|
// What's the '^{}'? Explanation below:
|
|
// https://stackoverflow.com/questions/15472107/when-listing-git-ls-remote-why-theres-after-the-tag-name
|
|
try {
|
|
console.log(`Checking if tag ${tag} exists in remote`); // Log as this action is slow
|
|
const out = shell(`git ls-remote --tags ${GIT_REPO_URL} refs/tags/${tag}`);
|
|
|
|
// Empty means not found
|
|
if (!out) return null;
|
|
|
|
// Return the first part of the first line
|
|
return out.split(/\s+/)[0];
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Throws if there are any tracked or untracked changes
|
|
*/
|
|
export function assertGitDirectoryIsClean() {
|
|
// From https://unix.stackexchange.com/questions/155046/determine-if-git-working-directory-is-clean-from-a-script
|
|
const changedFileList = shell("git status --porcelain");
|
|
if (changedFileList) {
|
|
throw Error(`git directory must be clean, changed files:\n${changedFileList}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the package.json JSON of the main package (lodestar-cli)
|
|
* @typedef {Object} PackageJson
|
|
* @property {string} version - Clean semver version '1.1.0'
|
|
* @returns {PackageJson}
|
|
*/
|
|
export function readMainPackageJson() {
|
|
const packageJsonPath = path.join(MAIN_PACKAGE_PATH, "package.json");
|
|
|
|
/** @type {string} */
|
|
let jsonStr;
|
|
try {
|
|
jsonStr = fs.readFileSync(packageJsonPath, "utf8");
|
|
} catch (e) {
|
|
if (e.code === "ENOENT") {
|
|
throw Error(`Must run script from repo root dir, package.json not found at ${packageJsonPath}`);
|
|
} else {
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
/** @type {PackageJson} */
|
|
const json = JSON.parse(jsonStr);
|
|
if (!json.version) throw Error(`Empty .version in ${packageJsonPath}`);
|
|
|
|
return json;
|
|
}
|
|
|
|
/**
|
|
* Returns formated details about the commit
|
|
* @param {string} commit
|
|
* @returns {string}
|
|
*/
|
|
export function getCommitDetails(commit) {
|
|
// commit <hash>
|
|
// Author: <author>
|
|
// Date: <author-date>
|
|
//
|
|
// <title-line>
|
|
//
|
|
// <full-commit-message>
|
|
return shell(`git log -n 1 ${commit} --date=relative --pretty=medium`);
|
|
}
|
|
|
|
/**
|
|
* Return the currently checked-out branch
|
|
* @returns {string}
|
|
*/
|
|
export function getCurrentBranch() {
|
|
return shell("git rev-parse --abbrev-ref HEAD");
|
|
}
|
|
|
|
/**
|
|
* Sync remote branches and tags
|
|
* @returns {void}
|
|
*/
|
|
export function syncGitRemote() {
|
|
shell("git fetch -pt");
|
|
}
|
|
|
|
/**
|
|
* Print usage and exit if no args or --help is provided
|
|
*/
|
|
export function usage(helpText) {
|
|
if (process.argv.includes("--help") || process.argv.includes("-h") || !process.argv[2]) {
|
|
console.log(helpText);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Compares tags to find the next, yet unpublished, rc tag
|
|
* @param {string} version clean major.minor.patch version
|
|
* @returns {string}
|
|
*/
|
|
export function getNextRcTag(version) {
|
|
const latestRc = shell(`git tag -l v${version}-rc.*`).split("\n").sort(semver.rcompare)[0];
|
|
if (latestRc) {
|
|
return `v${semver.inc(latestRc, "prerelease")}`;
|
|
}
|
|
return `v${version}-rc.0`;
|
|
}
|