From e29f197478b12ef85928f5b138bd4eca7e500fb5 Mon Sep 17 00:00:00 2001 From: Marat K Date: Wed, 28 Oct 2026 15:04:07 +0100 Subject: [PATCH 1/3] ci: check dependency licenses Co-Authored-By: Claude Sonnet 5.5 --- .github/workflows/licenses.yml | 29 +++++++++++++++++++++++++++++ deny.toml | 12 ++++++++++++ scripts/check-npm-licenses.mjs | 31 +++++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+) create mode 100644 .github/workflows/licenses.yml create mode 100644 deny.toml create mode 100644 scripts/check-npm-licenses.mjs diff --git a/.github/workflows/licenses.yml b/.github/workflows/licenses.yml new file mode 100644 index 0000000..fe462ae --- /dev/null +++ b/.github/workflows/licenses.yml @@ -0,0 +1,29 @@ +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 + + 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 scripts/check-npm-licenses.mjs diff --git a/deny.toml b/deny.toml new file mode 100644 index 0000000..548a82d --- /dev/null +++ b/deny.toml @@ -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", +] diff --git a/scripts/check-npm-licenses.mjs b/scripts/check-npm-licenses.mjs new file mode 100644 index 0000000..893e3fd --- /dev/null +++ b/scripts/check-npm-licenses.mjs @@ -0,0 +1,31 @@ +import { readFileSync } from 'node:fs'; + +const lock = JSON.parse(readFileSync('package-lock.json', 'utf8')); +if (lock.lockfileVersion !== 3 || !lock.packages) { + throw new Error('Expected a package-lock.json with lockfileVersion 3 and packages'); +} + +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)', +]); + +let failed = false; +for (const [path, pkg] of Object.entries(lock.packages)) { + if (path === '') continue; + if (typeof pkg.license !== 'string' || !allowed.has(pkg.license)) { + console.error(`${path}: unapproved or missing license ${JSON.stringify(pkg.license)}`); + failed = true; + } +} +if (failed) process.exitCode = 1; From 6a104d77c2f8daf7e0c895348caa486c5e6cf703 Mon Sep 17 00:00:00 2001 From: Marat K Date: Wed, 28 Oct 2026 15:07:16 +0100 Subject: [PATCH 2/3] test: cover npm license policy Co-Authored-By: Claude Sonnet 5.5 --- .github/workflows/licenses.yml | 1 + scripts/check-npm-licenses.mjs | 32 ++++-------------------------- scripts/npm-license-policy.mjs | 30 ++++++++++++++++++++++++++++ tests/npm-license-policy.test.mjs | 33 +++++++++++++++++++++++++++++++ 4 files changed, 68 insertions(+), 28 deletions(-) create mode 100644 scripts/npm-license-policy.mjs create mode 100644 tests/npm-license-policy.test.mjs diff --git a/.github/workflows/licenses.yml b/.github/workflows/licenses.yml index fe462ae..80e5360 100644 --- a/.github/workflows/licenses.yml +++ b/.github/workflows/licenses.yml @@ -26,4 +26,5 @@ jobs: - 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 diff --git a/scripts/check-npm-licenses.mjs b/scripts/check-npm-licenses.mjs index 893e3fd..e529002 100644 --- a/scripts/check-npm-licenses.mjs +++ b/scripts/check-npm-licenses.mjs @@ -1,31 +1,7 @@ import { readFileSync } from 'node:fs'; +import { checkLicenses } from './npm-license-policy.mjs'; const lock = JSON.parse(readFileSync('package-lock.json', 'utf8')); -if (lock.lockfileVersion !== 3 || !lock.packages) { - throw new Error('Expected a package-lock.json with lockfileVersion 3 and packages'); -} - -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)', -]); - -let failed = false; -for (const [path, pkg] of Object.entries(lock.packages)) { - if (path === '') continue; - if (typeof pkg.license !== 'string' || !allowed.has(pkg.license)) { - console.error(`${path}: unapproved or missing license ${JSON.stringify(pkg.license)}`); - failed = true; - } -} -if (failed) process.exitCode = 1; +const failures = checkLicenses(lock); +for (const failure of failures) console.error(failure); +if (failures.length > 0) process.exitCode = 1; diff --git a/scripts/npm-license-policy.mjs b/scripts/npm-license-policy.mjs new file mode 100644 index 0000000..a0c9a85 --- /dev/null +++ b/scripts/npm-license-policy.mjs @@ -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; +} diff --git a/tests/npm-license-policy.test.mjs b/tests/npm-license-policy.test.mjs new file mode 100644 index 0000000..ff53538 --- /dev/null +++ b/tests/npm-license-policy.test.mjs @@ -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: [] })); +}); From ef5e4ecd303a94e5882123899a4095998cb9d7c3 Mon Sep 17 00:00:00 2001 From: Marat K Date: Wed, 28 Oct 2026 19:43:56 +0100 Subject: [PATCH 3/3] ci: check C ABI fixture licenses Co-Authored-By: Claude Sonnet 5.5 --- .github/workflows/licenses.yml | 10 ++++++++++ tests/cabi-fixture/Cargo.toml | 1 + 2 files changed, 11 insertions(+) diff --git a/.github/workflows/licenses.yml b/.github/workflows/licenses.yml index 80e5360..da084be 100644 --- a/.github/workflows/licenses.yml +++ b/.github/workflows/licenses.yml @@ -18,6 +18,16 @@ jobs: 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 diff --git a/tests/cabi-fixture/Cargo.toml b/tests/cabi-fixture/Cargo.toml index 1e6329b..b469f7f 100644 --- a/tests/cabi-fixture/Cargo.toml +++ b/tests/cabi-fixture/Cargo.toml @@ -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