diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 00000000..560e2e59 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,13 @@ +#!/bin/sh +# +# Pre-commit gate. Installed by `make hooks`, which sets core.hooksPath to this +# directory — so this file is versioned, reviewable, and reaches everyone on +# their next pull rather than only the people who remember to re-copy it. +# +# A thin wrapper on purpose: the checks and the staged-file logic live in the +# Makefile's lint-staged target, so `make lint-staged` reproduces exactly what +# blocked a commit, and there is one place to change what runs. +# +# To bypass in an emergency: git commit --no-verify + +exec make --no-print-directory lint-staged diff --git a/.github/actions/trivy-cache/action.yml b/.github/actions/trivy-cache/action.yml new file mode 100644 index 00000000..d92cf37c --- /dev/null +++ b/.github/actions/trivy-cache/action.yml @@ -0,0 +1,26 @@ +name: Cache Trivy +description: > + Restores the pinned trivy binary and its vulnerability DB from the Actions + cache. Both scan jobs need this, so it lives here rather than being pasted + into each one — the trivy-action this replaced cached both for us, and + `make trivy-deps`/`trivy-image` alone would re-download the binary (~30MB) + and the DB (~100MB+) on every run. + +runs: + using: composite + steps: + # Keyed on the Makefile's own hash so a TRIVY_VERSION bump there busts the + # cache automatically, plus a daily date so the DB itself never goes stale + # for longer than a day. + - id: date + shell: bash + run: echo "today=$(date -u +%F)" >> "$GITHUB_OUTPUT" + + - uses: actions/cache@v4 + with: + path: | + bin/trivy + ~/.cache/trivy + key: trivy-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('Makefile') }}-${{ steps.date.outputs.today }} + restore-keys: | + trivy-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('Makefile') }}- diff --git a/.github/workflows/beckn_ci.yml b/.github/workflows/beckn_ci.yml deleted file mode 100644 index df6c1051..00000000 --- a/.github/workflows/beckn_ci.yml +++ /dev/null @@ -1,88 +0,0 @@ -name: CI/CD Pipeline - -on: - pull_request: - branches: - - beckn-onix-v1.0-develop - -env: - APP_DIRECTORY: "shared/plugin" # Root directory to start searching from - -jobs: - lint_and_test: - runs-on: ubuntu-latest - if: github.event_name == 'pull_request' - timeout-minutes: 10 # Increased timeout due to additional steps - steps: - # 1. Checkout the code from the test branch (triggered by PR) - - name: Checkout code - uses: actions/checkout@v4 - - # 2. Set up Go environment - - name: Set up Go 1.24.0 - uses: actions/setup-go@v4 - with: - go-version: '1.24.0' - - # 3. Install golangci-lint - - name: Install golangci-lint - run: | - go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest - - # 4. Run golangci-lint on the entire repo, starting from the root directory - - name: Run golangci-lint - run: | - golangci-lint run ./... # This will lint all Go files in the repo and subdirectories - - # 5. Run unit tests with coverage in the entire repository - - name: Run unit tests with coverage - run: | - # Create a directory to store coverage files - mkdir -p $GITHUB_WORKSPACE/coverage_files - - # Find all *_test.go files and run `go test` for each - find ./ -type f -name '*_test.go' | while read test_file; do - # Get the directory of the test file - test_dir=$(dirname "$test_file") - # Get the name of the Go file associated with the test - go_file="${test_file/_test.go/.go}" - - # Run tests and store coverage for each Go file in a separate file - echo "Running tests in $test_dir for $go_file" - go test -v -coverprofile=$GITHUB_WORKSPACE/coverage_files/coverage_$(basename "$go_file" .go).out $test_dir - done - - # 6. List the generated coverage files for debugging purposes - #- name: List coverage files - #run: | - #echo "Listing all generated coverage files:" - #ls -l $GITHUB_WORKSPACE/coverage_files/ - - # 7. Check coverage for each generated coverage file - - name: Check coverage for each test file - run: | - # Loop through each coverage file in the coverage_files directory - for coverage_file in $GITHUB_WORKSPACE/coverage_files/coverage_*.out; do - echo "Checking coverage for $coverage_file" - - # Get the coverage percentage for each file - coverage=$(go tool cover -func=$coverage_file | grep total | awk '{print $3}' | sed 's/%//') - echo "Coverage for $coverage_file: $coverage%" - - # If coverage is below threshold (90%), fail the job - if (( $(echo "$coverage < 80" | bc -l) )); then - echo "Coverage for $coverage_file is below 90%. Failing the job." - exit 1 - fi - done - - # 7. Build the Go code - #- name: Build Go code - # run: | - # go build -o myapp ${{ env.APP_DIRECTORY }}/... - # if [ ! -f myapp ]; then - # echo "Build failed: myapp executable was not created." - # exit 1 - # else - # echo "Build succeeded: myapp executable created." - # fi diff --git a/.github/workflows/beckn_ci_test.yml b/.github/workflows/beckn_ci_test.yml deleted file mode 100644 index e8d9ae6b..00000000 --- a/.github/workflows/beckn_ci_test.yml +++ /dev/null @@ -1,85 +0,0 @@ -name: CI/CD Test Pipeline - -on: - pull_request: - branches: - - beckn-onix-v1.0-develop - -env: - APP_DIRECTORY: "shared/plugin" - -jobs: - lint_and_test: - runs-on: ubuntu-latest - if: github.event_name == 'pull_request' - timeout-minutes: 10 - outputs: - coverage_ok: ${{ steps.coverage_check.outputs.coverage_ok }} - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Go 1.24.0 - uses: actions/setup-go@v4 - with: - go-version: '1.24.0' - - - name: Install golangci-lint - run: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest - - - name: Run golangci-lint - run: golangci-lint run ./... - - - name: Run unit tests with coverage - run: | - mkdir -p $GITHUB_WORKSPACE/coverage_files - test_files=$(find ./ -type f -name '*_test.go') - if [ -z "$test_files" ]; then - echo "No test cases found. Skipping." - exit 0 - fi - for test_file in $test_files; do - test_dir=$(dirname "$test_file") - go_file="${test_file/_test.go/.go}" - echo "Running tests in $test_dir for $go_file" - go test -v -coverprofile=$GITHUB_WORKSPACE/coverage_files/coverage_$(basename "$go_file" .go).out $test_dir || echo "Tests failed, but continuing." - done - - - name: Check coverage for each test file - id: coverage_check - run: | - echo "coverage_ok=true" >> $GITHUB_OUTPUT - coverage_files=$(find $GITHUB_WORKSPACE/coverage_files -name "coverage_*.out") - if [ -z "$coverage_files" ]; then - echo "No coverage files found. Skipping coverage check." - exit 0 - fi - for coverage_file in $coverage_files; do - echo "Checking coverage for $coverage_file" - coverage=$(go tool cover -func=$coverage_file | grep total | awk '{print $3}' | sed 's/%//') - echo "Coverage: $coverage%" - if (( $(echo "$coverage < 90" | bc -l) )); then - echo "coverage_ok=false" >> $GITHUB_OUTPUT - break - fi - done - - require_exception_approval: - needs: lint_and_test - if: needs.lint_and_test.outputs.coverage_ok == 'false' - runs-on: ubuntu-latest - environment: - name: coverage-exception - url: https://your-coverage-dashboard.com # Optional - steps: - - name: Manual approval required - run: echo "Coverage < 90%. Approval required to continue." - - proceed_with_merge: - needs: [lint_and_test, require_exception_approval] - if: | - needs.lint_and_test.outputs.coverage_ok == 'true' || success() - runs-on: ubuntu-latest - steps: - - name: Proceed with merge - run: echo "Coverage requirement met or exception approved. Merge allowed." diff --git a/.github/workflows/build-and-deploy-plugins.yml b/.github/workflows/build-and-deploy-plugins.yml deleted file mode 100644 index 79c33e07..00000000 --- a/.github/workflows/build-and-deploy-plugins.yml +++ /dev/null @@ -1,116 +0,0 @@ -name: Build and Upload Plugins - -on: - workflow_dispatch: - inputs: - target_branch: - description: 'Branch to deploy' - required: true - default: 'beckn-onix-v1.0-develop' - - -jobs: - build-and-upload: - runs-on: ubuntu-latest - env: - GCS_BUCKET: ${{ secrets.GCS_BUCKET }} - PLUGIN_OUTPUT_DIR: ./generated - ZIP_FILE: plugins_bundle.zip - - steps: - - name: Checkout this repo - uses: actions/checkout@v4 - with: - ref: ${{ github.event.inputs.target_branch }} - - - name: Show selected branch - run: echo "Deploying branch:${{ github.event.inputs.target_branch }}" - - - - name: Clone GitHub and Gerrit plugin repos - run: | - # Example GitHub clone - git clone -b beckn-onix-v1.0-develop https://${{ secrets.PAT_GITHUB }}:@github.com/beckn/beckn-onix.git github-repo - - # Example Gerrit clone - git clone https://${{ secrets.GERRIT_USERNAME }}:${{ secrets.GERRIT_PAT }}@open-networks.googlesource.com/onix-dev gerrit-repo - - - name: List directory structure - run: | - echo "📂 Contents of root:" - ls -alh - - echo "📂 Contents of GitHub repo:" - ls -alh github-repo - - echo "📂 Deep list of GitHub repo:" - find github-repo - - echo "📂 Contents of Gerrit repo:" - ls -alh gerrit-repo - - echo "📂 Deep list of Gerrit repo:" - find gerrit-repo - - - - name: Build Go plugins in Docker - run: | - set -e - mkdir -p $PLUGIN_OUTPUT_DIR - - BUILD_CMDS="" - - # GitHub plugins - for dir in github-repo/pkg/plugin/implementation/*; do - if [ -d "$dir/cmd" ]; then - plugin=$(basename "$dir") - BUILD_CMDS+="cd github-repo && go build -buildmode=plugin -buildvcs=false -o ../${PLUGIN_OUTPUT_DIR}/${plugin}.so ./pkg/plugin/implementation/${plugin}/cmd && cd - && " - fi - done - - # Gerrit plugins — build in their own repo/module context - for dir in gerrit-repo/plugins/*; do - if [ -d "$dir/cmd" ]; then - plugin=$(basename "$dir") - BUILD_CMDS+="cd gerrit-repo && go build -buildmode=plugin -buildvcs=false -o ../${PLUGIN_OUTPUT_DIR}/${plugin}.so ./plugins/${plugin}/cmd && cd - && " - fi - done - - BUILD_CMDS=${BUILD_CMDS%" && "} - echo "🛠️ Running build commands inside Docker:" - echo "$BUILD_CMDS" - - docker run --rm -v "$(pwd)":/app -w /app golang:1.24-bullseye sh -c "$BUILD_CMDS" - - - name: List built plugin files - run: | - echo "Looking in $PLUGIN_OUTPUT_DIR" - ls -lh $PLUGIN_OUTPUT_DIR || echo "⚠️ Directory does not exist" - find $PLUGIN_OUTPUT_DIR -name '*.so' || echo "⚠️ No .so files found" - - echo "Creating zip archive..." - cd "$PLUGIN_OUTPUT_DIR" - zip -r "../$ZIP_FILE" *.so - echo "Created $ZIP_FILE" - cd .. - - - name: List zip output - run: | - ls -lh plugins_bundle.zip - - - - name: Authenticate to GCP - run: | - echo '${{ secrets.GOOGLE_APPLICATION_CREDENTIALS_JSON }}' > gcloud-key.json - gcloud auth activate-service-account --key-file=gcloud-key.json - gcloud config set project trusty-relic-370809 - env: - GOOGLE_APPLICATION_CREDENTIALS: gcloud-key.json - - - name: Upload to GCS - run: | - gsutil -m cp -r $ZIP_FILE gs://${GCS_BUCKET}/plugins/ - - - name: Cleanup - run: | - rm -rf $PLUGIN_OUTPUT_DIR $ZIP_FILE gcloud-key.json diff --git a/.github/workflows/ci-release.yml b/.github/workflows/ci-release.yml new file mode 100644 index 00000000..5c283ea1 --- /dev/null +++ b/.github/workflows/ci-release.yml @@ -0,0 +1,121 @@ +# Named "CI" on purpose, same as ci.yml: the check names GitHub reports are +# " / ", so this keeps them reading +# "CI / Build Artifact (amd64)" and "CI / Publish Artifact". +# +# A separate file rather than two more jobs in ci.yml behind an `if:`. A job +# whose `if:` evaluates false still posts a *skipped* check to the PR, with +# `${{ matrix.* }}` left unexpanded in its name because the matrix leg never +# materialised. A workflow whose trigger does not match simply does not exist +# on that event, so a PR shows nothing from here at all. +# +# The one cost: the Actions sidebar lists two entries called "CI". Rename this +# workflow if that matters more than the check names do. +name: CI + +on: + push: + # Both spellings on purpose. Every tag in this repo so far is v-prefixed + # (v1.8.2, v2.0.1-rc1), but a release cut as 1.0.0 or 1.0.0-RC1 should + # publish rather than silently do nothing. These are GitHub filter + # patterns, not regexes: + and [] work, so the digits are real digits and + # a branch-shaped tag like `release/foo` cannot match. + tags: + - "[0-9]+.[0-9]+.[0-9]+" + - "[0-9]+.[0-9]+.[0-9]+-*" + - "v[0-9]+.[0-9]+.[0-9]+" + - "v[0-9]+.[0-9]+.[0-9]+-*" + +# Least privilege at the workflow level; both jobs add packages: write for the +# push to ghcr.io. +permissions: + contents: read + +# Per tag, and deliberately never cancelled: a publish that is half-done is +# worse than one that finishes and is then superseded. +concurrency: + group: ci-release-${{ github.ref }} + cancel-in-progress: false + +jobs: + build-artifact: + # One leg per architecture, on a runner of that architecture. Not one check + # with QEMU: the image compiles every plugin with `go build + # -buildmode=plugin`, and emulating an arm64 Go toolchain build turns a + # few minutes into tens of them. Both runner labels are free for public + # repos, so the only cost of the second leg is that it is a second check. + name: Build Artifact (${{ matrix.arch }}) + runs-on: ${{ matrix.runner }} + strategy: + # A tag that can only ship one architecture should not ship at all, and + # stopping the other leg early keeps a doomed release from occupying a + # runner for another ten minutes. + fail-fast: true + matrix: + include: + - arch: amd64 + runner: ubuntu-latest + - arch: arm64 + runner: ubuntu-24.04-arm + permissions: + contents: read + # The push to ghcr.io. Read is not enough even for a push by digest. + packages: write + steps: + # fetch-depth: 0 — version-vars.sh derives ONIX_VERSION from + # `git describe --tags`, which needs the tag objects, not just the commit. + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + # The default builder uses the `docker` driver, which supports neither + # push-by-digest nor --metadata-file. This switches to docker-container. + - uses: docker/setup-buildx-action@v3 + + - uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # Pushes untagged, by digest, and writes digest-.txt. Nothing is + # visible under a version tag until Publish Artifact below has both. + - run: make image-build ARCH=${{ matrix.arch }} + + - uses: actions/upload-artifact@v4 + with: + name: digest-${{ matrix.arch }} + path: digest-${{ matrix.arch }}.txt + if-no-files-found: error + + publish-artifact: + name: Publish Artifact + # No always() and no if-failure handling: needs alone means a failed or + # cancelled build leg skips this job, which is the point — a version tag + # must never resolve to one architecture. + needs: build-artifact + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + # Checkout is for the Makefile and for the tag objects version-vars.sh + # reads; this job builds nothing. + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - uses: actions/download-artifact@v4 + with: + pattern: digest-* + merge-multiple: true + + # Binds the tag — and :latest, for a plain vX.Y.Z — to the digests the + # build legs pushed, then inspects the result so the log shows which + # platforms the published tag actually resolves to. + - run: make image-publish diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cef2efdb..5aee8d29 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,36 +1,197 @@ -name: Go CI +name: CI + +# Two checks and no more: "End to End verification" and "Security Scan". +# +# The two tag-time checks live in ci-release.yml, not here behind an `if:`. A +# job whose `if:` is false still posts a skipped check, and its `name:` is +# reported with `${{ matrix.* }}` unexpanded because the matrix leg never +# materialised — so gating tag jobs inside this file put two permanently +# skipped checks on every PR. A separate workflow with a tag-only trigger +# simply does not exist on a PR. That file is also named `CI`, so the check +# names still read "CI / Build Artifact (amd64)" and "CI / Publish Artifact". +# +# Every step below is either a one-line `make` call or GitHub plumbing +# (`${{ }}` expressions, $GITHUB_STEP_SUMMARY). The build, test, scan and +# publish logic lives in the Makefile, so a red check reproduces locally by +# running the exact command the log shows. on: - pull_request: - branches: - - beck-onix-v1.0-develop - - beck-onix-v1.0 push: - branches: - - beck-onix-v1.0-develop - - beck-onix-v1.0 + # development as well as main: this is the branch PRs target and merge + # into, so a merge landing there runs the same gates the PR did. No tags — + # a tag re-running the tests its PR already gated on proves nothing. + branches: [main, development] + pull_request: + +# Least privilege at the workflow level; the jobs that need more declare it for +# themselves. +permissions: + contents: read + +# A new commit on a PR makes the previous commit's runs worthless, so they are +# cancelled rather than left to finish. github.ref is refs/pull//merge for a +# pull_request event, so every push to the PR's head branch lands in the same +# group and supersedes the run before it. +concurrency: + group: ci-${{ github.ref }} + # PRs only. On main and development every commit keeps its own recorded + # verdict — cancelling there would leave a trunk commit with a grey check and + # no way to tell afterwards whether it was ever green. + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + jobs: - test: + verify: + name: End to End verification + runs-on: ubuntu-latest + # pull-requests: write is for the coverage comment below. + permissions: + contents: read + pull-requests: write + steps: + # fetch-depth: 0 — cover-diff needs the base branch's history locally to + # compute the changed-file set; a PR checkout otherwise holds only the + # head commit. + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - run: make build + - run: make test-ci + + # Diff-scoped, never whole-repo: the repo's existing coverage is well + # under the threshold, so a whole-repo gate would fail every PR + # regardless of what it changed. What a review can act on is the number + # for the lines the PR itself touched. On pull_request the base is the + # PR's target branch; on a push it's github.event.before, the commit the + # branch pointed at before this one landed. + # + # BASE_REF goes through env, not string-interpolated into the run script: + # `?=` in the Makefile means the environment wins, and nothing untrusted + # ends up inside a shell command. + # + # continue-on-error so a coverage miss still posts its report; the last + # step in the job turns that outcome back into a failure. + - name: Check coverage of changed files + id: coverage + continue-on-error: true + env: + BASE_REF: ${{ github.event_name == 'pull_request' && format('origin/{0}', github.event.pull_request.base.ref) || github.event.before }} + run: make cover-diff + + # cover-diff writes coverage-report.md on every exit path, including its + # own error paths, so this needs no existence guard. + - name: Post the result to the job summary + run: cat coverage-report.md >> "$GITHUB_STEP_SUMMARY" + + # Always posted — pass, fail or not-applicable. A comment that only shows + # up on failure is ambiguous from the PR itself: did coverage run at all, + # pass, or never trigger? Updated in place across runs via find-comment, + # which matches the marker cover-diff writes into the report. + # + # continue-on-error on both: a fork PR's default GITHUB_TOKEN is + # read-only, so create-or-update-comment 403s there even with + # pull-requests: write declared. Coverage is already gated below; a + # comment that can't post must never fail the job over that. + - name: Find the existing result comment + if: github.event_name == 'pull_request' + continue-on-error: true + uses: peter-evans/find-comment@v4 + id: find-coverage-comment + with: + issue-number: ${{ github.event.pull_request.number }} + comment-author: "github-actions[bot]" + body-includes: "" + + - name: Post the result to the PR + if: github.event_name == 'pull_request' + continue-on-error: true + uses: peter-evans/create-or-update-comment@v5 + with: + comment-id: ${{ steps.find-coverage-comment.outputs.comment-id }} + issue-number: ${{ github.event.pull_request.number }} + body-path: coverage-report.md + edit-mode: replace + + # Turns the continue-on-error above back into a job failure. cover-diff + # already logged the ::error:: annotation and the per-file breakdown, so + # there is nothing to say here beyond the exit code. + - name: Gate on minimum coverage + if: steps.coverage.outcome == 'failure' + run: exit 1 + + security-scan: + name: Security Scan runs-on: ubuntu-latest + # Both scans in one job, one check. They were two jobs plus a gate job, and + # three red-or-green checks to say one thing — "is this branch shippable" — + # was more to read than it was worth. The dependency scan needs only a + # checkout, so it runs first and its findings are in the log before the + # image finishes building. + # + # pull-requests: write is the PR comment. No security-events: write — the + # SARIF upload is gone, see below. + permissions: + contents: read + pull-requests: write steps: - - uses: actions/checkout@v2 - - name: Set up Go - uses: actions/setup-go@v2 + - uses: actions/checkout@v4 + - uses: ./.github/actions/trivy-cache + + # Catches what the image scan structurally cannot: a vulnerable module + # only the test suite imports, so it is never linked into the binary and + # never appears in a layer. + - run: make trivy-deps + + # And the reverse — Trivy reads the image's base layers plus the Go build + # info embedded in the binary, including `stdlib`, so a Go toolchain CVE + # shows up here and nowhere the dependency scan can see. Hence both. + - run: make docker IMAGE=network-adapter:${{ github.sha }} + - run: make trivy-image IMAGE=network-adapter:${{ github.sha }} + + # No upload-sarif here, on purpose. github/codeql-action/upload-sarif + # posts its own check, "Code scanning results / Trivy", which is not a + # job and cannot be renamed or suppressed — so it was a third check on + # every PR. And it reported the opposite of the truth: on the run that + # found 20 HIGH stdlib CVEs it went green with "No new alerts in code + # change", because none of them were new to the diff, while + # make trivy-gate failed on the same SARIF. Two checks disagreeing about + # one scan is worse than one check, and the gate is the one that is + # right. Every finding is in the PR comment below with its severity and + # fixed version. + # + # The cost: no Security-tab history or diff annotations for Trivy. The + # comment and the gate are the surface. + + # One comment covering both scans, rendered from the SARIF already + # written — no third and fourth scan. + - name: Assemble the PR comment + if: github.event_name == 'pull_request' + continue-on-error: true + run: make trivy-report + + - name: Find the existing scan comment + if: github.event_name == 'pull_request' + continue-on-error: true + uses: peter-evans/find-comment@v4 + id: find-comment with: - go-version: '1.20' - - name: Install dependencies - run: go mod tidy - - name: Run tests - run: go test -coverprofile=coverage.out ./... - - name: Check coverage - run: | - coverage=$(go tool cover -func=coverage.out | grep total | awk '{print $3}' | sed 's/%//') - if (( $(echo "$coverage < 90" | bc -l) )); then - echo "Coverage is below 90%" - exit 1 - fi - - name: Run golangci-lint - run: golangci-lint run - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v5 + issue-number: ${{ github.event.pull_request.number }} + comment-author: "github-actions[bot]" + body-includes: "" + + - name: Post the scan report to the PR + if: github.event_name == 'pull_request' + continue-on-error: true + uses: peter-evans/create-or-update-comment@v5 with: - files: ./coverage.out + comment-id: ${{ steps.find-comment.outputs.comment-id }} + issue-number: ${{ github.event.pull_request.number }} + body-path: trivy-report.md + edit-mode: replace + + # The gate, last: it reads both reports and fails on any in-band + # finding or on a report that never got written, so the report is on the + # PR before the check goes red. + - run: make trivy-gate diff --git a/.github/workflows/deploy-to-gke-BS.yml b/.github/workflows/deploy-to-gke-BS.yml deleted file mode 100644 index 39a6156e..00000000 --- a/.github/workflows/deploy-to-gke-BS.yml +++ /dev/null @@ -1,69 +0,0 @@ -name: CI/CD to GKE updated - -on: - #push: - workflow_dispatch: - -jobs: - deploy: - name: Build and Deploy to GKE - runs-on: ubuntu-latest - - steps: - - name: Checkout Code - uses: actions/checkout@v3 - with: - # Full history + tags, so `git describe --tags` in version-vars.sh - # can actually resolve a release tag instead of always falling back - # to the bare commit SHA (the default shallow checkout fetches no - # tag refs at all). - fetch-depth: 0 - - - name: Authenticate to Google Cloud - uses: google-github-actions/auth@v2 - with: - credentials_json: '${{ secrets.GOOGLE_APPLICATION_CREDENTIALS_JSON }}' - - - name: Set up gcloud CLI - uses: google-github-actions/setup-gcloud@v1 - with: - project_id: ${{ secrets.GCP_PROJECT }} - export_default_credentials: true - - - name: Install GKE Auth Plugin - run: gcloud components install gke-gcloud-auth-plugin --quiet - - - name: Configure Docker to use Artifact Registry - run: gcloud auth configure-docker ${{ secrets.GCP_REGION }}-docker.pkg.dev - - - name: Build Docker Image - run: | - IMAGE_NAME=${{ secrets.GCP_REGION }}-docker.pkg.dev/${{ secrets.GCP_PROJECT }}/${{ secrets.GCP_REPO }}/beckn-onix:${{ github.sha }} - source install/scripts/version-vars.sh - docker build -f Dockerfile.adapter \ - --build-arg ONIX_VERSION="$ONIX_VERSION" \ - --build-arg GIT_COMMIT="$GIT_COMMIT" \ - --build-arg GIT_TREE_STATE="$GIT_TREE_STATE" \ - --build-arg BUILD_DATE="$BUILD_DATE" \ - -t $IMAGE_NAME . - docker push $IMAGE_NAME - - - name: Get GKE Credentials - run: | - gcloud container clusters get-credentials ${{ secrets.GKE_CLUSTER }} \ - --zone ${{ secrets.GCP_REGION }} \ - --project ${{ secrets.GCP_PROJECT }} - - - name: Deploy to GKE using Kubernetes Manifests - run: | - IMAGE_NAME=${{ secrets.GCP_REGION }}-docker.pkg.dev/${{ secrets.GCP_PROJECT }}/${{ secrets.GCP_REPO }}/beckn-onix:${{ github.sha }} - - # Replace image in deployment YAML - sed -i "s|image: .*|image: $IMAGE_NAME|g" Deployment/deployment.yaml - - # Apply Kubernetes manifests - kubectl apply -f Deployment/deployment.yaml --namespace=onix-adapter - kubectl apply -f Deployment/service.yaml --namespace=onix-adapter - - # Wait for rollout to complete - kubectl rollout status Deployment/onix-demo-adapter --namespace=onix-adapter diff --git a/.github/workflows/deploy-to-gke.yml b/.github/workflows/deploy-to-gke.yml deleted file mode 100644 index bcb43efd..00000000 --- a/.github/workflows/deploy-to-gke.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: Deploy to GKE - -on: - workflow_dispatch: - inputs: - service_name: - description: 'Name of the Kubernetes service to deploy' - required: true - type: string - cluster_name: - description: 'Name of the GKE cluster' - required: true - type: string - -jobs: - deploy: - runs-on: ubuntu-latest - - env: - PROJECT_ID: ${{ secrets.GCP_PROJECT_ID }} - REGION: ${{ secrets.GCP_REGION }} - GKE_CLUSTER: ${{ github.event.inputs.cluster_name }} - SERVICE_NAME: ${{ github.event.inputs.service_name }} - - steps: - - name: Checkout source - uses: actions/checkout@v3 - - - name: Authenticate to Google Cloud - uses: google-github-actions/auth@v2 - with: - credentials_json: ${{ secrets.GCP_SA_KEY }} - - - name: Set up GKE credentials - uses: google-github-actions/get-gke-credentials@v1 - with: - cluster_name: ${{ env.GKE_CLUSTER }} - location: ${{ env.REGION }} - project_id: ${{ env.PROJECT_ID }} - - - name: Deploy to GKE - run: | - echo "Deploying service $SERVICE_NAME to cluster $GKE_CLUSTER" - kubectl set image deployment/$SERVICE_NAME $SERVICE_NAME=gcr.io/$PROJECT_ID/$SERVICE_NAME:latest --record diff --git a/.github/workflows/onix-gcp-terraform-deploy.yml b/.github/workflows/onix-gcp-terraform-deploy.yml deleted file mode 100644 index 478979be..00000000 --- a/.github/workflows/onix-gcp-terraform-deploy.yml +++ /dev/null @@ -1,66 +0,0 @@ -name: Terraform Deploy to GCP - -on: - push: - workflow_dispatch: # Manual triggerr - -jobs: - plan: - name: Terraform Plan Only - runs-on: ubuntu-latest - - steps: - - name: Checkout this repository - uses: actions/checkout@v3 - - - name: Clone Terraform repo from Gerrit - run: | - git clone https://${{ secrets.GERRIT_USERNAME }}:${{ secrets.GERRIT_PAT }}@open-networks.googlesource.com/onix-dev gerrit-repo - echo "==== Contents of Terraform-dir ====" - pwd - cd gerrit-repo/Terraform-CICD - pwd - ls -la - - - name: Authenticate to Google Cloud - run: echo '${{ secrets.GOOGLE_APPLICATION_CREDENTIALS_JSON }}' > gcp-key.json - - - name: Set up Terraform - uses: hashicorp/setup-terraform@v3 - with: - terraform_version: 1.5.0 - - - name: Write GCP credentials to file - run: echo '${{ secrets.GOOGLE_APPLICATION_CREDENTIALS_JSON }}' > gcp-key.json - - - name: Export GCP credentials environment variable - run: echo "GOOGLE_APPLICATION_CREDENTIALS=$GITHUB_WORKSPACE/gcp-key.json" >> $GITHUB_ENV - - - name: Create backend.tf and Terraform Init - working-directory: ./gerrit-repo/Terraform-CICD - env: - GCS_BUCKET: beckn-cicd-tf-state-bucket - run: | - cat < backend.tf - terraform { - backend "gcs" { - bucket = "${GCS_BUCKET}" - prefix = "terraform/state" - credentials = "${{ github.workspace }}/gcp-key.json" - } - } - EOF - - terraform init - - - name: Terraform Plan - working-directory: ./gerrit-repo/Terraform-CICD - run: terraform plan - - - name: Terraform Apply - working-directory: ./gerrit-repo/Terraform-CICD - run: terraform apply -var="subnet_name=onix-gke-subnet" -auto-approve - - - name: Clean up credentials - run: rm -f gcp-key.json - diff --git a/.gitignore b/.gitignore index 618e9b42..6292bb58 100644 --- a/.gitignore +++ b/.gitignore @@ -143,6 +143,21 @@ create_benchmark_issues.sh # Ignore coverage output files coverage.out coverage.html +coverage-plugin.out +coverage-report.md + +# Makefile tool/build artifacts (golangci-lint, gotestsum, trivy binaries) +/bin/ + +# Scan output: the two SARIF reports trivy-deps/trivy-image write and the +# single comment trivy-report renders from them. +trivy-deps.sarif +trivy-image.sarif +trivy-report.md + +# Release output: image-build writes one of each per architecture. +image-metadata-*.json +digest-*.txt # Ignore the schema directory used for testing /schemas/ diff --git a/Dockerfile.adapter b/Dockerfile.adapter index 41dd7ca8..e7222efe 100644 --- a/Dockerfile.adapter +++ b/Dockerfile.adapter @@ -1,4 +1,4 @@ -FROM golang:1.26.1-bookworm AS builder +FROM golang:1.26.8-bookworm AS builder WORKDIR /workspace/app COPY cmd/adapter ./cmd/adapter diff --git a/Dockerfile.adapter-with-plugins b/Dockerfile.adapter-with-plugins index a7ba5e98..e333faea 100644 --- a/Dockerfile.adapter-with-plugins +++ b/Dockerfile.adapter-with-plugins @@ -1,4 +1,4 @@ -FROM golang:1.26.1-bookworm AS builder +FROM golang:1.26.8-bookworm AS builder WORKDIR /workspace/app diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..98ceb306 --- /dev/null +++ b/Makefile @@ -0,0 +1,454 @@ +# OAN Network Adapter — build, test, lint and security-scan targets. +# +# Single source of truth for the ci.yml and ci-release.yml workflows: every CI +# step is a one-line `make ` call, so a red check reproduces locally by +# running the command the log shows. Anything left inline in a workflow is +# GitHub context (`${{ }}` expressions, $GITHUB_STEP_SUMMARY writes) that has +# no meaning outside a runner. +# +# No DB, no sqlc/migrate, no separate tools/ module — golangci-lint, gotestsum +# and trivy install straight into bin/ via `go install` / curl. + +GO ?= go +BIN_DIR := bin +IMAGE ?= network-adapter:dev + +# Where a tag push publishes to. Derived from GITHUB_REPOSITORY rather than +# written out, so a fork publishes to its own namespace and there is no repo +# name to update if this one is ever renamed; `tr` because ghcr.io rejects an +# uppercase path and GITHUB_REPOSITORY is mixed-case (OpenAgriNet/...). +REGISTRY ?= ghcr.io +IMAGE_REPO ?= $(REGISTRY)/$(shell printf '%s' '$(GITHUB_REPOSITORY)' | tr '[:upper:]' '[:lower:]') + +# The arch of the machine running make, so neither a local run nor the build +# matrix has to pass it: each arch is built on a runner of that arch, never +# under QEMU, because Dockerfile.adapter-with-plugins compiles every plugin +# with `go build -buildmode=plugin` and a plugin .so must match the adapter +# binary's GOARCH exactly. +ARCH ?= $(shell uname -m | sed -e 's/^x86_64$$/amd64/' -e 's/^aarch64$$/arm64/') + +# CI thresholds/pins live here, not duplicated into workflow env blocks — one +# source of truth for both a local `make` run and the GitHub Actions runner. +MIN_COVERAGE ?= 80 +# development, not main: every branch in this repo is cut from development and +# PRs target it, so that is the base a local `make cover-diff` must compare to. +BASE_REF ?= origin/development +SEVERITY ?= CRITICAL,HIGH,MEDIUM,LOW +GOLANGCI_LINT_VERSION := v2.5.0 +GOTESTSUM_VERSION := v1.13.0 +TRIVY_VERSION := v0.74.0 +ACTIONLINT_VERSION := v1.7.12 + +GOLANGCI_LINT := $(BIN_DIR)/golangci-lint +GOTESTSUM := $(BIN_DIR)/gotestsum +TRIVY := $(BIN_DIR)/trivy +ACTIONLINT := $(BIN_DIR)/actionlint + +# From GOROOT, not PATH: `go` is always resolvable here (every other target +# needs it), and gofmt sits next to it, so this works even where only the +# toolchain's bin dir is on PATH. Expanded at recipe time, hence the `$$`. +GOFMT = $$($(GO) env GOROOT)/bin/gofmt + +# pkg/plugin and benchmarks/e2e each build a real .so with a plain `go build +# -buildmode=plugin` subprocess, then load it with plugin.Open in the same test +# run. Two things make that .so unloadable: +# +# -race — the subprocess build carries no -race flag of its own, so a +# race-instrumented test binary and a non-race .so mismatch. +# ./... — instrumenting the whole module's coverage in one build gives +# shared packages (e.g. pkg/plugin/definition) a build identity +# the subprocess build doesn't share, so plugin.Open rejects the +# .so as "built with a different version of" that package. +# +# So they run as their own invocation, without -race and with their own +# coverage profile. Named once here and shared by test, cover and test-ci — +# the three must never disagree about which packages are carved out. +# +# Deliberately no -coverpkg anywhere, and it can't be added: benchmarks/e2e is +# the only test-only package in the module (`go list` confirms it holds no +# non-test files), so it is the only place -coverpkg would credit coverage of +# the packages it drives — and it is in this carve-out precisely because that +# whole-module instrumentation is what makes plugin.Open reject the .so. The +# code benchmarks/e2e exercises is therefore credited only by its own +# packages' tests, which is why cover-diff gates on the diff rather than on a +# module-wide total. +PLUGIN_PKGS := ./pkg/plugin ./benchmarks/e2e/... +MAIN_PKGS = $$($(GO) list ./... | grep -vE '/pkg/plugin$$|/benchmarks/e2e$$') + +# Only used inside a workflow; a local run gets a placeholder rather than a +# broken link. +RUN_URL ?= $(if $(GITHUB_RUN_ID),$(GITHUB_SERVER_URL)/$(GITHUB_REPOSITORY)/actions/runs/$(GITHUB_RUN_ID),local run) + +.DEFAULT_GOAL := help + +## help: list the available targets +help: + @grep -hE '^## [a-z]' $(MAKEFILE_LIST) | sed 's/^## / /' | sort + +## build: compile the adapter binary +# Scoped to cmd/adapter, not ./... — pkg/plugin/implementation/*/cmd holds +# `package main` sources meant only for `go build -buildmode=plugin` +# (install/build-plugins.sh), with no func main() for an ordinary build. +build: + $(GO) build -trimpath -o $(BIN_DIR)/ ./cmd/adapter/... + +## test: run the unit and integration suites (plugin packages without -race) +test: + $(GO) test -race $(MAIN_PKGS) + $(GO) test $(PLUGIN_PKGS) + +## cover: run the suites and write a merged coverage profile to coverage.out +cover: + $(GO) test -race -covermode=atomic -coverprofile=coverage.out $(MAIN_PKGS) + $(GO) test -covermode=atomic -coverprofile=coverage-plugin.out $(PLUGIN_PKGS) + @$(MAKE) --no-print-directory merge-coverage + +## test-ci: cover, through gotestsum — one line per package. What ci.yml calls. +test-ci: $(GOTESTSUM) + $(GOTESTSUM) --format pkgname --format-hide-empty-pkg -- \ + -race -coverprofile=coverage.out -covermode=atomic $(MAIN_PKGS) + $(GOTESTSUM) --format pkgname --format-hide-empty-pkg -- \ + -coverprofile=coverage-plugin.out -covermode=atomic $(PLUGIN_PKGS) + @$(MAKE) --no-print-directory merge-coverage + +# `;` not `&&`, and always removes the intermediate: a failed tail must not +# leave coverage-plugin.out behind for someone to pick up by hand and misread. +merge-coverage: + @tail -n +2 coverage-plugin.out >> coverage.out; rm -f coverage-plugin.out + +# cover-diff needs a profile but must not re-run the suites in CI, where +# test-ci already wrote one. A file rule gives it both: present (CI) and make +# skips this; absent (clean local checkout) and it runs the suites once. +coverage.out: + @$(MAKE) --no-print-directory cover + +# The marker is written into the report itself, not added by the workflow: +# find-comment matches on this exact string to update its comment in place +# rather than posting a new one on every run. +COVER_MARKER := +SEC_MARKER := + +# Named once and shared by trivy-report and trivy-gate, so the report the PR +# shows and the report the gate reads can never be a different set of files. +SARIF_REPORTS := trivy-deps.sarif trivy-image.sarif + +## cover-diff: coverage of the files changed vs BASE_REF, gated on MIN_COVERAGE +# A PR review needs the diff's number, not the whole repo's. On failure, names +# the changed files dragging it down, worst first. Always writes +# coverage-report.md — the workflow reads that file unconditionally, so every +# exit path here has to produce it. +cover-diff: coverage.out + @if ! git rev-parse --verify --quiet "$(BASE_REF)^{commit}" >/dev/null; then \ + echo "::error::BASE_REF '$(BASE_REF)' does not resolve to a commit — cannot compute the changed-file set"; \ + echo "📊 **Test Coverage: ❌ Failed** — BASE_REF \`$(BASE_REF)\` does not resolve to a commit" > coverage-report.md; \ + exit 1; \ + fi; \ + if ! DIFF=$$(git diff --name-only --diff-filter=ACMR "$(BASE_REF)...HEAD" -- '*.go'); then \ + echo "::error::git diff against '$(BASE_REF)' failed — the changed-file set is unknown, not empty"; \ + echo "📊 **Test Coverage: ❌ Failed** — \`git diff\` against \`$(BASE_REF)\` failed" > coverage-report.md; \ + exit 1; \ + fi; \ + CHANGED=$$(printf '%s\n' "$$DIFF" | grep -v '_test\.go$$'); \ + if [ -z "$$CHANGED" ]; then \ + printf '%s\n' "$(COVER_MARKER)" "📊 **Test Coverage: ✅ Passed** — not applicable, no changed Go files vs $(BASE_REF)" | tee coverage-report.md; \ + exit 0; \ + fi; \ + MODULE=$$($(GO) list -m); \ + RESULT=$$(echo "$$CHANGED" | awk -v mod="$$MODULE/" -v min="$(MIN_COVERAGE)" ' \ + NR==FNR { want[mod $$0] = 1; next } \ + { f = $$1; sub(/:.*/, "", f); if (!(f in want)) next; \ + tot[f] += $$(NF-1); if ($$NF > 0) cov[f] += $$(NF-1) } \ + END { \ + T = 0; C = 0; \ + for (f in tot) { \ + T += tot[f]; C += cov[f]; \ + p = int(cov[f] * 100 / tot[f]); \ + disp = f; sub("^" mod, "", disp); \ + if (p < min) print "FILE\t" p "\t" disp; \ + } \ + if (T == 0) { print "EMPTY"; exit } \ + print "TOTAL\t" int(C * 100 / T) \ + }' - coverage.out); \ + if echo "$$RESULT" | grep -q '^EMPTY$$'; then \ + printf '%s\n' "$(COVER_MARKER)" "📊 **Test Coverage: ✅ Passed** — not applicable, changed files carry no coverable statements" | tee coverage-report.md; \ + exit 0; \ + fi; \ + PCT=$$(echo "$$RESULT" | awk -F'\t' '$$1=="TOTAL"{print $$2}'); \ + { \ + echo "$(COVER_MARKER)"; \ + if [ "$$PCT" -lt "$(MIN_COVERAGE)" ]; then \ + BELOW=$$(echo "$$RESULT" | awk -F'\t' '$$1=="FILE"{printf "%s\t%s\n",$$2,$$3}' | sort -n); \ + TOTAL_BELOW=$$(echo "$$BELOW" | wc -l); \ + echo "📊 **Test Coverage: ❌ Failed** — $${PCT}% of changed lines covered, min $(MIN_COVERAGE)%"; \ + echo; \ + echo "| File | Coverage |"; \ + echo "|---|---|"; \ + echo "$$BELOW" | head -15 | awk -F'\t' '{printf "| `%s` | %s%% |\n", $$2, $$1}'; \ + [ "$$TOTAL_BELOW" -gt 15 ] && echo "| … | $$((TOTAL_BELOW - 15)) more file(s) below $(MIN_COVERAGE)% |"; \ + else \ + echo "📊 **Test Coverage: ✅ Passed** — $${PCT}% of changed lines covered, min $(MIN_COVERAGE)%"; \ + fi; \ + } > coverage-report.md; \ + cat coverage-report.md; \ + if [ "$$PCT" -lt "$(MIN_COVERAGE)" ]; then \ + echo "::error::changed-file coverage is $${PCT}%, below the $(MIN_COVERAGE)% minimum"; \ + exit 1; \ + fi + +## trivy-deps: scan the dependency graph, SARIF report to trivy-deps.sarif +# Catches what the image scan structurally cannot — a vulnerable module only +# the test suite imports, so it is never linked into the binary or a layer. +# --skip-dirs bin: the workflow restores the cached trivy binary into bin/ +# before this runs, and a scanner reporting on its own binary is noise. +trivy-deps: $(TRIVY) + $(TRIVY) fs . --skip-dirs $(BIN_DIR) --severity $(SEVERITY) --exit-code 0 \ + --format sarif --output trivy-deps.sarif + +## trivy-image: scan IMAGE, SARIF report to trivy-image.sarif +# Reads the base layers plus the Go build info embedded in the binary — +# including `stdlib`, so a Go toolchain CVE shows up here and nowhere else. +trivy-image: $(TRIVY) + $(TRIVY) image $(IMAGE) --severity $(SEVERITY) --exit-code 0 \ + --format sarif --output trivy-image.sarif + +## trivy-report: render both SARIF reports as one PR comment, trivy-report.md +# One comment covering both scans, not one comment each: the two scans run in +# the same job now, and two bot comments per PR was the noise this is meant to +# cut. A missing report is written into the comment as missing rather than +# skipped — trivy-gate fails on it, and the comment has to agree with the gate. +# +# The jq program lives in tools/trivy-comment.jq rather than inline: as a file +# it is lintable (`jq -n -f`), diffable, and free of Makefile `$$`/backslash +# escaping. +trivy-report: + @{ \ + echo "$(SEC_MARKER)"; \ + echo "## 🛡️ Trivy security scan ($(SEVERITY))"; \ + echo "[View full run]($(RUN_URL))"; \ + for report in $(SARIF_REPORTS); do \ + case "$$report" in \ + trivy-deps.sarif) title="Go dependencies";; \ + trivy-image.sarif) title="Container image";; \ + *) title="$$report";; \ + esac; \ + echo; echo "### $$title"; echo; \ + if [ -s "$$report" ]; then \ + jq -r --arg severity "$(SEVERITY)" -f tools/trivy-comment.jq "$$report"; \ + else \ + echo "⚠️ No report — the scan did not produce $$report."; \ + fi; \ + done; \ + } > trivy-report.md + +## trivy-gate: fail if either SARIF report carries a finding, or is missing +# Reads the reports the two scans already produced rather than scanning a third +# and fourth time. A missing or unparsable report is a failure, not a pass: a +# scan that silently wrote nothing must not turn the gate into a green no-op. +trivy-gate: + @fail=0; \ + for report in $(SARIF_REPORTS); do \ + if [ ! -s "$$report" ]; then \ + echo "$$report: MISSING — no scan produced it"; \ + fail=1; continue; \ + fi; \ + count=$$(jq '[.runs[].results[]?] | length' "$$report" 2>/dev/null); \ + if [ -z "$$count" ]; then \ + echo "$$report: UNREADABLE — not valid SARIF"; \ + fail=1; continue; \ + fi; \ + echo "$$report: $$count $(SEVERITY)"; \ + if [ "$$count" -gt 0 ]; then \ + jq -r '.runs[].results[]? | "\(.ruleId) \(.message.text)"' "$$report"; \ + fail=1; \ + fi; \ + done; \ + [ "$$fail" -eq 0 ] || echo "::error::Trivy findings at $(SEVERITY), or a missing report — see the log above"; \ + exit $$fail + +## lint: vet, format check and static analysis +lint: $(GOLANGCI_LINT) + $(GOLANGCI_LINT) run ./... + $(GOLANGCI_LINT) fmt --diff ./... + +## fmt: apply the formatters that lint checks for +fmt: $(GOLANGCI_LINT) + $(GOLANGCI_LINT) fmt ./... + +## lint-actions: validate the workflows and composite actions +# Not wired into ci.yml on purpose — the pre-commit hook is the gate. Run +# whole-repo rather than per-file: actionlint resolves `needs:` across a +# workflow's jobs and checks `uses: ./.github/actions/...` against the action +# on disk, so a single file in isolation is not enough to judge either. +lint-actions: $(ACTIONLINT) + $(ACTIONLINT) + +## lint-staged: the pre-commit lints, against the staged files only. What the hook runs. +# Staged-only, and only these two checks, because both can pass today: +# +# workflows actionlint reports nothing on the current tree, so it blocks +# from day one. +# formatting 22 files in the repo are not gofmt-clean. Scoped to what you +# staged, that history is someone else's problem until you touch +# one of those files — at which point `make fmt` fixes it. +# +# Deliberately NOT `golangci-lint run`: with no .golangci.yml it uses tool +# defaults against a codebase that has never been linted, so it would reject +# every commit. Nor the test suite — a pre-commit hook has to stay in seconds, +# and CI is where `make test-ci` belongs. +# +# Reads the working tree, not the staged blob. A file staged clean but dirty in +# the working copy is reported here; that is the conservative direction, and +# avoids checking out the index to a temp dir on every commit. +lint-staged: + @STAGED=$$(git diff --cached --name-only --diff-filter=ACMR); \ + if [ -z "$$STAGED" ]; then \ + echo "lint-staged: nothing staged"; \ + exit 0; \ + fi; \ + fail=0; \ + if printf '%s\n' "$$STAGED" | grep -qE '^\.github/(workflows/.*\.ya?ml|actions/.*/action\.ya?ml)$$'; then \ + echo "==> lint-actions (staged workflow or action change)"; \ + $(MAKE) --no-print-directory lint-actions || fail=1; \ + fi; \ + GOFILES=$$(printf '%s\n' "$$STAGED" | grep '\.go$$' || true); \ + if [ -n "$$GOFILES" ]; then \ + echo "==> gofmt (staged Go files)"; \ + UNFMT=$$(printf '%s\n' "$$GOFILES" | xargs $(GOFMT) -l); \ + if [ -n "$$UNFMT" ]; then \ + echo "not gofmt-clean:"; \ + printf ' %s\n' $$UNFMT; \ + echo "run \`make fmt\` (or gofmt -w on the files above), then stage the result"; \ + fail=1; \ + fi; \ + fi; \ + if [ "$$fail" -ne 0 ]; then \ + echo; \ + echo "pre-commit checks failed — commit aborted"; \ + exit 1; \ + fi; \ + echo "lint-staged: ok" + +## hooks: point git at the repo's versioned hooks (run once per clone) +# core.hooksPath rather than copying into .git/hooks: the hook stays in the +# repo, under review, and a change to it reaches everyone on their next pull +# instead of only the people who re-copy it. +hooks: + git config core.hooksPath .githooks + @echo "core.hooksPath -> .githooks, running: $$(ls .githooks | tr '\n' ' ')" + +## docker: build the shipped adapter image — the Dockerfile and build args CI scans +# Dockerfile.adapter-with-plugins, not Dockerfile.adapter: the plugins image is +# what the Security Scan job scans and what a tag publishes, so a local +# `make docker && make trivy-image` scans the same thing CI gates on. The vars +# come from the script rather than being named again here, so ONIX_VERSION and +# friends are spelled out in exactly one place. +docker: + . install/scripts/version-vars.sh && \ + docker build -f Dockerfile.adapter-with-plugins \ + --build-arg ONIX_VERSION="$$ONIX_VERSION" \ + --build-arg GIT_COMMIT="$$GIT_COMMIT" \ + --build-arg GIT_TREE_STATE="$$GIT_TREE_STATE" \ + --build-arg BUILD_DATE="$$BUILD_DATE" \ + -t $(IMAGE) . + +## image-build: push this arch's image to IMAGE_REPO by digest (ARCH, no tag) +# Pushed untagged, by digest only. Nothing binds a version tag to it until +# image-publish has both arches, so a release that builds amd64 and then fails +# on arm64 never leaves behind a tag `docker pull` resolves on one platform and +# 404s on the other. +# +# --provenance=false: with provenance on, buildx wraps even a single-platform +# push in an OCI index to carry the attestation, and `imagetools create` would +# then compose indexes of indexes. A plain manifest per arch is what makes the +# two-platform index image-publish builds a clean one. +# +# Same Dockerfile and same version build args as `docker` above, so what a tag +# publishes is what CI scanned on the PR. +image-build: require-image-repo + . install/scripts/version-vars.sh && \ + docker buildx build -f Dockerfile.adapter-with-plugins \ + --build-arg ONIX_VERSION="$$ONIX_VERSION" \ + --build-arg GIT_COMMIT="$$GIT_COMMIT" \ + --build-arg GIT_TREE_STATE="$$GIT_TREE_STATE" \ + --build-arg BUILD_DATE="$$BUILD_DATE" \ + --platform linux/$(ARCH) \ + --provenance=false \ + --output type=image,name=$(IMAGE_REPO),push-by-digest=true,name-canonical=true,push=true \ + --metadata-file image-metadata-$(ARCH).json . + @jq -r '."containerimage.digest"' image-metadata-$(ARCH).json > digest-$(ARCH).txt + @echo "pushed $(IMAGE_REPO)@$$(cat digest-$(ARCH).txt) (linux/$(ARCH))" + +## image-publish: tag the digests image-build pushed as one multi-arch release +# The only step that creates a user-visible tag. Reads whatever digest-*.txt +# files are present rather than a fixed arch list, so adding an arch to the +# build matrix needs no change here. +# +# `&&` between every step, not `;`: a recipe is one shell invocation with no +# `set -e`, so with `;` the exit status would be `imagetools inspect`'s alone. +# A failed `create` on a tag that already exists would then leave inspect +# reporting the *previous* index and this job green — the published tag would +# point at the wrong digests with nothing red to say so. +# +# The tag comes from version-vars.sh, the same place the binary's -ldflags +# version comes from, so the image tag and `adapter --version` can't disagree. +# `latest` moves only for a plain vX.Y.Z: git describe renders a pre-release as +# v2.0.1-rc1 and an untagged commit as v1.8.2-3-gabc1234, and neither should be +# what `docker pull` gives someone who asked for no tag at all. +image-publish: require-image-repo + @ls digest-*.txt >/dev/null 2>&1 || \ + { echo "::error::no digest-*.txt — run image-build on each arch first"; exit 1; } + . install/scripts/version-vars.sh && \ + tags="-t $(IMAGE_REPO):$$ONIX_VERSION" && \ + case "$$ONIX_VERSION" in \ + *-*) echo "$$ONIX_VERSION is not a plain release — not moving :latest";; \ + *) tags="$$tags -t $(IMAGE_REPO):latest";; \ + esac && \ + docker buildx imagetools create $$tags \ + $$(for d in digest-*.txt; do echo "$(IMAGE_REPO)@$$(cat $$d)"; done) && \ + docker buildx imagetools inspect $(IMAGE_REPO):$$ONIX_VERSION + +# Split out so both image targets fail the same way, naming the thing to set, +# instead of pushing to a repo path that is just the registry and a slash. +require-image-repo: + @test "$(IMAGE_REPO)" != "$(REGISTRY)/" || \ + { echo "::error::IMAGE_REPO is empty — set GITHUB_REPOSITORY=owner/repo, or IMAGE_REPO directly"; exit 1; } + +## clean: remove build output and coverage/scan artifacts +clean: + rm -rf $(BIN_DIR) coverage.out coverage-plugin.out coverage-report.md \ + $(SARIF_REPORTS) trivy-report.md \ + image-metadata-*.json digest-*.txt + +$(GOLANGCI_LINT): + @mkdir -p $(BIN_DIR) + GOBIN=$(abspath $(BIN_DIR)) $(GO) install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION) + +# gotestsum is CI-only (see ci.yml), so it doesn't belong in the adapter's or +# the linter's dependency graph either one. +$(GOTESTSUM): + @mkdir -p $(BIN_DIR) + GOBIN=$(abspath $(BIN_DIR)) $(GO) install gotest.tools/gotestsum@$(GOTESTSUM_VERSION) + +# Pinned like the others, and go-installable, so no curl | sh for this one. +$(ACTIONLINT): + @mkdir -p $(BIN_DIR) + GOBIN=$(abspath $(BIN_DIR)) $(GO) install github.com/rhysd/actionlint/cmd/actionlint@$(ACTIONLINT_VERSION) + +# The prebuilt release binary, not `go install`: trivy's rpm-db parser needs +# cgo, and its module graph is comparable in size to golangci-lint's for a +# tool nothing here imports — the official install script is what +# aquasecurity itself recommends over building from source for exactly this. +# +# The script is fetched at $(TRIVY_VERSION), not at main: this pipes a remote +# script into sh in a job that holds the runner's GITHUB_TOKEN, so what runs +# has to be the reviewed script for the pinned release rather than whatever is +# on the default branch at the time. Every other tool here is pinned too. +$(TRIVY): + @mkdir -p $(BIN_DIR) + curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/$(TRIVY_VERSION)/contrib/install.sh | \ + sh -s -- -b $(abspath $(BIN_DIR)) $(TRIVY_VERSION) + +.PHONY: help build test cover test-ci merge-coverage cover-diff lint fmt \ + lint-actions lint-staged hooks \ + trivy-deps trivy-image trivy-report trivy-gate \ + docker image-build image-publish require-image-repo clean diff --git a/go.mod b/go.mod index fe8b8670..5715fa56 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,10 @@ module github.com/beckn-one/beckn-onix -go 1.26.1 +go 1.26.8 require ( github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 - golang.org/x/crypto v0.54.0 + golang.org/x/crypto v0.55.0 ) require github.com/stretchr/testify v1.11.1 @@ -19,7 +19,7 @@ require ( require github.com/zenazn/pkcs7pad v0.0.0-20170308005700-253a5b1f0e03 -require golang.org/x/text v0.40.0 // indirect +require golang.org/x/text v0.41.0 // indirect require ( github.com/agnivade/levenshtein v1.2.1 // indirect @@ -80,11 +80,11 @@ require ( go.uber.org/atomic v1.11.0 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/net v0.56.0 // indirect + golang.org/x/net v0.57.0 // indirect golang.org/x/sys v0.47.0 // indirect golang.org/x/time v0.15.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260427160629-7cedc36a6bc4 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect google.golang.org/protobuf v1.36.11 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) @@ -102,25 +102,25 @@ require ( github.com/lestrrat-go/jwx/v3 v3.1.0 github.com/open-policy-agent/opa v1.15.2 github.com/prometheus/client_golang v1.23.2 - github.com/rabbitmq/amqp091-go v1.11.0 + github.com/rabbitmq/amqp091-go v1.13.0 github.com/redis/go-redis/extra/redisotel/v9 v9.19.0 github.com/redis/go-redis/v9 v9.19.0 github.com/rs/zerolog v1.35.1 go.opentelemetry.io/contrib/instrumentation/runtime v0.68.0 - go.opentelemetry.io/otel v1.43.0 + go.opentelemetry.io/otel v1.44.0 go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0 go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 go.opentelemetry.io/otel/exporters/prometheus v0.65.0 go.opentelemetry.io/otel/log v0.19.0 - go.opentelemetry.io/otel/metric v1.43.0 - go.opentelemetry.io/otel/sdk v1.43.0 + go.opentelemetry.io/otel/metric v1.44.0 + go.opentelemetry.io/otel/sdk v1.44.0 go.opentelemetry.io/otel/sdk/log v0.19.0 - go.opentelemetry.io/otel/sdk/metric v1.43.0 - go.opentelemetry.io/otel/trace v1.43.0 + go.opentelemetry.io/otel/sdk/metric v1.44.0 + go.opentelemetry.io/otel/trace v1.44.0 go.uber.org/automaxprocs v1.6.0 golang.org/x/sync v0.22.0 - google.golang.org/grpc v1.82.1 + google.golang.org/grpc v1.83.1 gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/yaml.v2 v2.4.0 ) diff --git a/go.sum b/go.sum index 055511f8..35da526b 100644 --- a/go.sum +++ b/go.sum @@ -165,8 +165,8 @@ github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEo github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM= github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= -github.com/rabbitmq/amqp091-go v1.11.0 h1:HxIctVm9Gid/Vtn706necmZ7Wj6pgGI2eqplRbEY8O8= -github.com/rabbitmq/amqp091-go v1.11.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o= +github.com/rabbitmq/amqp091-go v1.13.0 h1:L8NA1WtF76C6KA3LAoufjfLgbist/If1UQYcsOjtxXA= +github.com/rabbitmq/amqp091-go v1.13.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o= github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg= github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/redis/go-redis/extra/rediscmd/v9 v9.19.0 h1:QL3vQTj64ZQpxiDZx6bFYS7oN37EdHHqiYGz3grgTRI= @@ -219,8 +219,8 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/runtime v0.68.0 h1:jhVIQEprwUTV+KfzzliLidclhoTOoHTgdz96kAyR8mU= go.opentelemetry.io/contrib/instrumentation/runtime v0.68.0/go.mod h1:4HsdbLUbernaTnA8CNaNE+1g026SciXb3juRYe3l8EY= -go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= -go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= +go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0 h1:Dn8rkudDzY6KV9dr/D/bTUuWgqDf9xe0rr4G2elrn0Y= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0/go.mod h1:gMk9F0xDgyN9M/3Ed5Y1wKcx/9mlU91NXY2SNq7RQuU= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0 h1:8UQVDcZxOJLtX6gxtDt3vY2WTgvZqMQRzjsqiIHQdkc= @@ -233,18 +233,20 @@ go.opentelemetry.io/otel/exporters/prometheus v0.65.0 h1:jOveH/b4lU9HT7y+Gfamf18 go.opentelemetry.io/otel/exporters/prometheus v0.65.0/go.mod h1:i1P8pcumauPtUI4YNopea1dhzEMuEqWP1xoUZDylLHo= go.opentelemetry.io/otel/log v0.19.0 h1:KUZs/GOsw79TBBMfDWsXS+KZ4g2Ckzksd1ymzsIEbo4= go.opentelemetry.io/otel/log v0.19.0/go.mod h1:5DQYeGmxVIr4n0/BcJvF4upsraHjg6vudJJpnkL6Ipk= -go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= -go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= -go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= -go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= +go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= +go.opentelemetry.io/otel/metric/x v0.66.0 h1:YkCrx1zLOChi9ZcZ6euupOcsgzbVlec7D/xoEU1+cTA= +go.opentelemetry.io/otel/metric/x v0.66.0/go.mod h1:d1+BDj9t96do0/1LoU1ayfCv79ZgNE41qbhBvnMOBZk= +go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= +go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= go.opentelemetry.io/otel/sdk/log v0.19.0 h1:scYVLqT22D2gqXItnWiocLUKGH9yvkkeql5dBDiXyko= go.opentelemetry.io/otel/sdk/log v0.19.0/go.mod h1:vFBowwXGLlW9AvpuF7bMgnNI95LiW10szrOdvzBHlAg= go.opentelemetry.io/otel/sdk/log/logtest v0.19.0 h1:BEbF7ZBB6qQloV/Ub1+3NQoOUnVtcGkU3XX4Ws3GQfk= go.opentelemetry.io/otel/sdk/log/logtest v0.19.0/go.mod h1:Lua81/3yM0wOmoHTokLj9y9ADeA02v1naRrVrkAZuKk= -go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= -go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= -go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= -go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= +go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= +go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= +go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= @@ -257,30 +259,30 @@ go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= -golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= -golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= -golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= -golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= -golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= +golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= +golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= +golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk= +golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= +golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= +golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= -golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= +golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= -golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= -golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= +golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE= +golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= -google.golang.org/genproto/googleapis/api v0.0.0-20260427160629-7cedc36a6bc4 h1:yOzSCGPx+cp5VO7IxvZ9SBFF7j1tZVcNtlHR2iYKtVo= -google.golang.org/genproto/googleapis/api v0.0.0-20260427160629-7cedc36a6bc4/go.mod h1:Q9HWtNeE7tM9npdIsEvqXj1QJIvVoeAV3rtXtS715Cw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4 h1:tEkOQcXgF6dH1G+MVKZrfpYvozGrzb91k6ha7jireSM= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= -google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= +google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8= +google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.83.1 h1:HIO0+BEtBP6soyqvqC8sNUjZ7bTs+0hFQuFF+RAy++Y= +google.golang.org/grpc v1.83.1/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/tools/trivy-comment.jq b/tools/trivy-comment.jq new file mode 100644 index 00000000..b56ba9fd --- /dev/null +++ b/tools/trivy-comment.jq @@ -0,0 +1,36 @@ +# Render a Trivy SARIF report as a markdown table for a PR comment. +# +# Used by the Makefile's trivy-report target for both the dependency and the +# image scan — one program, not a copy per scan. +# +# $severity is the band the scan was run at (the Makefile's SEVERITY), passed +# in with --arg so the "nothing found" line names the band it actually checked +# rather than hardcoding one that can drift from the scan. +# +# Trivy's SARIF carries no structured per-field severity/version columns: each +# finding's detail lives as prose in message.text ("Package: ...\nSeverity: +# ...\n..."), so the columns below are pulled out of that text rather than read +# from dedicated JSON fields. +# +# Lint with: jq -n --arg severity "" -f tools/trivy-comment.jq + +# capture returns null when the pattern doesn't match, so `// {v: default}` +# supplies the fallback rather than letting a missing field print "null". +def val(re; default): (capture(re) // {v: default}).v; + +[ .runs[]?.results[]? ] as $found +| if ($found | length) == 0 then + "No findings at " + $severity + "." + else + "| Package | Severity | Installed | Fixed in | Advisory |", + "|---|---|---|---|---|", + ( $found[] + | .ruleId as $id + | (.message.text // "") as $m + | "| `" + ($m | val("Package: (?[^\\n]+)"; "?")) + "` " + + "| " + ($m | val("Severity: (?[^\\n]+)"; "?")) + " " + + "| " + ($m | val("Installed Version: (?[^\\n]+)"; "?")) + " " + + "| " + ($m | val("Fixed Version: (?[^\\n]+)"; "—")) + " " + + "| [" + $id + "](" + ($m | val("Link: \\[[^]]+\\]\\((?[^)]+)\\)"; "")) + ") |" + ) + end