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 aa6a5535a5..b51b19c454 100644 --- a/.github/workflows/license-inventory.yml +++ b/.github/workflows/license-inventory.yml @@ -1,11 +1,52 @@ +# Weekly OSS license inventory for the Android app. +# +# 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: 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-latest-8-vcpu + steps: + - uses: actions/checkout@v5 + - name: Setup CI + uses: ./.github/actions/common-setup + with: + 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 + + - name: Stage inventory (sorted for stable diffs) + run: | + mkdir -p .license + 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 + branch: chore/license-inventory + title: "chore: update license inventory" + commit-message: "chore: update license inventory" + labels: dependencies + body: | + 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 8069ba6381..0000000000 --- a/.github/workflows/license.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: license -on: - pull_request: - push: - branches: [develop, "renovate/**"] -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 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