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 0000000000..1f0ab2cc5a --- /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 f507d911b3..454321b6a2 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 {