Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .github/workflows/licenses.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: License checks

on:
push:
branches: [main]
pull_request:

permissions:
contents: read

jobs:
cargo-licenses:
name: Cargo dependency licenses
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: EmbarkStudios/cargo-deny-action@b66acf5e9fe20f8aba065be86778a8a4c846f902
with:
command: check licenses

fixture-licenses:
name: C ABI fixture dependency licenses
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: EmbarkStudios/cargo-deny-action@b66acf5e9fe20f8aba065be86778a8a4c846f902
with:
manifest-path: ./tests/cabi-fixture/Cargo.toml
command: check licenses

npm-licenses:
name: npm dependency licenses
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '24'
- run: node --test tests/npm-license-policy.test.mjs
- run: node scripts/check-npm-licenses.mjs
12 changes: 12 additions & 0 deletions deny.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[licenses]
confidence-threshold = 0.8
allow = [
"Apache-2.0",
"BSD-2-Clause",
"BSD-3-Clause",
"BSL-1.0",
"ISC",
"MIT",
"Unicode-3.0",
"Zlib",
]
7 changes: 7 additions & 0 deletions scripts/check-npm-licenses.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { readFileSync } from 'node:fs';
import { checkLicenses } from './npm-license-policy.mjs';

const lock = JSON.parse(readFileSync('package-lock.json', 'utf8'));
const failures = checkLicenses(lock);
for (const failure of failures) console.error(failure);
if (failures.length > 0) process.exitCode = 1;
30 changes: 30 additions & 0 deletions scripts/npm-license-policy.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
const allowed = new Set([
'Apache-2.0',
'BSD-2-Clause',
'BSD-3-Clause',
'BSL-1.0',
'ISC',
'MIT',
'Unicode-3.0',
'Zlib',
'MIT OR Apache-2.0',
'Apache-2.0 OR MIT',
'(MIT OR Apache-2.0)',
'(Apache-2.0 OR MIT)',
]);

export function checkLicenses(lock) {
if (lock?.lockfileVersion !== 3 || !lock.packages || typeof lock.packages !== 'object' ||
Array.isArray(lock.packages) || !lock.packages['']) {
throw new Error('Expected a package-lock.json with lockfileVersion 3 and packages');
}

const failures = [];
for (const [path, pkg] of Object.entries(lock.packages)) {
if (path === '') continue;
if (typeof pkg?.license !== 'string' || !allowed.has(pkg.license)) {
failures.push(`${path}: unapproved or missing license ${JSON.stringify(pkg?.license)}`);
}
}
return failures;
}
1 change: 1 addition & 0 deletions tests/cabi-fixture/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
name = "ktav-cabi-fixture"
version = "0.0.0"
edition = "2021"
license = "MIT OR Apache-2.0"
publish = false
# Must match the parent crate's MSRV. This is not cosmetic: without it
# Cargo resolves the fixture's graph with no MSRV awareness and writes a
Expand Down
33 changes: 33 additions & 0 deletions tests/npm-license-policy.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { checkLicenses } from '../scripts/npm-license-policy.mjs';

function lock(packages) {
return { lockfileVersion: 3, packages: { '': {}, ...packages } };
}

test('accepts explicitly approved licenses on nested dependencies', () => {
assert.deepEqual(checkLicenses(lock({
'node_modules/a': { license: 'MIT OR Apache-2.0' },
'node_modules/a/node_modules/b': { license: 'ISC' },
})), []);
});

test('rejects missing, unknown, and unapproved compound licenses', () => {
const failures = checkLicenses(lock({
'node_modules/a': {},
'node_modules/b': { license: 'UNKNOWN' },
'node_modules/c': { license: 'MIT AND GPL-3.0-only' },
'node_modules/d': { license: 'MIT-fake' },
}));
assert.equal(failures.length, 4);
for (const name of ['a', 'b', 'c', 'd']) {
assert.ok(failures.some((failure) => failure.startsWith(`node_modules/${name}:`)));
}
});

test('rejects missing or incompatible lockfile package maps', () => {
assert.throws(() => checkLicenses({ lockfileVersion: 2, packages: {} }));
assert.throws(() => checkLicenses({ lockfileVersion: 3 }));
assert.throws(() => checkLicenses({ lockfileVersion: 3, packages: [] }));
});
Loading