Skip to content

feat(build): ship third-party license attribution and gate dependency licenses - #141

Merged
sergiught merged 2 commits into
mainfrom
feat/third-party-license-attribution
Sep 12, 2026
Merged

sergiught merged 2 commits into
mainfrom
feat/third-party-license-attribution

Conversation

@sergiught

@sergiught sergiught commented Sep 12, 2026

Copy link
Copy Markdown
Owner

📝 Summary

The ofga binary statically links 127 third-party modules under MIT, BSD and Apache-2.0. All three require reproducing the module's copyright notice and license text when distributing in binary form — and none of our artifacts carried any. Archives shipped only our own LICENSE; the deb/rpm/apk packages shipped no license file at all (nfpm's license: field sets package metadata, it installs nothing). Nine dependencies also ship a NOTICE that Apache-2.0 §4(d) requires travel with derivative distributions.

This adds tools/licensegen, which generates THIRD_PARTY_LICENSES and doubles as an allowlist gate, and ships the bundle in every artifact.

No copyleft exposure was found. Every linked module is permissive, so nothing had to be removed or replaced — the gap was attribution only:

License Modules
MIT 50
Apache-2.0 42
BSD-3-Clause 19
BSD-2-Clause 3
Apache-2.0 + BSD-3-Clause 8
Apache-2.0 + MIT 2
BSD-2-Clause + ISC 1
Apache-2.0 + BSD-3-Clause + MIT 1
Apache-2.0 + CC-BY-SA-4.0 1

Design notes

  • Unioned across the whole release matrix, not the host. go list -deps is platform-dependent: github.com/ebitengine/purego (Apache-2.0) is reachable only on darwin. Generating from whichever machine ran the tool under-attributed the others — the macOS archives would have shipped a list omitting a module those binaries actually link. The generator now queries all four goos/goarch targets from .goreleaser.yaml and unions the result. Over-attributing on a given platform is harmless; omitting a linked module is the violation. This also makes output host-independent, so make licenses gives the same bundle on macOS and Linux — otherwise a Mac contributor would regenerate a 123-module file that CI then rejected. A test pins releaseTargets to .goreleaser.yaml so the two cannot drift.
  • Scoped to ./cmd/ofga, not ./... — build-time-only tooling (go-md2man, blackfriday, licensecheck itself) ships to nobody and stays out of what we attribute.
  • google/licensecheck for detection — it reports every match in a file. 13 dependencies are multi-licensed (gopkg.in/yaml.v3 is MIT + Apache-2.0 split by file; klauspost/compress is Apache-2.0 + BSD-3 + MIT), and reporting one license each would understate them.
  • The generator is the gate. It refuses to emit a bundle containing a license outside the allowlist, or text it cannot identify. CI runs it and diffs, catching a disallowed license and a stale bundle in one step.
  • Carve-outs are per module with a stated reason, not a wider allowlist — so the next dependency under that license still fails. Currently one: opencontainers/go-digest, whose LICENSE.docs is CC-BY-SA-4.0 but covers that repo's prose only.
  • Added as a CI step, not a job, so branch protection's pinned job names are unaffected.

🏷 Type of change

  • 🐛 Bug fix (non-breaking)
  • ✨ Feature (non-breaking)
  • 💥 Breaking change (users must update)
  • ♻️ Refactor (no functional change)
  • 📚 Docs only
  • 🛠 Build / CI / tooling

🔗 Related issues

None.

✅ How to verify

make check          # now includes license-check
make licenses       # regenerate the bundle

Artifact coverage was verified against a real make release-snapshot, opening each package rather than trusting the config:

Artifact Path
.tar.gz (Linux and Darwin) THIRD_PARTY_LICENSES
deb / rpm / apk /usr/share/doc/ofga/{copyright,THIRD_PARTY_LICENSES}
container images /usr/share/licenses/ofga/THIRD_PARTY_LICENSES
AUR /usr/share/licenses/ofga-bin/THIRD_PARTY_LICENSES
Homebrew cask inherits from the archive

Also checked:

  • The bundle's 127 entries match the union of go list -deps across all four release targets exactly — nothing missing.
  • The AUR package() script was simulated against the real release tarball; both license files install.
  • The staleness gate was verified by breaking it: appending a line makes make license-check fail, and make licenses restores it.
  • Generation is reproducible — the bundle built on a cold-cache CI runner is byte-identical to the committed one.

Not covered: the release workflow itself has not run with these changes. Snapshot mode exercises archives, packages and images but skips signing, publishing, AUR upload and the Homebrew tap; none of those steps were modified. Test-only dependencies are deliberately unscanned — they create no distribution obligation, but a copyleft test dependency would not trip the gate.

📋 Checklist

  • 🧪 Tests added or updated where it makes sense
  • 📖 Docs updated (README / command --help examples) if behavior changed
  • 🏷 PR title follows Conventional Commits
  • 🟢 make check passes locally

🤖 Generated with Claude Code

… licenses

The ofga binary statically links 126 third-party modules under MIT, BSD and
Apache-2.0. All three require reproducing the module's copyright notice and
license text when distributing in binary form, and none of our artifacts
carried any: archives shipped only our own LICENSE, and the deb/rpm/apk
packages shipped no license file at all (nfpm's `license:` field sets package
metadata, it installs nothing). Nine dependencies also ship a NOTICE that
Apache-2.0 section 4(d) requires travel with derivative distributions.

tools/licensegen enumerates the modules linked into ./cmd/ofga, reproduces
every license and NOTICE file, and writes THIRD_PARTY_LICENSES. It is scoped to
the binary rather than ./... so build-time-only tooling stays out of what we
have to attribute. Detection uses google/licensecheck, which reports every
match in a file — 13 of our dependencies are multi-licensed, and reporting one
license each would understate them.

The generator doubles as the gate: it refuses to emit a bundle containing a
license outside the allowlist, or text it cannot identify. Carve-outs are
recorded per module with a reason rather than by widening the allowlist, so the
next dependency arriving under the same license still fails. CI runs the
generator and diffs, which catches both a disallowed license and a stale
bundle. Added as a step beside the docs freshness check, not a new job, so
branch protection's pinned job names are unaffected.

The bundle now ships in the archives, deb/rpm/apk (alongside our own LICENSE at
the conventional /usr/share/doc/ofga/copyright), both container images, and the
AUR package. The Homebrew cask inherits it from the archive.
Build constraints make the linked module set platform-specific, so generating
the bundle from whichever machine happened to run the tool under-attributed the
other platforms. github.com/ebitengine/purego (Apache-2.0) is reachable only on
darwin: the macOS archives were shipping an attribution list that omitted a
module those binaries actually link, which is the violation this tool exists to
prevent, just narrowed to one platform.

The generator now unions `go list -deps` across the full goos/goarch matrix
from .goreleaser.yaml. Over-attributing on a given platform is harmless — the
bundle is a superset — while omitting a linked module is not.

This also makes the output host-independent, so `make licenses` produces the
same bundle on macOS and Linux. Without it a macOS contributor regenerating the
bundle would have produced a 123-module file that CI then rejected.

A test pins releaseTargets to the matrix in .goreleaser.yaml so the two cannot
drift apart silently.
@sergiught
sergiught merged commit 0075851 into main Sep 12, 2026
8 checks passed
@sergiught
sergiught deleted the feat/third-party-license-attribution branch September 12, 2026 12:49
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