Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/scripts/verify-action-pins.mjs
T

109 lines
4.1 KiB
JavaScript

#!/usr/bin/env node
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const WORKFLOW_ROOTS = [".gitea/workflows"];
const COMMIT_SHA_PATTERN = /^[0-9a-f]{40}$/;
const IMAGE_DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/;
const EXACT_SEMVER_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
function inspectExecutablePackagePin(line, relativePath, lineNumber) {
const match = line.match(/\b(?:npx(?:\s+--yes|\s+-y)?|pnpm\s+dlx)\s+(@[^\s@]+\/[^\s@]+|[^\s@]+)@([^\s"']+)/);
if (!match) return null;
const [, packageName, reference] = match;
if (EXACT_SEMVER_PATTERN.test(reference)) {
return { pass: `${relativePath}:${lineNumber}: executable package ${packageName} pinned to ${reference}` };
}
return {
failure: `${relativePath}:${lineNumber}: executable package must use an exact semantic version (${packageName}@${reference})`,
};
}
function listWorkflowFiles(rootDir, relativeDirectory) {
const absoluteDirectory = path.join(rootDir, relativeDirectory);
if (!fs.existsSync(absoluteDirectory)) return [];
return fs.readdirSync(absoluteDirectory, { withFileTypes: true }).flatMap((entry) => {
const relativePath = path.join(relativeDirectory, entry.name);
if (entry.isDirectory()) return listWorkflowFiles(rootDir, relativePath);
return /\.ya?ml$/i.test(entry.name) ? [relativePath] : [];
});
}
export function inspectWorkflowActionPins(source, relativePath) {
const passes = [];
const failures = [];
for (const [index, line] of source.split(/\r?\n/).entries()) {
const executablePin = inspectExecutablePackagePin(line, relativePath, index + 1);
if (executablePin?.pass) passes.push(executablePin.pass);
if (executablePin?.failure) failures.push(executablePin.failure);
const match = line.match(/^\s*-?\s*uses:\s*["']?([^"'#\s]+)["']?/);
if (!match) continue;
const target = match[1];
if (target.startsWith("./")) {
passes.push(`${relativePath}:${index + 1}: local action or reusable workflow`);
continue;
}
if (target.startsWith("docker://")) {
const digest = target.slice(target.lastIndexOf("@") + 1);
if (target.includes("@") && IMAGE_DIGEST_PATTERN.test(digest)) {
passes.push(`${relativePath}:${index + 1}: container action pinned by digest`);
} else {
failures.push(`${relativePath}:${index + 1}: container action must use an immutable sha256 digest (${target})`);
}
continue;
}
const separatorIndex = target.lastIndexOf("@");
const reference = separatorIndex >= 0 ? target.slice(separatorIndex + 1) : "";
if (separatorIndex > 0 && COMMIT_SHA_PATTERN.test(reference)) {
passes.push(`${relativePath}:${index + 1}: remote action pinned to ${reference}`);
} else {
failures.push(`${relativePath}:${index + 1}: remote action must use an immutable 40-character commit SHA (${target})`);
}
}
return { passes, failures };
}
export function verifyActionPins(rootDir = process.cwd()) {
const files = WORKFLOW_ROOTS.flatMap((relativeDirectory) => listWorkflowFiles(rootDir, relativeDirectory));
const result = { passes: [], failures: [] };
for (const relativePath of files) {
const source = fs.readFileSync(path.join(rootDir, relativePath), "utf8");
const fileResult = inspectWorkflowActionPins(source, relativePath);
result.passes.push(...fileResult.passes);
result.failures.push(...fileResult.failures);
}
if (files.length === 0) {
result.failures.push("No Gitea workflow files were found");
}
return result;
}
export function formatActionPinResult(result) {
const status = result.failures.length === 0 ? "PASS" : "FAIL";
return [
`${status} workflow action pin verification`,
...result.failures.map((failure) => `FAIL ${failure}`),
`Summary: ${result.passes.length} pinned/local action references, ${result.failures.length} failures`,
"",
].join("\n");
}
const isCli = process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
if (isCli) {
const result = verifyActionPins();
process.stdout.write(formatActionPinResult(result));
if (result.failures.length > 0) process.exitCode = 1;
}