Skip to the content

Capsid menu

scripts/check-callers.mjs

62 lines and 3 functions in DrDustinEdwards/devkit, at commit 505cda4 on 2026-10-24. This file on GitHub.

Functions

The functions of check-callers.mjs
FunctionLinesComplexityCoveredCRAPMutation
callerProblems22 to 422100%OkCRAP 2No data: mutation testing has not run on this repo yet
<anonymous:27>27 to 394100%OkCRAP 4No data: mutation testing has not run on this repo yet
<anonymous:61>61 to 6110%OkCRAP 2No 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);
}