Back to core workflows Fix dependency resolution Tune package metadata Validate before publishing

Checking Dependency Licenses in CI

Every package you install arrives with a licence, and some licences impose obligations your product cannot meet: copyleft terms that require releasing source, non-commercial clauses, or no licence at all, which legally means no permission to use. A single transitive dependency can introduce one of these without anyone noticing, because licence changes are rarely mentioned in changelogs. Checking licences automatically in CI turns that invisible risk into a failing build with a clear message. This guide builds an allowlist-based licence check, explains how licence data is read from node_modules, and handles the exceptions every real project needs.

What goes wrong without a check

Licence problems surface late and expensively: in a customer's procurement review, in an acquisition's due diligence, or when a compliance scan in a downstream pipeline blocks a release. The failures look like this:

# A downstream compliance scanner
✖ some-chart-lib@4.2.0   License: AGPL-3.0-only   Policy: DENIED (network copyleft)
✖ tiny-color-util@1.0.3  License: UNKNOWN         Policy: DENIED (no license declared)
✖ pdf-parser-lite@2.1.0  License: SEE LICENSE IN LICENSE.md  Policy: REVIEW

Each of these arrived as a transitive dependency of something reasonable. some-chart-lib changed licence in a minor release. tiny-color-util never declared one. pdf-parser-lite points at a file whose contents a machine cannot classify. The goal of a CI check is to catch all three at the pull request that introduces them. Licence review belongs alongside the security and update checks in Dependency Auditing and Automated Updates.

How licence data is collected

Tools read the license field of each installed package's package.json, which should be an SPDX expression such as MIT, Apache-2.0 or (MIT OR Apache-2.0). When the field is missing or non-standard, they fall back to scanning LICENSE files and guessing. The check then compares each package's licence with a policy.

A licence check in CI The check walks the installed dependency tree, reads each package's SPDX license field, evaluates it against an allowlist, and fails on anything denied or unknown. installed tree production dependencies only read license field SPDX expression per package evaluate policy allowlist + reviewed exceptions pass or fail list denied and unknown packages
The allowlist is the policy; exceptions are explicit, per package and version, and reviewed like code.

Scope matters. Licence obligations usually attach to what you distribute or run in production, so most policies check production dependencies only (--omit=dev), while a separate, looser report covers development tooling. Check your own policy with your legal team; the tooling supports either.

Setting up the check

license-checker-rseidelsohn, a maintained fork of the original license-checker, works with npm, pnpm and Yarn installs:

npm install -D license-checker-rseidelsohn
{
  "scripts": {
    "licenses:check": "license-checker-rseidelsohn --production --excludePrivatePackages --onlyAllow \"MIT;ISC;BSD-2-Clause;BSD-3-Clause;Apache-2.0;0BSD;BlueOak-1.0.0;CC0-1.0;Unlicense;Python-2.0\" --excludePackages \"$(node scripts/license-exceptions.mjs)\"",
    "licenses:report": "license-checker-rseidelsohn --production --csv --out licenses.csv"
  }
}

Keep exceptions in a reviewed file rather than a long command line:

// scripts/license-exceptions.mjs
// Each entry: exact package@version, reason, reviewer, date. Printed as a semicolon list.
const exceptions = [
  // Dual-licensed; we use it under MIT per the README. Reviewed 2026-02-10.
  'pdf-parser-lite@2.1.0',
  // Internal package published without a license field. Owned by platform team.
  '@acme/legacy-config@3.0.1',
];
console.log(exceptions.join(';'));

pnpm also has a built-in report: pnpm licenses list --prod --json prints every production dependency grouped by licence, which is easy to evaluate with a short script if you prefer not to add a tool.

Licence categories and a typical default policy Groups common licences into permissive, weak copyleft, strong or network copyleft, and missing or custom, with a default policy for each. Examples Typical policy Permissive MIT, ISC, BSD, Apache-2.0 allow Weak copyleft LGPL, MPL-2.0, EPL-2.0 review per use Strong / network copyleft GPL, AGPL deny for distributed code Missing or custom UNLICENSED, SEE LICENSE IN deny until reviewed
Most policies allow permissive licences, review weak copyleft, and deny strong copyleft and unknown licences — confirm your own with legal.

Handling the hard cases

OR and AND expressions. (MIT OR GPL-3.0) lets you choose MIT, so it should pass an MIT allowlist. (MIT AND CC-BY-4.0) requires both, so both must be allowed. Most checkers evaluate SPDX expressions correctly; verify yours does with a fixture before relying on it.

SEE LICENSE IN <file>. The package points at a custom licence file. A human must read it once; if acceptable, add the exact version to the exceptions file. Pin by version, because the next release may change the text.

