Skip to content

Add version and upstream license links to THIRD_PARTY_NOTICES.md - #2794

Draft
abrarshivani wants to merge 18 commits into
NVIDIA:mainfrom
abrarshivani:tpn-version-location
Draft

Add version and upstream license links to THIRD_PARTY_NOTICES.md#2794
abrarshivani wants to merge 18 commits into
NVIDIA:mainfrom
abrarshivani:tpn-version-location

Conversation

@abrarshivani

@abrarshivani abrarshivani commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Adds Version and Location columns to THIRD_PARTY_NOTICES.md. Location links to the license
file in the dependency's own upstream repository, pinned to the version we redistribute:

Package Version License Location
github.com/klauspost/compress/zstd/internal/xxhash v1.19.1 MIT LICENSE.txt

Version was dropped twice before (ab62fd0ac) because a bump with no license change dirtied the
file. That is reversed here: a notices file that does not say which version it describes cannot be
matched to a release. The churn is real but small, one index row and two bullets per bump.

Every URL in the file was verified by fetching it and comparing its sha256 against the copy under
vendor/. A URL that does not match is never written, so there are no dead links and none point
at the wrong license. 153 URLs across 114 modules.

That includes the secondary license files a module ships alongside its main one, which are easy to
lose: go.yaml.in/yaml/v2 carries LICENSE.libyaml (MIT, for the embedded libyaml port) on top of
its Apache-2.0 LICENSE, github.com/opencontainers/go-digest carries LICENSE.docs, and the
golang.org/x/* modules and google.golang.org/protobuf each carry PATENTS.

What to review

Hand-written, 917 lines:

File Lines
tools/verify-license-urls.sh 227
tools/license-overrides.tsv 8
tools/resolve-module-repos.sh 172
tools/license-url-lib.sh 156
tools/generate-third-party-notices_test.sh 146
tools/generate-third-party-notices.sh 112
tools/license-url-lib_test.sh 88
.github/workflows/third-party-notices-links.yaml 61
tools/test-helpers.sh 45
Makefile 19
.github/workflows/third-party-notices-check.yaml 5

Generated, do not read: THIRD_PARTY_NOTICES.md (1,570), tools/license-urls.tsv (158),
tools/module-repos.tsv (128).

tools/verify-license-urls.sh is the one to read. Everything else supports it.

How it works

vendor/ gives the module and version for free, and the license file names, but it does not
record the upstream repository. dario.cat/mergo actually lives at github.com/imdario/mergo,
k8s.io/api at github.com/kubernetes/api. Scraping that out of vendored sources is wrong more
often than right, so two committed maps carry it instead, both machine-generated:

tools/module-repos.tsv maps module to repository. Resolution is the Go module proxy's Origin
(101 of 124), then the go-import meta tag that go get itself uses (21), then the
github.com/<org>/<repo> path shape (2). Nothing is hand-written. It is keyed by module and not
by version, so a bump does not invalidate it.

tools/license-urls.tsv maps module, version and license path to a verified URL. A row is written
only when the bytes at that URL hash identically to the vendored copy. Probing for a 200 is not
enough: it cannot tell a correct link from one that returns 200 for the wrong license.

Both are produced out of band by make third-party-notices-repos and make third-party-notices-urls, which need network. make third-party-notices reads them offline, so
make check-third-party-notices stays hermetic and cannot flap on a proxy that withholds Origin.

Scope is what we ship. The verifier reuses the generator's own collection, so it covers the
packages go-licenses attributes to ./cmd/.... vendor/ holds 168 license files; the 15 that
belong to build and test only dependencies (ginkgo, gomega, go-cmp, x/tools and friends) are not
redistributed and are not listed.

License files are enumerated from vendor/ rather than from the go-licenses save output, because
that output keeps only the one file it classifies as the license per package and drops the rest.

tools/license-overrides.tsv corrects the License column where go-licenses under-reports it. Three
modules ship one file holding two licenses: gopkg.in/yaml.v3 and go.yaml.in/yaml/v3 each carry a
full-text MIT section plus a short-form Apache-2.0 grant, and go.yaml.in/yaml/v2 carries
LICENSE (Apache-2.0) alongside LICENSE.libyaml (MIT). go-licenses classifies each as a single
license, so all three read Apache-2.0 / MIT from the override instead. It is curated by hand
rather than detected, because scanning license text cannot tell BSD-2-Clause from BSD-3-Clause and
a wrong addition to this file is worse than an omission. Generation fails if an override names a
package that is no longer in the index, so the file cannot rot unnoticed.

.github/workflows/third-party-notices-links.yaml re-verifies every URL weekly. Links are proven
correct when written, but upstream can retag or archive a repository afterwards and no offline gate
can see that.

Note for dependency bumps

A version change now needs two commands, because a verified URL contains the version:

make third-party-notices-urls   # network
make third-party-notices        # offline

Renovate and Dependabot cannot do the first on their own. Their bump job needs wiring, or a human
runs it. This is the direct cost of requiring every link to be verified rather than derived.

Testing

make test-tools runs the new bash suites, 48 assertions across two files, and is now part of
CHECK_TARGETS so it runs in CI.

Verified by hand:

  • 124 index rows, five columns, no floating HEAD or branch refs
  • all 114 module@version pairs match vendor/modules.txt
  • regeneration is byte-deterministic across runs
  • the gate fails closed: removing the klauspost/compress rows from tools/license-urls.tsv
    makes make third-party-notices exit non-zero naming that module

The notices generator had no tests. Guard main so the script can be
sourced, and add a minimal assertion harness for the URL derivation
added next.

Signed-off-by: Abrar Shivani <ashivani@nvidia.com>
The original tautology assertion would silently pass in CI environments
where go-licenses is installed, allowing a regressed guard to corrupt
the tracked THIRD_PARTY_NOTICES.md file.

Add a deterministic structural check for BASH_SOURCE[0] in the
generator, and redirect OUTPUT to a throwaway path to prevent file
corruption if the guard ever regresses.

Signed-off-by: Abrar Shivani <ashivani@nvidia.com>
Proxy case-encoding is done in awk: the obvious sed form silently
corrupts every module path containing a capital, which the proxy then
rejects. Also covers version and ref normalisation, the gopkg.in
convention, GitHub submodule subdirectories, and the blob and raw URL
templates for GitHub and Gerrit.

Signed-off-by: Abrar Shivani <ashivani@nvidia.com>
annotate_modules keeps the version it already parsed instead of
discarding it, and each package's license files are looked up in the
verified URL map. A missing entry is fatal. Each file's URL is printed
under its own heading, which is the only unambiguous place for it when a
package carries several, and the Dependency header no longer disagrees
with the Module bullet.

Signed-off-by: Abrar Shivani <ashivani@nvidia.com>
location_cell was called nested inside printf's argument list, so its
die/exit only killed the command substitution, not the script; under
set -e a package with no verified URL rendered a blank Location cell
and the generator still exited 0. Hoist the call to its own statement
so the failure propagates, and add a regression test that a missing
map entry aborts emit_index_table.

Signed-off-by: Abrar Shivani <ashivani@nvidia.com>
Uses the module proxy Origin, then the go-import meta tag, then the
github.com path shape. Upstream publishes these mappings, so nothing is
hand-written: dario.cat/mergo resolves to imdario/mergo and go.yaml.in
to yaml/go-yaml.

The EXIT trap that removes the temp file referenced a variable local to
main(); once main returned normally rather than exiting, the process's
implicit exit fired that trap after the variable had gone out of scope,
and set -u turned the cleanup itself into a failure that overwrote a
successful run's exit status. main() now exits explicitly so the trap
runs while the variable is still in scope.

Signed-off-by: Abrar Shivani <ashivani@nvidia.com>
A URL is recorded only when the file it serves hashes identically to the
copy under vendor/. Probing for a 200 cannot tell a correct link from one
that serves the wrong licence; hashing can, and it also removes the need
to guess whether a submodule inherits its licence from the repository
root or ships its own.

Scope is the shipped set: the verifier reuses the generator's collection,
so build- and test-only dependencies are not resolved or documented.

Ref candidates for go.googlesource.com repos are split into tag names and
commit hashes so only tag names get the refs/tags/ qualification; a raw
commit hash under refs/tags/ 404s, which otherwise breaks pseudo-versioned
modules such as google.golang.org/protobuf.

Signed-off-by: Abrar Shivani <ashivani@nvidia.com>
curl piped into sha256sum hashes zero bytes to a fixed, valid-looking
constant on any transport failure -- a 404, a DNS error, a timeout, a
rate-limit. The match at the candidate loop compared only that string
against the vendored file's hash, so a failed fetch against a 0-byte
licence file would have read as a genuine match and written an URL
nothing ever verified. Gate the comparison on remote_sha's exit status
so a failed fetch is correctly treated as no match.

Signed-off-by: Abrar Shivani <ashivani@nvidia.com>
The generator fails when a license file has no verified URL, so the
existing freshness gate also catches a bump that skipped the network
step. A scheduled job re-checks every link against the vendored bytes to
catch upstream retagging after the fact.

Signed-off-by: Abrar Shivani <ashivani@nvidia.com>
- location_cell and verify-license-urls.sh's per-package loop can each
  find zero license files without dying, producing a blank Location cell
  or a silent pass; both now fail closed.
- location_for treats an empty URL field as a match instead of a miss.
- emit_sections swallowed a license_dir_within_module failure and could
  publish a link resolved against the wrong directory; it now dies like
  location_cell.
- fence_for propagated grep's no-match exit status through pipefail,
  which is harmless today only because every caller is inside a command
  substitution.
- resolve-module-repos.sh died on any unresolved module, including the
  build/test-only ones out of scope for the notices document; downgraded
  to a warning since verify-license-urls.sh already enforces fail-closed
  for in-scope modules.
- test-tools wasn't wired into any CI-run target, so the regression tests
  guarding these paths never ran automatically; added it to CHECK_TARGETS.
- Three regression tests sourced the generator by a repo-root-relative
  path and passed vacuously outside the repo root; they now take the
  absolute path.
- Reworded the notices header sentence that contradicted the adjacent
  gpuop-cfg text to state the rule the tooling actually implements, and
  regenerated THIRD_PARTY_NOTICES.md (prose only; all 124 rows and every
  license text are unchanged).

Signed-off-by: Abrar Shivani <ashivani@nvidia.com>
Add shellcheck disable=SC1091 alongside the existing source= directives
so shellcheck's static-only run (no -x) stays quiet about sourced
sibling scripts. Discard the unused license CSV field in
verify-license-urls.sh with _ instead of a named unused variable, and
mark the deliberately single-quoted bash -c bodies in the notices test
with disable=SC2016. No behavior changes.

Signed-off-by: Abrar Shivani <ashivani@nvidia.com>
license_files_for enumerated licenses from the go-licenses save cache,
which keeps only the single file go-licenses classifies as the license
per package. Sibling license files in the same vendor directory - a
second license, a PATENTS grant, a docs license - were silently
dropped, and tools/verify-license-urls.sh enumerated the same cache,
so the URL map agreed with the omission and nothing caught it.

Switch all three enumeration call sites to the governing directory in
vendor/ that license_dir_within_module already computes, rather than
the cache. Enumerating straight from vendor/ can now match a genuine
source file that starts with a license-shaped header (kube-openapi's
pkg/validation/spec/license.go), so license_files_for gets an
extension exclusion for common source file types.

Recovers 15 previously unattributed files: PATENTS grants for every
golang.org/x/* module and google.golang.org/protobuf, LICENSE.libyaml
for the embedded libyaml port in go.yaml.in/yaml/v2, LICENSE.docs in
opencontainers/go-digest, and PATENTS/AUTHORS files nested under
third_party subpackages of several k8s.io modules.

Signed-off-by: Abrar Shivani <ashivani@nvidia.com>
Replace bare-adjective and abbreviated local variable names in
tools/generate-third-party-notices.sh, tools/verify-license-urls.sh, and
tools/resolve-module-repos.sh with names that carry their meaning (e.g.
lf -> license_file, tmp -> verified_urls_tmp_file / repos_tmp_file,
rsha -> remote_sha_value). Pure rename, no behavior change.

Signed-off-by: Abrar Shivani <ashivani@nvidia.com>
The three function headers described what the code below already shows.
Keep only the rationale a reader cannot recover from the code: why the
name filter exists, why the nearest enclosing directory wins, and what
the link separator mirrors.

Signed-off-by: Abrar Shivani <ashivani@nvidia.com>
@abrarshivani
abrarshivani marked this pull request as draft August 25, 2026 00:44
go-licenses classifies a license file as whichever license it scores
highest, so gopkg.in/yaml.v3, go.yaml.in/yaml/v3 and go.yaml.in/yaml/v2
each report only one identifier even though their license file(s) bundle
both Apache-2.0 and MIT grants. The license text was already reproduced
in full; only the reported identifier understated it.

Add tools/license-overrides.tsv, a hand-curated map of package to the
correct joined identifier, and wire it into both the index table and the
license sections so they agree. A stale-entry guard fails the build if an
override names a package that no longer appears in the generated index.

Signed-off-by: Abrar Shivani <ashivani@nvidia.com>
The index table repeats the module path next to the package path even
when they match, which is true for most rows. Package and Location
already identify the dependency, and the Module bullet in each
package's detail section still explains the cases where the package
path and module diverge, so the index no longer needs the column.

Signed-off-by: Abrar Shivani <ashivani@nvidia.com>
Follows the removal of the Module column from the index table: the
module a package belongs to is no longer shown anywhere in the
generated notices, so drop the remaining per-section bullet and
reword the header prose that described it.

Signed-off-by: Abrar Shivani <ashivani@nvidia.com>
The rewording after the Module bullet was removed left a short line, and
the em dashes did not match the surrounding paragraphs, which use
semicolons.

Signed-off-by: Abrar Shivani <ashivani@nvidia.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant