From 9e44ab6d5a816972b1dfcc6f3f60d7a858f5e851 Mon Sep 17 00:00:00 2001 From: Peter Somogyvari Date: Mon, 15 Nov 2021 16:22:22 -0800 Subject: [PATCH] ci: custom check to verify uniform sibling dependency versions #1534 With this, the `yarn custom-checks` script will go through all the package.json files and validate the following things: 1. The top level "version" property in the package.json of the package is set to a value that equals whatever is set as the project-wide version of the monorepo as defined by the lerna.json file's "version" property. (the latter gets automatically bumped by the relese process) 2. Verifies that all dependencies within each package.json pointing to sibling packages are also using the correct version from lerna.json as described above. 3. Same check as in 2) but for the "devDependencies" section of each package.json file within the monorepo. The upside of this custom check is that maintainers no longer need to manually ensure that the version numbers are kept up to date which was especially tricky when a new package is being added in a pull request that has been open for a while and we issued one or more releases during the lifetime of the pull request. Fixes #1534 Signed-off-by: Peter Somogyvari Signed-off-by: Youngone Lee --- .../check-sibling-dep-version-consistency.ts | 129 ++++++++++++++++++ tools/custom-checks/run-custom-checks.ts | 10 +- 2 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 tools/custom-checks/check-sibling-dep-version-consistency.ts diff --git a/tools/custom-checks/check-sibling-dep-version-consistency.ts b/tools/custom-checks/check-sibling-dep-version-consistency.ts new file mode 100644 index 00000000000..1f0ab2cc5a3 --- /dev/null +++ b/tools/custom-checks/check-sibling-dep-version-consistency.ts @@ -0,0 +1,129 @@ +import fs from "fs-extra"; +import path from "path"; +import { fileURLToPath } from "url"; +import { globby, Options as GlobbyOptions } from "globby"; +import { RuntimeError } from "run-time-error"; +import { isStdLibRecord } from "./is-std-lib-record"; + +export interface ICheckSiblingDepVersionConsistencyRequest { + readonly argv: string[]; + readonly env: NodeJS.ProcessEnv; + /** + * The version that will be used as the correct one when checking the sibling + * package dependency version declarations. + * If you omit this (optional) parameter then the root package.json file + * will be parsed to obtain its value at runtime. + */ + readonly version?: string; +} + +/** + * Verifies that each sibling dependency is up to date with the latest version + * that was released at any given time. + * Note: This only checks dependency versions for the packages that are hosted + * within this monorepo (Hyperledger Cactus) not dependencies in general. + * + * For example if the cmd-api-server package depends on the common package and + * currently the project is on release v1.2.3 then the dependency declaration of + * the package.json file of the cmd-api-server package should also use this + * v1.2.3 version not something outdated such as v0.3.1 because that may cause + * (according to our experience) strange build issues that confuse people a lot + * for no good reason. + * + * @returns An array with the first item being a boolean indicating + * 1) success (`true`) or 2) failure (`false`) + */ +export async function checkSiblingDepVersionConsistency( + req: ICheckSiblingDepVersionConsistencyRequest, +): Promise<[boolean, string[]]> { + const TAG = "[tools/check-sibling-dep-version-consistency.ts]"; + const __filename = fileURLToPath(import.meta.url); + const __dirname = path.dirname(__filename); + const SCRIPT_DIR = __dirname; + const PROJECT_DIR = path.join(SCRIPT_DIR, "../../"); + console.log(`${TAG} SCRIPT_DIR=${SCRIPT_DIR}`); + console.log(`${TAG} PROJECT_DIR=${PROJECT_DIR}`); + + if (!req) { + throw new RuntimeError(`req parameter cannot be falsy.`); + } + if (!req.argv) { + throw new RuntimeError(`req.argv cannot be falsy.`); + } + if (!req.env) { + throw new RuntimeError(`req.env cannot be falsy.`); + } + + const globbyOpts: GlobbyOptions = { + cwd: PROJECT_DIR, + ignore: ["**/node_modules"], + }; + + const DEFAULT_GLOB = "**/cactus-*/package.json"; + + const pkgJsonPaths = await globby(DEFAULT_GLOB, globbyOpts); + console.log(`${TAG} package.json paths: (${pkgJsonPaths.length}): `); + + const lernaJsonPathAbs = path.join(PROJECT_DIR, "./lerna.json"); + console.log(`${TAG} Reading root lerna.json at ${lernaJsonPathAbs}`); + const lernaJson = await fs.readJSON(lernaJsonPathAbs); + const correctVersion = req.version || lernaJson.version; + console.log(`${TAG} Correct Version: ${correctVersion}`); + + const errors: string[] = []; + + const checks = pkgJsonPaths.map(async (pathRel) => { + const filePathAbs = path.join(PROJECT_DIR, pathRel); + const pkgJson: unknown = await fs.readJSON(filePathAbs); + if (typeof pkgJson !== "object") { + errors.push(`ERROR: ${pathRel} package.json cannot be empty.`); + return; + } + if (!pkgJson) { + errors.push(`ERROR: ${pathRel} package.json cannot be empty.`); + return; + } + if (!isStdLibRecord(pkgJson)) { + return; + } + + const { dependencies, devDependencies, version } = pkgJson; + + if (version !== correctVersion) { + const msg = `ERROR: ${pathRel} the package itself incorrectly has version ${version}. Expected ${correctVersion}`; + errors.push(msg); + } + + if (isStdLibRecord(dependencies)) { + Object.entries(dependencies).forEach(([depName, depVersion]) => { + if (!depName.startsWith("@hyperledger/cactus-")) { + return; + } + if (depVersion !== correctVersion) { + const msg = `ERROR: ${pathRel} dependencies.${depName} incorrectly has version ${depVersion}. Expected ${correctVersion}`; + errors.push(msg); + } + }); + } else { + console.log(`${TAG} ${pathRel} has no "dependencies". Skipping...`); + } + + if (isStdLibRecord(devDependencies)) { + Object.entries(devDependencies).forEach(([depName, depVersion]) => { + if (!depName.startsWith("@hyperledger/cactus-")) { + return; + } + if (depVersion !== correctVersion) { + const msg = `ERROR: ${pathRel} devDependencies.${depName} incorrectly has version ${depVersion}. Expected ${correctVersion}`; + errors.push(msg); + } + }); + } else { + console.log(`${TAG} ${pathRel} has no "devDependencies". Skipping...`); + } + }); + + await Promise.all(checks); + + return [errors.length === 0, errors]; +} diff --git a/tools/custom-checks/run-custom-checks.ts b/tools/custom-checks/run-custom-checks.ts index f507d911b34..454321b6a2e 100644 --- a/tools/custom-checks/run-custom-checks.ts +++ b/tools/custom-checks/run-custom-checks.ts @@ -1,12 +1,13 @@ import esMain from "es-main"; import { checkOpenApiJsonSpecs } from "./check-open-api-json-specs"; +import { checkSiblingDepVersionConsistency } from "./check-sibling-dep-version-consistency"; export async function runCustomChecks( argv: string[], env: NodeJS.ProcessEnv, version: string, ): Promise { - const TAG = "[tools/custom-checks/check-source-code.ts]"; + const TAG = "[tools/custom-checks/run-custom-checks.ts]"; let overallSuccess = true; let overallErrors: string[] = []; @@ -23,6 +24,13 @@ export async function runCustomChecks( overallSuccess = overallSuccess && success; } + { + const req = { argv, env }; + const [success, errors] = await checkSiblingDepVersionConsistency(req); + overallErrors = overallErrors.concat(errors); + overallSuccess = overallSuccess && success; + } + if (!overallSuccess) { overallErrors.forEach((it) => console.error(it)); } else {