* test(#431): policy-shell-pinning linter — RED baseline (37 violations on origin/next) Adds scripts/workflow-policy.cjs: H1 shell-policy linter with POLICY map, VIOLATION enum, matrix expansion, effective-shell resolution order, and runPolicyLint({ workflowsDir }) entry point. Adds tests/policy-shell-pinning.test.cjs: 8 tests (baseline + 6 synthetic counter-tests). Synthetic tests 2–7 pass; baseline test is intentionally RED (37 violations: 28 in test.yml, 9 in install-smoke.yml — all macos/windows lanes using shell: bash instead of native zsh/pwsh). Adds js-yaml@4.1.1 as devDependency for YAML parsing. * fix(#431): switch ubuntu/windows lanes to native shells; extract bash-isms to Node Remove all explicit shell: bash pins from ubuntu-only jobs (changes, lint-tests, coverage, required-tests, smoke-unpacked) — ubuntu runner default is bash, which is both H1-compliant and the runner default, making the pin redundant. For the test and test-full mixed-OS jobs (ubuntu+windows, windows+macos): - Move bash-ism steps to shell-agnostic Node scripts: scripts/ci-guard-runner.cjs — RUNNER_ENVIRONMENT check scripts/ci-rebase-check.cjs — git fetch+merge PR base branch scripts/check-npm-integrity.cjs — Node port of check-npm-integrity.sh scripts/ci-prepare-test-scope.cjs — write .ci-selected-tests.txt scripts/ci-smoke-skip.cjs — set skip= output for full-only matrix entries - Remove shell: bash from simple npm/node command steps (runner default applies) This brings Windows violations from 19 to 0. Remaining 17 violations are all MACOS_MISSING_EXPLICIT_ZSH in mixed-OS matrix jobs (test-full: windows+macos, install-smoke smoke: ubuntu+macos) — these require job splitting to fix; see BLOCKER in PR description. * fix(#431): update workflow-shell-pinning test for H1 policy The old test required all Windows-targeting npm steps to pin shell: bash (to prevent pwsh stderr-swallow). Under H1, Windows runners must use pwsh (native, no pin needed) — shell: bash on Windows is now the violation, not the fix. Update findViolations() to flag npm steps with effectiveShell === 'bash' (rather than effectiveShell === null). Update synthetic tests to verify the H1-inverted semantics: defaults.run.shell: bash on Windows is now 2 violations, not 0. Update test name and assertion messages to describe the H1 constraint rather than the old missing-pin constraint. * fix(#431): extend policy linter to resolve matrix.shell expressions - expandRunsOn now captures all matrix.include row keys as realization context (os, node-version, shell, full_only, etc.) instead of only os - effectiveShell now accepts a realizationContext and resolves ${{ matrix.<key> }} expressions against it before checking policy - Unresolvable matrix key in shell expression emits UNRESOLVABLE_MATRIX - Add 3 new tests: positive (zsh+pwsh per row → 0 violations), counter (bash in macOS row → WRONG_SHELL_FOR_OS), counter (missing shell key → UNRESOLVABLE_MATRIX) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(#431): apply matrix.shell pattern to test-full and smoke jobs (clears BLOCKER) test-full job (test.yml): - Add shell: pwsh/zsh per matrix.include row (windows-latest→pwsh, macos-latest→zsh) - Add job-level defaults.run.shell: ${{ matrix.shell }} - No step-level shell pins existed to remove smoke job (install-smoke.yml): - Add shell: bash/zsh per matrix.include row (ubuntu→bash, macos→zsh) - Add job-level defaults.run.shell: ${{ matrix.shell }} - No step-level shell pins existed to remove Policy linter now reports 0 violations across all workflow files. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(#431): migrate .sh check scripts to .cjs; remove .sh originals - Add scripts/check-env.cjs: Node.js port of check-env.sh with identical exit codes (0/1/2), human-readable and --json output, --help flag, and all 5 checks (node-version, npm-version, lockfile-present, lockfile-sync, version-manager-pin) - Migrate all callers: - package.json check:env → node scripts/check-env.cjs - package.json check:integrity → node scripts/check-npm-integrity.cjs - scripts/ci-test-scope.cjs path strings → .cjs equivalents - .github/workflows/release.yml rc+finalize jobs → node .cjs (drop chmod+x) - .github/workflows/security-scan.yml → node .cjs (drop chmod+x) - tests/check-env.test.cjs → spawn node process.execPath [.cjs] - tests/npm-integrity-gate.test.cjs → spawn node process.execPath [.cjs] - Delete scripts/check-env.sh and scripts/check-npm-integrity.sh Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(#431): update doc references from .sh to .cjs Update SECURITY.md and docs/contributing/bootstrap.md to reference the canonical Node invocation instead of the removed bash scripts. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(#431): use per-step shell:matrix.shell instead of defaults.run.shell (GHA compat) GHA does not reliably resolve matrix expressions inside defaults.run.shell. Per-step shell: always resolves correctly. Removed the defaults.run.shell block from the test-full job (test.yml) and the smoke job (install-smoke.yml), and added shell: \${{ matrix.shell }} directly on every run: step in both jobs. Codex finding: defaults.run.shell with matrix expressions is not a GHA-supported pattern; per-step shell: is the safe form. * fix(#431): policy linter validates every matrix.include row independently Removed runner-label-only dedup from expandRunsOn() in workflow-policy.cjs. The prior guard (if !realizations.find(r => r.runner === runner)) collapsed two macos-latest rows with different node-version/shell contexts into one, hiding the second row's policy violation. Each matrix.include row is a distinct CI realization with its own context; validating it twice is harmless but skipping it causes false negatives. Added counter-test (Test 8) in tests/policy-shell-pinning.test.cjs: two macos-latest rows (shell:zsh compliant + shell:bash violation) must produce exactly one WRONG_SHELL_FOR_OS violation on the second row. * fix(#431): remove dedup-by-runner in Cartesian matrix.<key> expansion (Codex round 3) The base-list path in expandRunsOn (matrix.<key> arrays, e.g. matrix.os) previously guarded each push with `if (!realizations.find(r => r.runner === runner))`, collapsing duplicate runner values into a single realization and hiding policy violations on later rows of a Cartesian matrix. Remove the guard unconditionally; each entry in the base-list array now produces its own realization, matching the same fix already applied to the matrix.include path. Add counter-test "Cartesian matrix os × shell — dedup must not collapse rows by runner alone": matrix.os: [macos-latest, macos-latest] + shell: ${{ matrix.shell }} now yields 2 realizations (not 1). Documents that Cartesian cross-product expansion (carrying all keys into realization context) is a separate follow-up; current violations are UNRESOLVABLE_MATRIX pending that work. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(#431): remove 60s timeout regression on npm ci --dry-run (parity with check-env.sh) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(#431): ci-rebase-check.cjs — return truthy sentinel on success (Codex round 4) run() used execFileSync with stdio:'inherit', which returns null on success. Caller checked `result !== null`, always false → every successful fetch fell through to "failed after 3 attempts" exit-1 path. Fix: run() now returns true on success, false on failure. Update caller from `result !== null` to `if (result)`. Adds tests/ci-rebase-check.test.cjs (5 tests) covering the sentinel contract and a local-bare-remote integration smoke that verifies the full fetch+merge path exits 0 when fetch succeeds. --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: CI Rebase Check <ci@gsd-redux>
446 lines
15 KiB
JavaScript
446 lines
15 KiB
JavaScript
'use strict';
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const yaml = require('js-yaml');
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Policy: native shell per OS
|
|
// ---------------------------------------------------------------------------
|
|
const POLICY = Object.freeze({
|
|
'ubuntu-latest': 'bash',
|
|
'ubuntu-22.04': 'bash',
|
|
'ubuntu-24.04': 'bash',
|
|
'macos-latest': 'zsh',
|
|
'macos-13': 'zsh',
|
|
'macos-14': 'zsh',
|
|
'macos-15': 'zsh',
|
|
'windows-latest': 'pwsh',
|
|
'windows-2022': 'pwsh',
|
|
'windows-2025': 'pwsh',
|
|
});
|
|
|
|
const VIOLATION = Object.freeze({
|
|
WRONG_SHELL_FOR_OS: 'wrong_shell_for_os',
|
|
MACOS_MISSING_EXPLICIT_ZSH: 'macos_missing_explicit_zsh',
|
|
UNKNOWN_RUNNER: 'unknown_runner',
|
|
UNRESOLVABLE_MATRIX: 'unresolvable_matrix',
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Runner default (GitHub Actions documented defaults, not policy)
|
|
// ---------------------------------------------------------------------------
|
|
function runnerDefault(runner) {
|
|
if (!runner) return null;
|
|
if (runner.startsWith('windows-')) return 'pwsh';
|
|
return 'bash'; // ubuntu-* and macos-* both default to bash on GHA
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Matrix expansion
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Expand a runs-on expression against a job's strategy.matrix.
|
|
* Returns an array of { runner: string, resolvable: boolean, context: object }
|
|
* objects where `context` holds ALL key→value pairs for the realization row
|
|
* (so shell expressions like ${{ matrix.shell }} can be resolved against it).
|
|
* 'resolvable: false' means the expression was an unresolved matrix ref.
|
|
*/
|
|
function expandRunsOn(runsOnRaw, matrix) {
|
|
if (!runsOnRaw) return [];
|
|
|
|
const raw = String(runsOnRaw).trim();
|
|
|
|
// Detect matrix expression: ${{ matrix.X }} or ${{ matrix['X'] }}
|
|
const matrixExprRe = /\$\{\{\s*matrix\.(\w+)\s*\}\}/;
|
|
const match = raw.match(matrixExprRe);
|
|
|
|
if (!match) {
|
|
// Literal runner label
|
|
return [{ runner: raw, resolvable: true, context: {} }];
|
|
}
|
|
|
|
const key = match[1];
|
|
|
|
if (!matrix) {
|
|
return [{ runner: raw, resolvable: false, context: {} }];
|
|
}
|
|
|
|
const realizations = [];
|
|
|
|
// matrix.include entries carry complete row context — prefer them as they
|
|
// contain all keys (os, node-version, shell, full_only, etc.).
|
|
// Each include row is a distinct CI realization and must be validated
|
|
// independently — even if two rows share the same runner label, their
|
|
// contexts (and therefore effective shells) may differ.
|
|
if (Array.isArray(matrix.include)) {
|
|
for (const entry of matrix.include) {
|
|
if (entry && entry[key] != null) {
|
|
const runner = String(entry[key]);
|
|
// Clone all keys from include row as the realization context
|
|
const context = {};
|
|
for (const [k, v] of Object.entries(entry)) {
|
|
context[k] = v != null ? String(v) : '';
|
|
}
|
|
realizations.push({ runner, resolvable: true, context });
|
|
}
|
|
}
|
|
}
|
|
|
|
// Collect values from matrix.<key> list (e.g. matrix.os: [ubuntu, macos])
|
|
// These base-list entries have no extra context beyond the key itself.
|
|
// Each entry is pushed unconditionally — deduplicating by runner alone
|
|
// would collapse distinct Cartesian rows (e.g. duplicate os values paired
|
|
// with different shell values) and hide policy violations on later rows.
|
|
if (Array.isArray(matrix[key])) {
|
|
for (const val of matrix[key]) {
|
|
const runner = String(val);
|
|
realizations.push({ runner, resolvable: true, context: { [key]: runner } });
|
|
}
|
|
}
|
|
|
|
// matrix.exclude: remove matches
|
|
if (Array.isArray(matrix.exclude)) {
|
|
for (const excl of matrix.exclude) {
|
|
if (excl && excl[key] != null) {
|
|
const exclRunner = String(excl[key]);
|
|
const idx = realizations.findIndex(r => r.runner === exclRunner);
|
|
if (idx !== -1) realizations.splice(idx, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (realizations.length === 0) {
|
|
// Could not resolve — no concrete values found
|
|
return [{ runner: raw, resolvable: false, context: {} }];
|
|
}
|
|
|
|
return realizations;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Matrix expression resolution
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* If `expr` is a `${{ matrix.<key> }}` expression, look up the value in
|
|
* `realizationContext` (a plain-object snapshot of one matrix.include row).
|
|
* Returns:
|
|
* { resolved: true, value: string } — expression resolved to a concrete value
|
|
* { resolved: false, key: string } — matrix key absent in this realization
|
|
* null — `expr` is not a matrix expression
|
|
*/
|
|
function resolveMatrixExpr(expr, realizationContext) {
|
|
if (!expr || typeof expr !== 'string') return null;
|
|
const m = expr.match(/^\s*\$\{\{\s*matrix\.(\w+)\s*\}\}\s*$/);
|
|
if (!m) return null;
|
|
const key = m[1];
|
|
if (!realizationContext || !(key in realizationContext)) {
|
|
return { resolved: false, key };
|
|
}
|
|
return { resolved: true, value: String(realizationContext[key]) };
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Effective-shell resolution
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Given a step's shell, job defaults, workflow defaults, runner, and the
|
|
* current matrix realization context, return the effective shell that will
|
|
* actually execute.
|
|
*
|
|
* Matrix expressions (`${{ matrix.shell }}`) in any shell field are resolved
|
|
* against `realizationContext` (a plain object of key→value for the current
|
|
* matrix.include row).
|
|
*
|
|
* Returns:
|
|
* { shell: string, unresolvable: false } — concrete shell value
|
|
* { shell: null, unresolvable: true, key: string } — matrix expr present but key missing
|
|
*/
|
|
function effectiveShell(stepShell, jobDefaultsShell, workflowDefaultsShell, runner, realizationContext) {
|
|
for (const raw of [stepShell, jobDefaultsShell, workflowDefaultsShell]) {
|
|
if (!raw) continue;
|
|
const mx = resolveMatrixExpr(raw, realizationContext);
|
|
if (mx !== null) {
|
|
// It's a matrix expression
|
|
if (!mx.resolved) {
|
|
return { shell: null, unresolvable: true, key: mx.key };
|
|
}
|
|
return { shell: mx.value, unresolvable: false };
|
|
}
|
|
// Literal value
|
|
return { shell: raw, unresolvable: false };
|
|
}
|
|
// Nothing set at any level — use runner default
|
|
return { shell: runnerDefault(runner), unresolvable: false };
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Violation detection
|
|
// ---------------------------------------------------------------------------
|
|
/**
|
|
* Determines whether a step/runner combination violates shell policy.
|
|
*
|
|
* `rawStepShell`, `rawJobDefaultsShell`, `rawWorkflowDefaultsShell` are the
|
|
* raw (possibly matrix-expression) values before resolution. They're used
|
|
* only for the MACOS_MISSING_EXPLICIT_ZSH sub-classification: that violation
|
|
* fires only when nothing is set at any level (all three are null/empty AND
|
|
* the runner default is wrong).
|
|
*/
|
|
function detectViolation(runner, resolvedShell, rawStepShell, rawJobDefaultsShell, rawWorkflowDefaultsShell) {
|
|
if (!(runner in POLICY)) {
|
|
return VIOLATION.UNKNOWN_RUNNER;
|
|
}
|
|
const expected = POLICY[runner];
|
|
if (resolvedShell !== expected) {
|
|
// Specific subtype for macOS missing explicit zsh:
|
|
// fires only when no shell is set at any level (inherited runner default).
|
|
if (runner.startsWith('macos-') && !rawStepShell && !rawJobDefaultsShell && !rawWorkflowDefaultsShell) {
|
|
return VIOLATION.MACOS_MISSING_EXPLICIT_ZSH;
|
|
}
|
|
return VIOLATION.WRONG_SHELL_FOR_OS;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Source map: find line numbers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Find the line number of a string in YAML text.
|
|
* Returns 1-based line number of the first occurrence at or after startLine.
|
|
*/
|
|
function findLineNumber(yamlText, searchStr, startLine) {
|
|
const lines = yamlText.split('\n');
|
|
const start = Math.max(0, (startLine || 1) - 1);
|
|
for (let i = start; i < lines.length; i++) {
|
|
if (lines[i].includes(searchStr)) {
|
|
return i + 1;
|
|
}
|
|
}
|
|
// Fall back to scanning from beginning
|
|
for (let i = 0; i < lines.length; i++) {
|
|
if (lines[i].includes(searchStr)) {
|
|
return i + 1;
|
|
}
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Core inspector
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* inspectWorkflow(yamlText, { filePath }) → structured inspection result
|
|
*/
|
|
function inspectWorkflow(yamlText, { filePath = '<unknown>' } = {}) {
|
|
let doc;
|
|
try {
|
|
doc = yaml.load(yamlText, { schema: yaml.DEFAULT_SCHEMA });
|
|
} catch (e) {
|
|
return {
|
|
filePath,
|
|
jobs: [],
|
|
workflowDefaultsShell: null,
|
|
parseError: e.message,
|
|
};
|
|
}
|
|
|
|
if (!doc || typeof doc !== 'object') {
|
|
return { filePath, jobs: [], workflowDefaultsShell: null };
|
|
}
|
|
|
|
const workflowDefaultsShell =
|
|
doc.defaults?.run?.shell ?? null;
|
|
|
|
const jobs = [];
|
|
|
|
for (const [jobId, jobDef] of Object.entries(doc.jobs || {})) {
|
|
if (!jobDef || typeof jobDef !== 'object') continue;
|
|
|
|
const runsOnRaw = jobDef['runs-on'];
|
|
const matrix = jobDef.strategy?.matrix ?? null;
|
|
const jobDefaultsShell = jobDef.defaults?.run?.shell ?? null;
|
|
|
|
const runsOnStr = runsOnRaw != null ? String(runsOnRaw) : '';
|
|
const runsOnExpressions = [runsOnStr];
|
|
const runnerRealizations = expandRunsOn(runsOnStr, matrix);
|
|
|
|
const steps = [];
|
|
|
|
for (const [stepIndex, step] of (jobDef.steps || []).entries()) {
|
|
if (!step || typeof step !== 'object') continue;
|
|
|
|
// Only check steps that actually run shell scripts (have `run:`)
|
|
if (!step.run) continue;
|
|
|
|
const stepShell = step.shell ?? null;
|
|
const stepName = step.name ?? `step-${stepIndex}`;
|
|
|
|
for (const { runner, resolvable, context: realizationContext } of runnerRealizations) {
|
|
if (!resolvable) {
|
|
// Can't resolve runner — emit UNRESOLVABLE_MATRIX
|
|
const lineNum = findLineNumber(yamlText, stepName !== `step-${stepIndex}` ? stepName : String(step.run).slice(0, 20));
|
|
steps.push({
|
|
index: stepIndex,
|
|
name: stepName,
|
|
stepShell,
|
|
effectiveShell: null,
|
|
runner,
|
|
violation: VIOLATION.UNRESOLVABLE_MATRIX,
|
|
evidence: {
|
|
line: lineNum,
|
|
snippet: `runs-on: ${runsOnStr} (unresolvable matrix expression)`,
|
|
},
|
|
});
|
|
continue;
|
|
}
|
|
|
|
const effResult = effectiveShell(stepShell, jobDefaultsShell, workflowDefaultsShell, runner, realizationContext);
|
|
|
|
// If a matrix expression referenced a key not present in this realization row
|
|
if (effResult.unresolvable) {
|
|
const lineNum = findLineNumber(yamlText, stepName !== `step-${stepIndex}` ? stepName : String(step.run).slice(0, 20));
|
|
steps.push({
|
|
index: stepIndex,
|
|
name: stepName,
|
|
stepShell,
|
|
effectiveShell: null,
|
|
runner,
|
|
violation: VIOLATION.UNRESOLVABLE_MATRIX,
|
|
evidence: {
|
|
line: lineNum,
|
|
snippet: `matrix.${effResult.key} not present in realization for runner=${runner}`,
|
|
},
|
|
});
|
|
continue;
|
|
}
|
|
|
|
const eff = effResult.shell;
|
|
const violation = detectViolation(runner, eff, stepShell, jobDefaultsShell, workflowDefaultsShell);
|
|
|
|
// Find evidence line: prefer step name, then shell:, then run: content
|
|
let evidenceLine = 1;
|
|
let evidenceSnippet = '';
|
|
|
|
if (stepName !== `step-${stepIndex}`) {
|
|
evidenceLine = findLineNumber(yamlText, stepName);
|
|
evidenceSnippet = `name: ${stepName}`;
|
|
} else if (stepShell) {
|
|
evidenceLine = findLineNumber(yamlText, `shell: ${stepShell}`);
|
|
evidenceSnippet = `shell: ${stepShell}`;
|
|
} else {
|
|
const runSnippet = String(step.run).split('\n')[0].slice(0, 40);
|
|
evidenceLine = findLineNumber(yamlText, runSnippet);
|
|
evidenceSnippet = runSnippet;
|
|
}
|
|
|
|
steps.push({
|
|
index: stepIndex,
|
|
name: stepName,
|
|
stepShell,
|
|
effectiveShell: eff,
|
|
runner,
|
|
violation: violation ?? null,
|
|
evidence: {
|
|
line: evidenceLine,
|
|
snippet: evidenceSnippet,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
const resolvedRunners = runnerRealizations
|
|
.filter(r => r.resolvable)
|
|
.map(r => r.runner);
|
|
|
|
jobs.push({
|
|
jobId,
|
|
runsOnExpressions,
|
|
resolvedRunners,
|
|
defaultsShell: jobDefaultsShell,
|
|
steps,
|
|
});
|
|
}
|
|
|
|
return {
|
|
filePath,
|
|
jobs,
|
|
workflowDefaultsShell,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* inspectWorkflowFile(absPath) — reads file from disk and calls inspectWorkflow.
|
|
*/
|
|
function inspectWorkflowFile(absPath) {
|
|
const text = fs.readFileSync(absPath, 'utf8');
|
|
return inspectWorkflow(text, { filePath: absPath });
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// runPolicyLint
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* runPolicyLint({ workflowsDir }) → { violations, summary }
|
|
*/
|
|
function runPolicyLint({ workflowsDir }) {
|
|
const absDir = path.resolve(workflowsDir);
|
|
const files = fs.readdirSync(absDir)
|
|
.filter(f => f.endsWith('.yml') || f.endsWith('.yaml'))
|
|
.map(f => path.join(absDir, f))
|
|
.sort();
|
|
|
|
const violations = [];
|
|
|
|
for (const filePath of files) {
|
|
const result = inspectWorkflowFile(filePath);
|
|
for (const job of result.jobs) {
|
|
for (const step of job.steps) {
|
|
if (step.violation) {
|
|
violations.push({
|
|
filePath: result.filePath,
|
|
jobId: job.jobId,
|
|
stepIndex: step.index,
|
|
stepName: step.name,
|
|
runner: step.runner,
|
|
effectiveShell: step.effectiveShell,
|
|
stepShell: step.stepShell,
|
|
violation: step.violation,
|
|
evidence: step.evidence,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const perViolationType = {};
|
|
for (const v of violations) {
|
|
perViolationType[v.violation] = (perViolationType[v.violation] || 0) + 1;
|
|
}
|
|
|
|
return {
|
|
violations,
|
|
summary: {
|
|
total: violations.length,
|
|
perViolationType,
|
|
},
|
|
};
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Exports
|
|
// ---------------------------------------------------------------------------
|
|
module.exports = {
|
|
POLICY,
|
|
VIOLATION,
|
|
inspectWorkflow,
|
|
inspectWorkflowFile,
|
|
runPolicyLint,
|
|
};
|