Run License Policy Checks in CI
Add a deterministic open source license policy check to any CI system that runs Node.js: producing component records, the CLI contract, exit codes, failing the build closed, keeping the JSON result as release evidence, and avoiding false failures.
Task: Fail a build when a dependency's licence is denied by your policy, and keep the JSON evidence for the release. · Uses @wardenkit/open-source-licence-policy · Product overview · Documentation
The contract a CI job depends on
A licence check is only useful in a pipeline if it is deterministic, offline and machine-readable. This one is all three: evaluation runs in your process, makes no network call and transmits nothing, JSON goes to stdout, human-readable diagnostics go to stderr, and the exit code is derived from your own policy.
| Exit code | Meaning | Suggested CI reaction |
|---|---|---|
| 0 | Evaluated, and the aggregate outcome is not CI-blocking under your policy. | Pass. |
| 1 | Evaluated, and the aggregate outcome is CI-blocking under your policy. | Fail the build. This is the intended enforcement path. |
| 2 | Usage error, malformed component document, or an invalid policy. | Fail the build as a pipeline defect, not as a licence finding. |
| 3 | Production authorization required, not yet valid, or expired. | Fail, and route to whoever owns the entitlement. |
| 4 | Local trust state or reference pack rejected. | Fail, and re-provision the pack. |
| 5 | Network request to the WardenKit service failed. | Only reachable from activate and refresh, never from evaluate. |
| 6 | The WardenKit perimeter rejected the request. | Only reachable from activate and refresh. |
Codes 5 and 6 cannot occur in an evaluation step, which is the property that makes this safe to put on the critical path of a release: an outage somewhere else cannot turn your build red.
Step 1 — produce the component records
The SDK performs no dependency discovery and no licence detection. You supply the component records, which is what keeps the evaluation deterministic and reviewable. Each record needs an id and exactly one SPDX licence expression; version and source are optional, and unknown fields are rejected.
[
{ "id": "left-pad", "version": "1.3.0", "licenseExpression": "MIT", "source": "npm ls --json" },
{ "id": "acme-http", "version": "4.2.1", "licenseExpression": "Apache-2.0 OR MIT" },
{ "id": "copyleft-tool", "version": "2.0.0", "licenseExpression": "AGPL-3.0-only" }
]Where the records come from is your decision. Two patterns work well, and they differ in what a diff tells you.
- Commit components.json and review it like any other source file. A dependency change becomes visible in the diff, and the check is fully reproducible from the repository alone.
- Generate it in the job from your own inventory — a lockfile reader, an SBOM you already produce, or a registry query — and upload it with the result. Nothing is committed, but the input is still retained as evidence.
Whichever you choose, keep the generation step separate from the evaluation step. A generator failure must be diagnosable as a generator failure, not reported as a licence finding.
Step 2 — the CI step itself
Validate the policy first, then evaluate. --output writes the result document to a file, so you can both fail the build and keep the evidence. Because a blocking outcome exits non-zero, capture the status explicitly rather than letting set -e abort before you can classify it.
#!/usr/bin/env sh
set -eu
# Fails as a pipeline defect if the policy document itself is invalid.
npx --no-install wardenkit-licence-policy validate-policy \
--policy licence-policy.json \
--json > licence-policy-validation.json
status=0
npx --no-install wardenkit-licence-policy evaluate \
--components components.json \
--policy licence-policy.json \
--output licence-policy-result.json \
--json || status=$?
case "$status" in
0) echo "licence policy: no blocking outcome" ;;
1) echo "licence policy: blocking outcome — see licence-policy-result.json" ;;
2) echo "licence policy: invalid input or policy document" ;;
3) echo "licence policy: production authorization required" ;;
4) echo "licence policy: local trust state or reference pack rejected" ;;
*) echo "licence policy: unexpected failure (exit $status)" ;;
esac
exit "$status"--no-install is deliberate: the package must already be installed from your lockfile, so a CI run cannot silently fetch a different version of the evaluator than the one you reviewed.
Step 3 — keep the result as release evidence
Upload licence-policy-result.json as a build artefact, and upload it even when the job fails — the failing run is exactly the one whose evidence you will want. The result carries the aggregate outcome, the per-component decisions and a provenance block recording the product version, your policyId, policyVersion and policyHash, and the SPDX reference version in use.
{
"outcome": "invalid-unsupported",
"componentCount": 6,
"ciBlocking": true,
"counts": {
"policy-allowed": 3,
"policy-denied": 1,
"review-required": 1,
"invalid-unsupported": 1
}
}Identical inputs, identical policy, identical product version and identical reference version produce byte-identical output, so an archived result can be re-derived rather than merely trusted. That is what makes it evidence instead of a log line.
Avoiding false failures
- Do not run the check on a partial dependency install. A truncated inventory produces a clean result for the wrong reason.
- Pin the package in your lockfile and install with npm ci, so the evaluator version recorded in provenance is the reviewed one.
- Keep policy validation as its own step, so exit code 2 is never mistaken for a licence finding.
- Decide deliberately whether review-required is CI-blocking. Blocking it stops the release on every new transitive dependency; not blocking it means the queue needs an owner.
- Expect exit 1 on your first run against real data, and read counts before changing the policy. Fail-closed on unknown identifiers is a design property, not a defect.
- Run the check on pull requests as well as on the default branch, so a denied licence is caught before it merges.
The evaluation step needs no service, no daemon, no cache and no credential. Of the CLI commands only activate and refresh contact the WardenKit service, and neither belongs in a build job. In the SDK, the managed Production evaluator (createManagedProductionEvaluator) may perform a bounded authorization refresh on evaluate activity; it transmits authorization data only, never component records, policies, expressions or results.
Next: the same check written as a complete GitHub Actions workflow.
Run this locally
- Try the same workflow locally — Open-Source Licence Policy SDK quickstart
- Learn what the SDK does, its boundaries and its plans — Open-Source Licence Policy SDK
Related guides
- Use the WardenKit Licence Policy CLI in GitHub Actions — Add a licence policy check to a GitHub repository that blocks a pull request on a denied licence.
- Open Source License Policy as Code — Express an existing open source licence policy as a versioned JSON document, validate it, and make its decisions reproducible.
- Validate SPDX License Expressions in Node.js — Call a local SPDX expression validator from your own Node.js code and act on its diagnostics.
Deterministic tooling, professional judgment
WardenKit performs deterministic technical validation, normalization and evaluation of the policy you author, over the records you supply. It does not provide legal advice, determine legal permissions or obligations, opine on licence compatibility, or certify compliance. Which licences your organization allows, denies or reviews remains your decision.
Product overview and pricing · All guides · Docs · Support
Corrections and questions: support@wardenkit.com.