Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 5m33s
CI / Build container images (pull_request) Successful in 4m38s
CI / Auth e2e pack (pull_request) Successful in 9m14s
CI / Import/export fidelity gate (pull_request) Successful in 1m12s
CD / Smoke tests against Test (push) Blocked by required conditions
CD / Deploy to Test (push) Blocked by required conditions
CD / Promote to Int (push) Blocked by required conditions
CI / Auth e2e pack (push) Blocked by required conditions
CI / Build container images (push) Blocked by required conditions
CI / Lint, typecheck, test (push) Waiting to run
CI / Import/export fidelity gate (push) Blocked by required conditions
CD / Build and push images (push) Has been cancelled
The release run now generates CycloneDX 1.6 SBOMs with a pinned anchore/syft container — one per released image (scanned from the freshly built image tar, OS packages included) and one for the pnpm workspace (from the lockfile) — plus the full pnpm licenses report, and attaches everything as build artefacts BEFORE publishing the release, so a red gate stops the release. Runner constraints dictated the mechanics (documented in the workflow): the job talks to the HOST daemon, so files travel into the syft container via docker cp and images via docker save to a tar copied the same way (syft cannot read a tar from stdin — verified). scripts/check-licenses.mjs is the documented license policy: permissive allowlist, MPL-2.0/CC-BY-4.0 with recorded reasoning, per-package exception table (khroma: MIT text shipped, metadata missing). CI runs the gate on every PR (pnpm licenses:check); positive and negative case tested locally, both SBOM paths tested against real images/lockfile. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168Ph5uBmHm8X28CSVpbpnJ
86 lines
3.0 KiB
JavaScript
86 lines
3.0 KiB
JavaScript
// License allowlist gate (issue #202): reads `pnpm licenses list --json`
|
|
// from stdin and fails when any dependency's license is outside the
|
|
// allowlist below. The allowlist is the documented policy — extending it is
|
|
// a deliberate, reviewed act, not a build fix.
|
|
//
|
|
// Usage: pnpm licenses list --json | node scripts/check-licenses.mjs
|
|
|
|
// Permissive licenses only, plus two consciously admitted cases:
|
|
// - MPL-2.0: file-level copyleft; we consume MPL packages (axe-core,
|
|
// dev-only) unmodified, which triggers no obligations beyond source
|
|
// availability of the (unmodified) files themselves.
|
|
// - CC-BY-4.0: attribution license used for data packages (browser
|
|
// compatibility data); attribution is satisfied by the license report.
|
|
const ALLOWED = new Set([
|
|
'MIT',
|
|
'MIT-0',
|
|
'ISC',
|
|
'Apache-2.0',
|
|
'BSD-2-Clause',
|
|
'BSD-3-Clause',
|
|
'0BSD',
|
|
'BlueOak-1.0.0',
|
|
'CC0-1.0',
|
|
'CC-BY-4.0',
|
|
'Python-2.0',
|
|
'Unlicense',
|
|
'MPL-2.0',
|
|
'Zlib',
|
|
]);
|
|
|
|
// Packages whose license METADATA is missing or wrong upstream, verified by
|
|
// hand against the shipped license text. Key = package name, value = the
|
|
// verification note an auditor reads.
|
|
const EXCEPTIONS = new Map([
|
|
[
|
|
'khroma',
|
|
'MIT — upstream ships the MIT text as its `license` file but omits the package.json license field (reported as "Unknown")',
|
|
],
|
|
]);
|
|
|
|
/** SPDX-light evaluation, sufficient for the expressions pnpm emits today:
|
|
* parentheses stripped, OR satisfied by any allowed alternative, AND by all
|
|
* parts. Nested mixed expressions would need a real parser — they fail
|
|
* closed here, which is the safe direction. */
|
|
function isAllowed(expression) {
|
|
const clean = expression.replace(/[()]/g, ' ').trim();
|
|
if (/\sOR\s/.test(clean)) {
|
|
return clean.split(/\s+OR\s+/).some((part) => isAllowed(part));
|
|
}
|
|
return clean.split(/\s+AND\s+/).every((part) => ALLOWED.has(part.trim()));
|
|
}
|
|
|
|
const input = await new Promise((resolve, reject) => {
|
|
let data = '';
|
|
process.stdin.setEncoding('utf8');
|
|
process.stdin.on('data', (chunk) => (data += chunk));
|
|
process.stdin.on('end', () => resolve(data));
|
|
process.stdin.on('error', reject);
|
|
});
|
|
|
|
const byLicense = JSON.parse(input);
|
|
const violations = [];
|
|
let packages = 0;
|
|
|
|
for (const [license, entries] of Object.entries(byLicense)) {
|
|
for (const entry of entries) {
|
|
packages += 1;
|
|
if (isAllowed(license)) continue;
|
|
if (EXCEPTIONS.has(entry.name)) continue;
|
|
violations.push({ name: entry.name, license });
|
|
}
|
|
}
|
|
|
|
if (violations.length > 0) {
|
|
console.error('licenses outside the documented allowlist:');
|
|
for (const v of violations) console.error(` ${v.name}: ${v.license}`);
|
|
console.error(
|
|
'Either the dependency goes, or the allowlist/exception table in scripts/check-licenses.mjs is extended in a reviewed change.',
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(
|
|
`license gate: ${packages} packages, ${Object.keys(byLicense).length} distinct license expressions, all within the allowlist (${EXCEPTIONS.size} documented exception${EXCEPTIONS.size === 1 ? '' : 's'})`,
|
|
);
|