Skip to content

Commit

Permalink
ci: custom check to verify uniform sibling dependency versions hyperl…
Browse files Browse the repository at this point in the history
…edger-cacti#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 hyperledger-cacti#1534

Signed-off-by: Peter Somogyvari <peter.somogyvari@accenture.com>
  • Loading branch information
petermetz authored and takeutak committed Nov 16, 2021
1 parent 80afaf4 commit 3d63b14
Show file tree
Hide file tree
Showing 2 changed files with 138 additions and 1 deletion.
129 changes: 129 additions & 0 deletions tools/custom-checks/check-sibling-dep-version-consistency.ts
Original file line number Diff line number Diff line change
@@ -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];
}
10 changes: 9 additions & 1 deletion tools/custom-checks/run-custom-checks.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
const TAG = "[tools/custom-checks/check-source-code.ts]";
const TAG = "[tools/custom-checks/run-custom-checks.ts]";
let overallSuccess = true;
let overallErrors: string[] = [];

Expand All @@ -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 {
Expand Down

0 comments on commit 3d63b14

Please sign in to comment.