From 34c06acc4cf984cb8d39c8dfa3e98341928d2a25 Mon Sep 17 00:00:00 2001 From: Hugo Linder Date: Tue, 2 Jun 2026 11:24:11 +0200 Subject: [PATCH 1/2] Inline OSS license workflows (RND-1996) A public repo cannot call a private repo's reusable workflow, so the prod-env reusable workflows are inlined here. Same Trivy gate (warn-only, HIGH,CRITICAL for a distribution context) and weekly inventory PR. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/license-inventory.yml | 52 ++++++++++++++++-- .github/workflows/license.yml | 72 +++++++++++++++++++++---- 2 files changed, 110 insertions(+), 14 deletions(-) diff --git a/.github/workflows/license-inventory.yml b/.github/workflows/license-inventory.yml index aa6a5535a5..5516717f8e 100644 --- a/.github/workflows/license-inventory.yml +++ b/.github/workflows/license-inventory.yml @@ -1,11 +1,55 @@ +# OSS license inventory (inlined). +# +# Inlined rather than calling the shared reusable workflow in HedvigInsurance/prod-env, +# because that repo is PRIVATE and this one is PUBLIC (a public repo cannot call a +# private reusable workflow). Keep in sync with prod-env/.github/workflows/license-inventory.yml. +# +# Regenerates the CycloneDX license inventory and opens a PR with it when (and only when) +# the dependency/license set changed. The committed file is annual-review evidence: +# "what changed since last year" is `git diff` on .license/inventory.cdx.json. name: license-inventory + on: schedule: - cron: "0 5 * * 1" # Mondays ~06:00 Europe/Stockholm workflow_dispatch: + +permissions: + contents: write + pull-requests: write + jobs: inventory: - permissions: - contents: write - pull-requests: write - uses: HedvigInsurance/prod-env/.github/workflows/license-inventory.yml@master + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + + - name: Generate license inventory (CycloneDX) + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 + with: + scan-type: fs + scan-ref: "." + scanners: license + format: cyclonedx + output: inventory.cdx.json + + # CycloneDX randomizes serialNumber + timestamp every run; strip them so a PR + # opens only when the actual component/license set changed. + - name: Normalize inventory + run: | + mkdir -p .license + jq 'del(.serialNumber, .metadata.timestamp)' inventory.cdx.json > .license/inventory.cdx.json + rm -f inventory.cdx.json + + - name: Open PR if inventory changed + uses: peter-evans/create-pull-request@22a9089034f40e5a961c8808d113e2c98fb63676 # v7.0.11 + with: + add-paths: .license/inventory.cdx.json + branch: chore/license-inventory + title: "chore: update license inventory" + commit-message: "chore: update license inventory" + labels: dependencies + body: | + Automated snapshot of the OSS license inventory, kept as annual-review + evidence. Opens only when the dependency/license set changed since the + last snapshot. diff --git a/.github/workflows/license.yml b/.github/workflows/license.yml index 8069ba6381..3e23ab30f5 100644 --- a/.github/workflows/license.yml +++ b/.github/workflows/license.yml @@ -1,16 +1,68 @@ +# OSS license gate (inlined). +# +# This is deliberately inlined rather than calling the shared reusable workflow in +# HedvigInsurance/prod-env: that repo is PRIVATE and this repo is PUBLIC, and a public +# repo cannot call a private repo's reusable workflow. Keep the policy block below in +# sync with prod-env/.github/workflows/license-gate.yml. +# +# Android ships to end-user devices (a distribution context), so we gate on HIGH,CRITICAL +# — blocking weak copyleft (LGPL/MPL/EPL) as well as forbidden licenses. name: license + on: pull_request: push: branches: [develop, "renovate/**"] + +permissions: + contents: read + jobs: - license: - uses: HedvigInsurance/prod-env/.github/workflows/license-gate.yml@master - with: - # Android ships to end-user devices (a distribution context), so block weak - # copyleft (LGPL/MPL/EPL) as well as forbidden licenses — not just the - # forbidden set that backends gate on. - gate-severity: "HIGH,CRITICAL" - # Warn-only pilot: reports findings but never fails the build. Review one run, - # populate `ignored-licenses` for any pre-existing hits, then remove this line. - enforce: false + license-gate: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + + # Category -> Trivy severity: forbidden=CRITICAL restricted=HIGH + # reciprocal=MEDIUM notice/permissive/unencumbered=LOW + - name: Write license policy + run: | + cat > "${RUNNER_TEMP}/trivy-license.yaml" <<'YAML' + license: + forbidden: + - AGPL-3.0 + - GPL-2.0 + - GPL-3.0 + - SSPL-1.0 + - BUSL-1.1 + - Commons-Clause + restricted: + - LGPL-2.1 + - LGPL-3.0 + - MPL-2.0 + - EPL-1.0 + - EPL-2.0 + notice: + - Apache-2.0 + - MIT + - BSD-2-Clause + - BSD-3-Clause + - ISC + unencumbered: + - CC0-1.0 + - Unlicense + permissive: [] + YAML + + - name: License gate + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 + with: + scan-type: fs + scan-ref: "." + scanners: license + trivy-config: ${{ runner.temp }}/trivy-license.yaml + severity: "HIGH,CRITICAL" # distribution context: forbidden + weak copyleft + # Warn-only pilot: reports findings but never fails the build. + # Change to "1" to enforce once the report is reviewed. + exit-code: "0" + format: table From fb9283e216769d9e432e8d1ead2f7dac87b24c39 Mon Sep 17 00:00:00 2001 From: Hugo Linder Date: Tue, 2 Jun 2026 12:44:50 +0200 Subject: [PATCH 2/2] Rework Android license checks to build-native tooling (RND-1996) Trivy fs scanning found ~0 Gradle dependencies (no lockfile to read), so the inventory was useless. Switch to the jaredsburrows licenseReleaseReport already wired into this build, which resolves the full dependency graph: - Gate: a script reads the report (folded into the existing build job, no extra Gradle run); warn-only for now. - Inventory: weekly job commits the sorted report JSON as review evidence. - Remove the Trivy-based license.yml. Co-Authored-By: Claude Opus 4.8 --- .github/scripts/license-gate.sh | 56 ++++++++++++++++++++ .github/workflows/license-inventory.yml | 51 +++++++++---------- .github/workflows/license.yml | 68 ------------------------- .github/workflows/pr.yml | 6 +++ 4 files changed, 86 insertions(+), 95 deletions(-) create mode 100755 .github/scripts/license-gate.sh delete mode 100644 .github/workflows/license.yml diff --git a/.github/scripts/license-gate.sh b/.github/scripts/license-gate.sh new file mode 100755 index 0000000000..46d27dcd8d --- /dev/null +++ b/.github/scripts/license-gate.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# OSS license gate for the Android app. +# +# Reads the jaredsburrows `licenseReleaseReport` JSON — which is build-resolved, so it +# reflects the app's real, complete dependency closure (unlike a manifest scanner). +# Android ships to end-user devices (a distribution context), so weak copyleft +# (LGPL/MPL/EPL) is blocked alongside strong/network copyleft and source-available. +# +# Usage: license-gate.sh [enforce] +# enforce=true -> exit 1 on violations (blocking) +# enforce=false -> warn only, always exit 0 (pilot; default) +set -euo pipefail + +REPORT="${1:?usage: license-gate.sh [enforce]}" +ENFORCE="${2:-false}" + +if [[ ! -f "$REPORT" ]]; then + echo "::warning::license report not found at $REPORT (did licenseReleaseReport run?) — skipping gate" + exit 0 +fi + +# jaredsburrows reports free-text license NAMES, not SPDX ids, so match by pattern. +# Blocked for a distributed app = any copyleft + source-available license. +BLOCK_RE='Affero|AGPL|Lesser General Public|LGPL|GNU General Public|GPLv|GPL-2|GPL-3|Mozilla Public|MPL-|Eclipse Public|EPL-|Common Development and Distribution|CDDL|Server Side Public|SSPL|Business Source|BUSL|Commons Clause' + +total=$(jq 'length' "$REPORT") +blocked=$(jq -r --arg re "$BLOCK_RE" ' + .[] | .dependency as $d | (.licenses // []) + | .[]? | select(.license | test($re; "i")) | "\($d) :: \(.license)"' "$REPORT" | sort -u) +unknown=$(jq -r '.[] | select((.licenses // []) | length == 0) | .dependency' "$REPORT" | sort -u) + +n_blocked=$([[ -n "$blocked" ]] && grep -c . <<<"$blocked" || echo 0) +n_unknown=$([[ -n "$unknown" ]] && grep -c . <<<"$unknown" || echo 0) + +echo "### License gate" +echo "Dependencies scanned: $total" +echo "Blocked (copyleft / source-available): $n_blocked" +[[ -n "$blocked" ]] && sed 's/^/ - /' <<<"$blocked" +echo "No detected license: $n_unknown" +[[ -n "$unknown" ]] && sed 's/^/ - /' <<<"$unknown" | head -20 + +if [[ -n "${GITHUB_STEP_SUMMARY:-}" ]]; then + { + echo "### License gate" + echo "- Dependencies: $total" + echo "- Blocked (copyleft / source-available): $n_blocked" + echo "- No detected license: $n_unknown" + [[ -n "$blocked" ]] && { echo; echo '```'; echo "$blocked"; echo '```'; } + } >> "$GITHUB_STEP_SUMMARY" +fi + +if [[ -n "$blocked" && "$ENFORCE" == "true" ]]; then + echo "::error::Disallowed licenses present. Remove/replace, or record an Engineering-Lead-approved exception." + exit 1 +fi +echo "OK ($([[ "$ENFORCE" == "true" ]] && echo enforcing || echo 'warn-only'))" diff --git a/.github/workflows/license-inventory.yml b/.github/workflows/license-inventory.yml index 5516717f8e..b51b19c454 100644 --- a/.github/workflows/license-inventory.yml +++ b/.github/workflows/license-inventory.yml @@ -1,12 +1,8 @@ -# OSS license inventory (inlined). +# Weekly OSS license inventory for the Android app. # -# Inlined rather than calling the shared reusable workflow in HedvigInsurance/prod-env, -# because that repo is PRIVATE and this one is PUBLIC (a public repo cannot call a -# private reusable workflow). Keep in sync with prod-env/.github/workflows/license-inventory.yml. -# -# Regenerates the CycloneDX license inventory and opens a PR with it when (and only when) -# the dependency/license set changed. The committed file is annual-review evidence: -# "what changed since last year" is `git diff` on .license/inventory.cdx.json. +# Uses the build-resolved jaredsburrows licenseReleaseReport (accurate, full dependency +# closure) — not a manifest scanner. Commits the sorted JSON as annual-review evidence; +# "what changed since last year" is `git diff` on .license/inventory.json. name: license-inventory on: @@ -20,36 +16,37 @@ permissions: jobs: inventory: - runs-on: ubuntu-24.04 + runs-on: ubuntu-latest-8-vcpu steps: - - uses: actions/checkout@v4 - - - name: Generate license inventory (CycloneDX) - uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 + - uses: actions/checkout@v5 + - name: Setup CI + uses: ./.github/actions/common-setup with: - scan-type: fs - scan-ref: "." - scanners: license - format: cyclonedx - output: inventory.cdx.json + gradle-cache-read-only: "true" + datadog-api-key: ${{ secrets.DATADOG_API_KEY }} + lokalise-id: ${{ secrets.LOKALISE_ID }} + lokalise-token: ${{ secrets.LOKALISE_TOKEN }} + + - name: Generate license report + run: ./gradlew licenseReleaseReport --no-configuration-cache --continue - # CycloneDX randomizes serialNumber + timestamp every run; strip them so a PR - # opens only when the actual component/license set changed. - - name: Normalize inventory + - name: Stage inventory (sorted for stable diffs) run: | mkdir -p .license - jq 'del(.serialNumber, .metadata.timestamp)' inventory.cdx.json > .license/inventory.cdx.json - rm -f inventory.cdx.json + rm -f .license/inventory.cdx.json # remove the old Trivy-based inventory + jq 'sort_by(.dependency)' \ + app/app/build/reports/licenses/licenseReleaseReport.json \ + > .license/inventory.json - name: Open PR if inventory changed uses: peter-evans/create-pull-request@22a9089034f40e5a961c8808d113e2c98fb63676 # v7.0.11 with: - add-paths: .license/inventory.cdx.json + add-paths: .license branch: chore/license-inventory title: "chore: update license inventory" commit-message: "chore: update license inventory" labels: dependencies body: | - Automated snapshot of the OSS license inventory, kept as annual-review - evidence. Opens only when the dependency/license set changed since the - last snapshot. + Build-resolved OSS license inventory (jaredsburrows `licenseReleaseReport`), + kept as annual-review evidence. Opens only when the dependency/license set + changed since the last snapshot. diff --git a/.github/workflows/license.yml b/.github/workflows/license.yml deleted file mode 100644 index 3e23ab30f5..0000000000 --- a/.github/workflows/license.yml +++ /dev/null @@ -1,68 +0,0 @@ -# OSS license gate (inlined). -# -# This is deliberately inlined rather than calling the shared reusable workflow in -# HedvigInsurance/prod-env: that repo is PRIVATE and this repo is PUBLIC, and a public -# repo cannot call a private repo's reusable workflow. Keep the policy block below in -# sync with prod-env/.github/workflows/license-gate.yml. -# -# Android ships to end-user devices (a distribution context), so we gate on HIGH,CRITICAL -# — blocking weak copyleft (LGPL/MPL/EPL) as well as forbidden licenses. -name: license - -on: - pull_request: - push: - branches: [develop, "renovate/**"] - -permissions: - contents: read - -jobs: - license-gate: - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@v4 - - # Category -> Trivy severity: forbidden=CRITICAL restricted=HIGH - # reciprocal=MEDIUM notice/permissive/unencumbered=LOW - - name: Write license policy - run: | - cat > "${RUNNER_TEMP}/trivy-license.yaml" <<'YAML' - license: - forbidden: - - AGPL-3.0 - - GPL-2.0 - - GPL-3.0 - - SSPL-1.0 - - BUSL-1.1 - - Commons-Clause - restricted: - - LGPL-2.1 - - LGPL-3.0 - - MPL-2.0 - - EPL-1.0 - - EPL-2.0 - notice: - - Apache-2.0 - - MIT - - BSD-2-Clause - - BSD-3-Clause - - ISC - unencumbered: - - CC0-1.0 - - Unlicense - permissive: [] - YAML - - - name: License gate - uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 - with: - scan-type: fs - scan-ref: "." - scanners: license - trivy-config: ${{ runner.temp }}/trivy-license.yaml - severity: "HIGH,CRITICAL" # distribution context: forbidden + weak copyleft - # Warn-only pilot: reports findings but never fails the build. - # Change to "1" to enforce once the report is reviewed. - exit-code: "0" - format: table diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 9ec77360e1..6abac98452 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -74,6 +74,12 @@ jobs: - name: Run license release report run: ./gradlew licenseReleaseReport --no-configuration-cache --continue continue-on-error: true + - name: OSS license gate (warn-only) + # Reuses the report generated just above (no extra Gradle run). Warn-only for the + # pilot: continue-on-error + enforce=false. To enforce: pass `true` and drop + # continue-on-error so a forbidden license blocks the build. + run: bash .github/scripts/license-gate.sh app/app/build/reports/licenses/licenseReleaseReport.json false + continue-on-error: true - name: Build run: "./gradlew :app:bundleDebug" - name: Setup build tool version variable