Files
lodestar/scripts/release/utils.mjs
Nazar Hussain 2ff1784005 chore: remove unused eslint directives (#7803)
**Motivation**

Remove any unused or inactive codebase.

**Description**

Remove all unused eslint directives as we are using `biome` now. 

**Steps to test or reproduce**

Run lint rules and all tests
2025-05-07 17:25:34 +02:00

285 lines
7.5 KiB
JavaScript

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`;
}