Shared GitHub Actions for PlaidCloud repositories.
Fails a build on a Python dependency whose license the organisation has not
approved. Replaces liccheck, which imports pkg_resources — removed in
setuptools 82 — and was last released in September 2023
(sc-24172).
license:
name: Check Dependency Licenses
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: PlaidCloud/plaidcloud-github-actions/license-check@v1
with:
extras: full,tracing| Input | Default | Notes |
|---|---|---|
package |
. |
What to install and scan. Ignored when requirements is set. |
requirements |
'' |
Install a requirements file instead of package, for a repo that ships an image and has nothing pip-installable of its own. |
extras |
'' |
Comma-separated, e.g. full,tracing. Empty installs base dependencies. |
policy |
'' |
Repo-local policy file, applied in addition to license-policy.txt, not instead of it. |
python-version |
3.12 |
|
uv-version |
0.11.10 |
|
trivy-version |
v0.70.0 |
Pinned deliberately — see below. |
diagnostics |
false |
Print what each Trivy scan surface saw. Run once when onboarding a repo. |
Trivy is the license extractor; the policy is applied by
check_licenses.py in this repo. That split is not stylistic:
- Trivy's license findings are not dependable. On 0.73.0 the findings
block — the surface
--exit-codeandlicense.ignoredact on — came back empty for a 53-package venv, whileResults[].Packages[].Licensesresolved every package on every version and platform tried. A findings-based gate would have kept passing, green, enforcing nothing, after a routine action bump.trivy-versionis pinned for the same reason. trivy rootfs, nevertrivy fs. On the same venv,fsreturns no result blocks at all.- Completeness is checked, not just violations. A scanner that cannot read
the venv reports nothing and exits 0 — a pass for a tree it never looked at.
Every
.dist-infoin the venv must appear in Trivy's package list, so the check self-calibrates as dependencies change rather than resting on a hardcoded floor.
license-check/license-policy.txt is the organisation allow-list. There is no
deny-list: anything not named fails, as does a package declaring no license at
all. That reproduces liccheck's level: cautious.
Entries are matched case-insensitively, and both SPDX identifiers and free-text
names are listed on purpose — Trivy normalises many packages to SPDX but passes
the declared text through verbatim when it cannot, so Apache-2.0 and
Apache Software License both reach the policy. A license's whole declared
string is matched first; only an unmatched one is treated as an SPDX expression
and split on AND/OR/WITH. Compound expressions are evaluated
conservatively — both sides of an OR must be approved, so a package we could
legally take under the permitted half asks for a human rather than passing
quietly.
Changing this file changes the gate for every repository on the same tag. Review it as policy, not as configuration.
A line of the form package: license[, license…] forgives one package a
license the organisation otherwise refuses. It replaces liccheck's
[Authorized Packages], and unlike that section it names what it forgives:
# PyInstaller is GPLv2-or-later with the bootloader exception, which permits
# distributing non-free programs built with it. That exception is prose — PyPI
# declares the bare GPLv2 classifier and no License-Expression, so no scanner
# reading metadata can see it. Permanent, not a migration convenience.
pyinstaller: GPL-2.0-or-later, GPL-2.0-only, GNU General Public License v2 (GPLv2)
Put repo-specific waivers in a repo-local file and pass it as policy:. Naming
the license matters: if the package relicenses, the waiver stops covering it and
the build fails, where a bare package name would keep waving it through.
Waivers that match no scanned package are reported on every run. An exemption list rots silently otherwise — liccheck's accumulated 22 entries across the org, most of them for packages whose licenses Trivy now reads without help.
A waiver forgives a license the scanner read. When the scanner cannot read one
at all, assert what it is with =:
# Declares no license of any kind. Google's, Apache-2.0 upstream.
google-crc32c = Apache-2.0
# Puts its whole license text in the `license` field, so Trivy passes it
# through as `text://MIT License Copyright (c) 2022 OpenAI…`.
tiktoken = MIT
The asserted license replaces what was scanned and is then checked like any other, so nobody can assert their way to something the policy refuses.
It applies only where the scanner produced nothing usable: no license, a
text:// blob, or a literal UNKNOWN. Asserting over a license Trivy read
correctly is an error, not an override — that is how a GPL package would get
laundered into MIT, and it is what a waiver is for. This is the capability
liccheck's [Authorized Packages] provided, except that it names the license
rather than just the package, so it can be reviewed and can go stale loudly.
@v1— moving, and what consumers should normally pin. A policy change reaches every repository on the next run, which is the entire point of putting the policy in one place; pinning exact versions everywhere would mean ten pull requests to disallow one license.@v1.0.0— frozen. Use it to hold a repository back deliberately, and say why in the workflow.
v1 is moved only for a change we intend to propagate, and only from a green
main. A change to an input's name or meaning is a v2, not a v1 move — the
gate failing everywhere at once is acceptable, a gate silently doing nothing
everywhere at once is not.