No licence at all. Legally, no licence means no permission. Ask the maintainer to add one, replace the package, or get explicit written permission. Do not treat missing as permissive.

Your own private packages. Internal packages often omit the license field. --excludePrivatePackages skips packages marked "private": true; for internal packages published to a private registry, add "license": "UNLICENSED" and exclude your scope explicitly.

Monorepos: one policy, many packages

In a workspace, different packages often have different obligations. A public open-source library published to npm, an internal service deployed to your own infrastructure, and a desktop application distributed to customers each trigger licence terms differently — distribution is the usual trigger for copyleft obligations, and network-copyleft licences such as AGPL add obligations for software offered over a network. A single allowlist for the whole repository is therefore either too strict for some packages or too loose for others.

Run the check per package instead, with a small shared policy file and per-package overrides. With pnpm, pnpm --filter <package> licenses list --prod --json gives the licences for one package's production tree, and a short script can apply a policy chosen by a field in that package's manifest, such as "acme": { "licensePolicy": "distributed" }. The same approach works with license-checker-rseidelsohn --start packages/desktop. Keep the policies themselves in one reviewed location, so changing what "distributed" allows is one pull request rather than many.

Attribution and notices

Checking licences is half the job; honouring them is the other half. Most permissive licences, including MIT, BSD and Apache-2.0, require that the copyright notice and licence text accompany copies of the software. For anything you distribute — a desktop app, a mobile app, an SDK, a Docker image given to customers — generate a third-party notices file at build time:

npx license-checker-rseidelsohn --production --plainVertical --out THIRD_PARTY_NOTICES.txt

Include the file in the distributed artefact, and regenerate it on every release so it always matches the shipped dependency tree. Apache-2.0 has an extra requirement worth knowing: if a dependency ships a NOTICE file, its contents must be reproduced too. Front-end bundles are a special case: bundlers concatenate code and can strip licence comments, so enable your bundler's licence extraction (for example, esbuild's --legal-comments=external or a Rollup licence plugin) to keep notices with the shipped JavaScript.

Worked example: a licence change in a minor release

A dependency bot opens a routine pull request upgrading a charting library from 4.1.9 to 4.2.0. Tests pass. The licence check fails: version 4.2.0 moved from MIT to AGPL-3.0. Without the check, the upgrade would have merged automatically under the team's auto-merge rule for minor versions. The team pins 4.1.9 with a Renovate rule that prevents further upgrades, opens an internal ticket to evaluate alternatives, and adds a comment in the exceptions file noting why the pin exists. Grouped, scheduled updates with the licence check as a required status — as set up in Configuring Renovate for Grouped Updates in a Monorepo — make this kind of catch routine.

A licence change caught at the pull request The dependency bot opens an upgrade PR, CI runs tests and the licence check, the licence check fails on AGPL, and the maintainer pins the previous version. Renovate CI Maintainer PR: chart-lib 4.1.9 -> 4.2.0 test s pass licence check: AGPL-3.0 denied pin 4.1.9, close PR
A required licence check stops an auto-merge that tests alone would have allowed.

CI integration

jobs:
  licenses:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with: { node-version: 22, cache: pnpm }
      - run: pnpm install --frozen-lockfile
      - run: pnpm run licenses:check
      - run: pnpm run licenses:report
      - uses: actions/upload-artifact@v4
        with: { name: licenses, path: licenses.csv }

Run the check on every pull request that changes a lockfile, and publish the CSV report as a build artifact so compliance teams can retrieve it for any release.

Prevention and CI/CD guardrails

  • Make the licence check a required status for merges, including bot pull requests.
  • Pin exceptions by exact version and record a reason and reviewer for each.
  • Scope to production dependencies unless your policy says otherwise.
  • Generate an attribution file for distributed software, since many permissive licences require it.

Frequently Asked Questions

Is this legal advice? No. Tools only report declared licences. Your organisation's legal or open-source programme office should define the allowlist and review exceptions.

Do I need to check devDependencies? Usually not for distribution obligations, since development tools are not shipped. Some organisations still review them for policy or security reasons; run a separate, non-blocking report for them.

What about licences of bundled code inside a package? A package may bundle third-party code under other licences, noted only in its own LICENSE or THIRD_PARTY_NOTICES file. Licence checkers do not see these; software composition analysis tools that scan file contents can.

How does this relate to an SBOM? An SBOM lists every component and its licence in a standard format. The licence check enforces policy; the SBOM documents what shipped. Generating one is covered in Generating an SBOM for a JavaScript Package.

Why does the checker report a licence different from the repository's LICENSE file? The checker reads the license field of the published package.json, which is what the author declared for that version. If it disagrees with the repository's LICENSE file, the published declaration is usually what matters, but the mismatch is worth raising with the maintainer and noting in your exceptions file.

Related

Dependency Auditing and Automated Updates