scripts/check-callers.mjs
62 lines and 3 functions in DrDustinEdwards/devkit, at commit 505cda4 on 2026-10-24. This file on GitHub.
Functions
| Function | Lines | Complexity | Covered | CRAP | Mutation |
|---|---|---|---|---|---|
| callerProblems | 22 to 42 | 2 | 100% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:27> | 27 to 39 | 4 | 100% | OkCRAP 4 | No data: mutation testing has not run on this repo yet |
| <anonymous:61> | 61 to 61 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
Imports
It imports no other module of this repo. No module of this repo imports it.
Source
#!/usr/bin/env node
// The check a caller of this repository's reusable workflows runs on its own workflow files:
//
// node check-callers.mjs .github/workflows/no-bloat.yml [more files]
//
// Every `uses:` that names this repository's reusable workflows must end in `@` and a full 40-character commit sha: a branch
// or a tag moves under a caller without a reviewed change to the caller. `secrets: inherit` is refused too, so a reader can
// see exactly which secrets the shared file receives.
//
// Exit 0 when every call is pinned, 1 when any is not, 2 when a file is unreadable or names no call at all.
import { readFileSync } from "node:fs";
import { pathToFileURL } from "node:url";
// A quoted value is the same value to YAML, so a quote around it is read through.
const SHARED = /^\s*uses:\s*["']?(?<repo>[\w.-]+)\/devkit\/\.github\/workflows\/(?<file>[\w.-]+)@(?<ref>[^\s"']+)/;
/**
* @param {string} text
* @returns {{ line: number, problem: string }[]}
*/
export function callerProblems(text) {
/** @type {{ line: number, problem: string }[]} */
const problems = [];
const lines = text.replace(/\r\n/g, "\n").split("\n");
let calls = 0;
lines.forEach((raw, index) => {
const line = raw.replace(/\s+#.*$/, "");
const call = SHARED.exec(line);
if (call?.groups) {
calls += 1;
if (!/^[0-9a-f]{40}$/.test(call.groups.ref)) {
problems.push({ line: index + 1, problem: `pinned to "${call.groups.ref}", not a full 40-character commit sha` });
}
}
if (/^\s*secrets:\s*(["']?)inherit\1\s*$/.test(line)) {
problems.push({ line: index + 1, problem: "secrets: inherit hides what the shared workflow receives; pass each secret by name" });
}
});
if (calls === 0) problems.push({ line: 0, problem: "no call to a devkit reusable workflow" });
return problems;
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
const files = process.argv.slice(2);
if (files.length === 0) {
console.error("usage: check-callers.mjs <workflow file>...");
process.exit(2);
}
let status = 0;
for (const file of files) {
let text;
try {
text = readFileSync(file, "utf8");
} catch (error) {
console.error(`${file}: cannot read: ${error instanceof Error ? error.message : error}`);
process.exit(2);
}
const problems = callerProblems(text);
for (const { line, problem } of problems) console.error(`${file}${line ? `:${line}` : ""}: ${problem}`);
if (problems.some((p) => p.line === 0)) status = Math.max(status, 2);
else if (problems.length > 0) status = Math.max(status, 1);
else console.log(`${file}: ok`);
}
process.exit(status);
}