From 4d0733d4135e7f92983504fbef4026059728ba71 Mon Sep 17 00:00:00 2001 From: Sung-Kyu Yoo Date: Tue, 29 Sep 2026 20:43:47 +0900 Subject: [PATCH 1/2] chore: bundle third-party license notices --- .github/workflows/cd.yml | 15 ++++- .github/workflows/ci.yaml | 4 ++ .github/workflows/release.yaml | 2 + .gitignore | 1 + CONTRIBUTING.md | 5 ++ Makefile | 9 ++- NOTICE | 5 ++ build/package/Dockerfile | 3 + build/package/Dockerfile.goreleaser | 2 + build/package/goreleaser.yaml | 10 +++- scripts/collect-third-party-licenses.sh | 74 +++++++++++++++++++++++++ scripts/verify-notice.sh | 8 ++- scripts/verify-release-artifacts.sh | 61 ++++++++++++++++++++ website/content/docs/release.md | 6 +- 14 files changed, 199 insertions(+), 6 deletions(-) create mode 100755 scripts/collect-third-party-licenses.sh create mode 100755 scripts/verify-release-artifacts.sh diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 825a469..1492078 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -9,6 +9,8 @@ on: - "go.sum" - "LICENSE" - "NOTICE" + - "scripts/collect-third-party-licenses.sh" + - "scripts/verify-release-artifacts.sh" - "build/package/Dockerfile" - "build/package/Dockerfile.goreleaser" - ".dockerignore" @@ -22,6 +24,8 @@ on: - "go.sum" - "LICENSE" - "NOTICE" + - "scripts/collect-third-party-licenses.sh" + - "scripts/verify-release-artifacts.sh" - "build/package/Dockerfile" - "build/package/Dockerfile.goreleaser" - ".dockerignore" @@ -69,7 +73,13 @@ jobs: # changes, so actually run the binary as the unprivileged user. - name: Smoke test the image if: github.event_name == 'pull_request' - run: docker run --rm ghcr.io/skyoo2003/kvs:dev version + run: | + docker run --rm ghcr.io/skyoo2003/kvs:dev version + docker run --rm --entrypoint sh ghcr.io/skyoo2003/kvs:dev -c ' + test -s /usr/share/licenses/kvs/LICENSE && + test -s /usr/share/licenses/kvs/NOTICE && + test -s /usr/share/licenses/kvs/THIRD_PARTY_LICENSES/README + ' # The job above only builds Dockerfile. The release path uses # Dockerfile.goreleaser + dockers_v2, which used to fail for the first time @@ -95,3 +105,6 @@ jobs: with: version: "~> v2" args: release --snapshot --clean --config build/package/goreleaser.yaml + + - name: Verify archive license bundle + run: make verify-release-artifacts diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c2054f4..8f09c5e 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -11,6 +11,8 @@ on: - "LICENSE" - "NOTICE" - "scripts/verify-notice.sh" + - "scripts/collect-third-party-licenses.sh" + - "scripts/verify-release-artifacts.sh" - "build/package/goreleaser.yaml" - "README.md" - ".goreleaser.yml" @@ -28,6 +30,8 @@ on: - "LICENSE" - "NOTICE" - "scripts/verify-notice.sh" + - "scripts/collect-third-party-licenses.sh" + - "scripts/verify-release-artifacts.sh" - "build/package/goreleaser.yaml" - "README.md" - ".goreleaser.yml" diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 89458b9..aeb52ae 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -57,6 +57,8 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} HOMEBREW_TAP_TOKEN: ${{ steps.tap_token.outputs.token }} + - name: Verify archive license bundle + run: make verify-release-artifacts # GoReleaser 2.18 still emits the deprecated `postflight` stanza. Rewrite # only its generated quarantine hook before the release job finishes. - name: Modernize the generated Homebrew cask diff --git a/.gitignore b/.gitignore index c48daad..224c654 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ coverage.html # GoReleaser dist/ +.release/ # Hugo /website/public/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b02f73f..1052857 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -89,6 +89,11 @@ make setup # installs pre-commit hooks ## PR Guidelines +By submitting a contribution, you confirm that you have the right to submit it +and license it under the [Apache License 2.0](LICENSE). Do not include copied +or generated third-party material unless its license and required notices are +identified for inclusion in the release notice bundle. + - Keep PRs small and focused on a single concern - Include tests for new functionality - Add a changelog fragment for user-visible changes: `changie new`, one sentence, two at most. diff --git a/Makefile b/Makefile index e736eac..25bb7ac 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: all setup clean build test lint lint-fix vet coverage race soak notice +.PHONY: all setup clean build test lint lint-fix vet coverage race soak notice licenses verify-release-artifacts # How long `make soak` runs for. The full run behind the numbers in the docs is SOAK=4h. SOAK ?= 5m @@ -12,6 +12,13 @@ all: vet lint test build notice: @./scripts/verify-notice.sh +licenses: + @rm -rf dist/THIRD_PARTY_LICENSES + @./scripts/collect-third-party-licenses.sh dist/THIRD_PARTY_LICENSES + +verify-release-artifacts: + @./scripts/verify-release-artifacts.sh dist + setup: @pre-commit install diff --git a/NOTICE b/NOTICE index 3dac68f..d1211ac 100644 --- a/NOTICE +++ b/NOTICE @@ -4,6 +4,11 @@ Copyright 2020-2026 Sung-Kyu Yoo This product includes third-party software. The inventory below describes the modules linked into the kvs binary. Their respective license terms apply. +Binary archives and container images also include a THIRD_PARTY_LICENSES +directory containing the exact license and upstream NOTICE files for these +module versions. The inventory is verified in CI against the modules linked +into ./cmd/kvs. + Third-party components ---------------------- - github.com/boltdb/bolt@v1.3.1 — MIT diff --git a/build/package/Dockerfile b/build/package/Dockerfile index 25cc89e..c229c5a 100644 --- a/build/package/Dockerfile +++ b/build/package/Dockerfile @@ -8,11 +8,14 @@ RUN apk --no-cache --no-progress add make WORKDIR /github.com/skyoo2003/kvs COPY . . RUN make build +RUN ./scripts/collect-third-party-licenses.sh /third_party_licenses FROM alpine:3.23 WORKDIR /kvs COPY --from=builder /github.com/skyoo2003/kvs/dist/kvs /usr/bin/kvs +COPY LICENSE NOTICE /usr/share/licenses/kvs/ +COPY --from=builder /third_party_licenses /usr/share/licenses/kvs/THIRD_PARTY_LICENSES EXPOSE 3456 3457 6379 diff --git a/build/package/Dockerfile.goreleaser b/build/package/Dockerfile.goreleaser index 679ed77..86e6591 100644 --- a/build/package/Dockerfile.goreleaser +++ b/build/package/Dockerfile.goreleaser @@ -6,6 +6,8 @@ FROM alpine:3.23 WORKDIR /kvs ARG TARGETPLATFORM COPY ${TARGETPLATFORM}/kvs /usr/bin/kvs +COPY LICENSE NOTICE /usr/share/licenses/kvs/ +COPY third_party_licenses /usr/share/licenses/kvs/THIRD_PARTY_LICENSES EXPOSE 3456 3457 6379 diff --git a/build/package/goreleaser.yaml b/build/package/goreleaser.yaml index 1b67ac9..735e123 100644 --- a/build/package/goreleaser.yaml +++ b/build/package/goreleaser.yaml @@ -9,6 +9,8 @@ env: before: hooks: - go mod download + - rm -rf .release/third_party_licenses + - ./scripts/collect-third-party-licenses.sh .release/third_party_licenses builds: - id: kvs @@ -48,6 +50,8 @@ archives: - README.md - CHANGELOG.md - CODE_OF_CONDUCT.md + - src: .release/third_party_licenses/** + dst: THIRD_PARTY_LICENSES changelog: disable: false @@ -88,12 +92,16 @@ dockers_v2: - images: - ghcr.io/skyoo2003/kvs dockerfile: build/package/Dockerfile.goreleaser + extra_files: + - LICENSE + - NOTICE + - .release/third_party_licenses ids: - kvs platforms: - linux/amd64 - linux/arm64 - sbom: false + sbom: true flags: - "--provenance=false" tags: diff --git a/scripts/collect-third-party-licenses.sh b/scripts/collect-third-party-licenses.sh new file mode 100755 index 0000000..e64d5a5 --- /dev/null +++ b/scripts/collect-third-party-licenses.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env sh +# Collect the license and notice files for every module linked into kvs. +# The output is intentionally built from the module cache so its contents match +# the exact module versions selected by go.mod/go.sum. +set -eu + +if [ "$#" -ne 1 ]; then + echo "usage: $0 OUTPUT_DIRECTORY" >&2 + exit 2 +fi + +root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +output=$1 + +case "$output" in + ''|/|.) + echo "refusing unsafe output directory: $output" >&2 + exit 2 + ;; +esac + +if [ -e "$output" ]; then + echo "output directory already exists: $output" >&2 + exit 2 +fi + +modules=$(mktemp) +trap 'rm -f "$modules"' EXIT + +cd "$root" +go list -deps -json ./cmd/kvs | + awk ' + /"Path":/ { path = $2; gsub(/[",]/, "", path) } + /"Version":/ { version = $2; gsub(/[",]/, "", version) } + /"Main": true/ { main = 1 } + /^}/ { + if (path != "" && version != "" && !main) print path "@" version + path = version = ""; main = 0 + } + ' | sort -u > "$modules" + +mkdir -p "$output" +{ + echo "Third-party license bundle for KVS" + echo + echo "This directory contains the license and notice files distributed by the" + echo "exact Go modules linked into this KVS binary. See ../NOTICE for the" + echo "component inventory." + echo + echo "Modules" + echo "-------" + sed 's/^/- /' "$modules" +} > "$output/README" + +while IFS= read -r module; do + path=${module%@*} + version=${module##*@} + module_dir=$(go list -m -f '{{.Dir}}' "$path") + destination="$output/$path@$version" + found=0 + + mkdir -p "$destination" + for file in "$module_dir"/LICENSE* "$module_dir"/COPYING* "$module_dir"/NOTICE*; do + if [ -f "$file" ]; then + cp "$file" "$destination/" + found=1 + fi + done + + if [ "$found" -eq 0 ]; then + echo "no top-level license or notice file found for $module" >&2 + exit 1 + fi +done < "$modules" diff --git a/scripts/verify-notice.sh b/scripts/verify-notice.sh index 386f3cc..05dd951 100755 --- a/scripts/verify-notice.sh +++ b/scripts/verify-notice.sh @@ -5,7 +5,8 @@ set -eu root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) actual=$(mktemp) recorded=$(mktemp) -trap 'rm -f "$actual" "$recorded"' EXIT +licenses=$(mktemp -d) +trap 'rm -f "$actual" "$recorded"; rm -rf "$licenses"' EXIT cd "$root" go list -deps -json ./cmd/kvs | @@ -26,3 +27,8 @@ if ! diff -u "$recorded" "$actual"; then echo "NOTICE's third-party component inventory is stale." >&2 exit 1 fi + +# A component list alone is not enough for licenses such as BSD-3-Clause: +# binary distributions must reproduce their copyright and license terms. The +# collector fails if any linked module lacks the material we package at release. +"$root/scripts/collect-third-party-licenses.sh" "$licenses/THIRD_PARTY_LICENSES" diff --git a/scripts/verify-release-artifacts.sh b/scripts/verify-release-artifacts.sh new file mode 100755 index 0000000..c2c5f15 --- /dev/null +++ b/scripts/verify-release-artifacts.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env sh +# Verify that every released archive carries the project's and dependencies' +# license material. GoReleaser emits tar.gz archives on Unix and zip archives +# on Windows. +set -eu + +if [ "$#" -ne 1 ]; then + echo "usage: $0 DIST_DIRECTORY" >&2 + exit 2 +fi + +dist=$1 +found=0 +modules=$(mktemp) +trap 'rm -f "$modules"' EXIT + +go list -deps -json ./cmd/kvs | + awk ' + /"Path":/ { path = $2; gsub(/[",]/, "", path) } + /"Version":/ { version = $2; gsub(/[",]/, "", version) } + /"Main": true/ { main = 1 } + /^}/ { + if (path != "" && version != "" && !main) print path "@" version + path = version = ""; main = 0 + } + ' | sort -u > "$modules" + +check_entries() { + archive=$1 + entries=$2 + for required in LICENSE NOTICE THIRD_PARTY_LICENSES/README; do + if ! printf '%s\n' "$entries" | grep -Fx "$required" >/dev/null; then + echo "$archive is missing $required" >&2 + exit 1 + fi + done + + while IFS= read -r module; do + if ! printf '%s\n' "$entries" | grep -F "THIRD_PARTY_LICENSES/$module/" >/dev/null; then + echo "$archive is missing license material for $module" >&2 + exit 1 + fi + done < "$modules" +} + +for archive in "$dist"/*.tar.gz; do + [ -e "$archive" ] || continue + check_entries "$archive" "$(tar -tzf "$archive")" + found=1 +done + +for archive in "$dist"/*.zip; do + [ -e "$archive" ] || continue + check_entries "$archive" "$(unzip -Z1 "$archive")" + found=1 +done + +if [ "$found" -eq 0 ]; then + echo "no release archives found in $dist" >&2 + exit 1 +fi diff --git a/website/content/docs/release.md b/website/content/docs/release.md index 146cb7b..d90e414 100644 --- a/website/content/docs/release.md +++ b/website/content/docs/release.md @@ -95,8 +95,10 @@ go in the same change — while it is there, it wins and the migration never fir | Homebrew cask | `skyoo2003/homebrew-tap`, installed with `brew install skyoo2003/tap/kvs` | | Release notes | The GitHub release body, taken from `changes/v1.2.3.md` | -Each archive carries the binary plus `LICENSE`, `NOTICE`, `README.md`, `CHANGELOG.md`, and -`CODE_OF_CONDUCT.md`. The binary reports the tag through `kvs version`. +Each archive carries the binary plus `LICENSE`, `NOTICE`, `THIRD_PARTY_LICENSES`, `README.md`, +`CHANGELOG.md`, and `CODE_OF_CONDUCT.md`. The license bundle contains the exact upstream license +and notice files for the modules linked into that release. The binary reports the tag through +`kvs version`. **Every container tag but the first moves.** `latest-alpine`, `v1-alpine`, and `v1.2-alpine` point at whatever was released most recently, which is why there are no pre-release tags: publishing From 5781f7345aa129dd4956fe6344ab3d0621a501b9 Mon Sep 17 00:00:00 2001 From: Sung-Kyu Yoo Date: Tue, 29 Sep 2026 20:51:28 +0900 Subject: [PATCH 2/2] fix: copy third-party licenses from goreleaser context path GoReleaser keeps the source path of extra_files, so the license bundle lands at .release/third_party_licenses in the Docker build context. --- build/package/Dockerfile.goreleaser | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/package/Dockerfile.goreleaser b/build/package/Dockerfile.goreleaser index 86e6591..6284c07 100644 --- a/build/package/Dockerfile.goreleaser +++ b/build/package/Dockerfile.goreleaser @@ -7,7 +7,7 @@ WORKDIR /kvs ARG TARGETPLATFORM COPY ${TARGETPLATFORM}/kvs /usr/bin/kvs COPY LICENSE NOTICE /usr/share/licenses/kvs/ -COPY third_party_licenses /usr/share/licenses/kvs/THIRD_PARTY_LICENSES +COPY .release/third_party_licenses /usr/share/licenses/kvs/THIRD_PARTY_LICENSES EXPOSE 3456 3457 6379