From b81a00afa74405af8bd33c0629eaa3c4e3300bd3 Mon Sep 17 00:00:00 2001 From: nisargabd Date: Thu, 3 Sep 2026 15:01:56 +0530 Subject: [PATCH 01/23] chore: add test, security and build-and-push CI workflows [#5] Adds a run-tests/security/build-and-push flow: gated lint, gotestsum with diff-scoped coverage, dependency+image Trivy scans gated on HIGH/CRITICAL, multi-registry multi-arch build-and-push on release tags. Both Dockerfiles switch to dhi.io base images (with public overrides for a local build without a DHI subscription), so the security and build-and-push jobs authenticate to dhi.io first. --- .github/actions/dhi-login/action.yml | 28 +++ .github/workflows/build-and-push.yml | 276 +++++++++++++++++++++++++++ .github/workflows/run-tests.yml | 107 +++++++++++ .github/workflows/security.yml | 215 +++++++++++++++++++++ .gitignore | 8 + Dockerfile.adapter | 36 +++- Dockerfile.adapter-with-plugins | 34 +++- Makefile | 175 +++++++++++++++++ 8 files changed, 864 insertions(+), 15 deletions(-) create mode 100644 .github/actions/dhi-login/action.yml create mode 100644 .github/workflows/build-and-push.yml create mode 100644 .github/workflows/run-tests.yml create mode 100644 .github/workflows/security.yml create mode 100644 Makefile diff --git a/.github/actions/dhi-login/action.yml b/.github/actions/dhi-login/action.yml new file mode 100644 index 00000000..4dac2fdb --- /dev/null +++ b/.github/actions/dhi-login/action.yml @@ -0,0 +1,28 @@ +name: Log in to dhi.io +description: > + Authenticates to dhi.io (Docker Hardened Images), gated on a Docker + subscription — anonymous pulls 401. Both Dockerfiles default to dhi.io/* + base images (see each Dockerfile's FROM comment), so every job that builds + or scans an image needs this first. Written directly to + ~/.docker/config.json rather than through docker/login-action, whose + username/password inputs don't accept a pre-combined blob — this matches + the format `docker login` itself writes. + +inputs: + username: + description: DHI credential username (org secret DOCKERHUB_USERNAME — DHI's subscription is tied to a Docker Hub account) + required: true + token: + description: DHI credential token (org secret DOCKERHUB_TOKEN) + required: true + +runs: + using: composite + steps: + - shell: bash + run: | + AUTH=$(printf '%s:%s' "${{ inputs.username }}" "${{ inputs.token }}" | base64 -w0) + mkdir -p ~/.docker + cat > ~/.docker/config.json <> "$GITHUB_OUTPUT" + + - name: Cache Trivy binary and vulnerability DB + uses: actions/cache@v4 + with: + path: | + bin/trivy + ~/.cache/trivy + key: trivy-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('Makefile') }}-${{ steps.trivy-date.outputs.date }} + restore-keys: | + trivy-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('Makefile') }}- + + - uses: docker/setup-buildx-action@v3 + + # One flag per registry, read from repo variables (Settings > Secrets + # and variables > Actions > Variables), so a registry is switched on + # without a workflow edit. GHCR is the only one we use today, so it + # defaults on; everything else defaults off until its secrets exist. + # Adding a registry later is: a flag here, a login step below, and one + # more branch in "Tag and push" and the manifest job's create_manifest + # calls — the build/scan steps never change. + - name: Set registry flags + run: | + echo "GHCR_ENABLED=${{ vars.GHCR_ENABLED != 'false' }}" >> "$GITHUB_ENV" + echo "DOCKERHUB_ENABLED=${{ vars.DOCKERHUB_ENABLED == 'true' }}" >> "$GITHUB_ENV" + echo "AZURE_CONTAINER_REGISTRY_ENABLED=${{ vars.AZURE_CONTAINER_REGISTRY_ENABLED == 'true' }}" >> "$GITHUB_ENV" + echo "GOOGLE_ARTIFACT_REGISTRY_ENABLED=${{ vars.GOOGLE_ARTIFACT_REGISTRY_ENABLED == 'true' }}" >> "$GITHUB_ENV" + + - name: Log in to dhi.io + uses: ./.github/actions/dhi-login + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + token: ${{ secrets.DOCKERHUB_TOKEN }} + + # GITHUB_TOKEN can be denied write_package when creating a brand-new + # package under a personal (non-org) namespace, even with this + # workflow's `packages: write` and the repo's default token permissions + # both set to write — a documented GHCR quirk. GHCR_PAT (classic PAT, + # write:packages) is the standard workaround; falls back to + # GITHUB_TOKEN when no such secret is set, which is enough once the + # package exists or for an org-owned repo. + - name: Log in to GHCR + if: env.GHCR_ENABLED == 'true' + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GHCR_PAT || secrets.GITHUB_TOKEN }} + + - name: Log in to Docker Hub + if: env.DOCKERHUB_ENABLED == 'true' + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} + + - name: Log in to Azure Container Registry + if: env.AZURE_CONTAINER_REGISTRY_ENABLED == 'true' + uses: docker/login-action@v3 + with: + registry: ${{ secrets.AZURE_REGISTRY_NAME }} + username: ${{ secrets.AZURE_REGISTRY_USERNAME }} + password: ${{ secrets.AZURE_REGISTRY_PASSWORD }} + + - name: Authenticate to Google Cloud + if: env.GOOGLE_ARTIFACT_REGISTRY_ENABLED == 'true' + uses: google-github-actions/auth@v3 + with: + token_format: "access_token" + workload_identity_provider: ${{ secrets.GOOGLE_WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_EMAIL }} + + - name: Configure Docker for Google Artifact Registry + if: env.GOOGLE_ARTIFACT_REGISTRY_ENABLED == 'true' + run: gcloud auth configure-docker ${{ secrets.GCP_REGION }}-docker.pkg.dev --quiet + + # GHCR image refs must be lowercase; github.repository_owner preserves + # the owner's real case, so it can't be used as-is. This resolves to + # whichever account or org owns the repo the tag was pushed to — a + # fork pushes under the fork owner, no per-person edit needed. + - name: Set image owner + run: echo "OWNER=$(echo '${{ github.repository_owner }}' | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_ENV" + + # Single source of truth for the build-time identity vars (see + # pkg/version) — written to $GITHUB_ENV so the build-push-action step + # below can reference them as build-args the same way a local + # `source install/scripts/version-vars.sh` would. + - name: Resolve version vars + run: | + source install/scripts/version-vars.sh + { + echo "ONIX_VERSION=$ONIX_VERSION" + echo "GIT_COMMIT=$GIT_COMMIT" + echo "GIT_TREE_STATE=$GIT_TREE_STATE" + echo "BUILD_DATE=$BUILD_DATE" + } >> "$GITHUB_ENV" + + # One local, unpushed tag per arch — the manifest job below stitches + # amd64 and arm64 into the single tag users actually pull. + # + # load: true instead of push: true — the image lands in this runner's + # local Docker daemon first so Trivy can scan the exact bytes about to + # ship, before it is tagged for or pushed to any registry. + - name: Build (local, unpushed) + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile.adapter + platforms: ${{ matrix.platform }} + build-args: | + ONIX_VERSION=${{ env.ONIX_VERSION }} + GIT_COMMIT=${{ env.GIT_COMMIT }} + GIT_TREE_STATE=${{ env.GIT_TREE_STATE }} + BUILD_DATE=${{ env.BUILD_DATE }} + load: true + tags: ${{ env.IMAGE_NAME }}:${{ github.ref_name }}-${{ matrix.arch }} + + # trivy-release-gate exits 1 on a finding — the release gate, not just + # a report — before any push below runs. One scan covers every registry. + - name: Trivy image scan (gate) + run: make trivy-release-gate IMAGE=${{ env.IMAGE_NAME }}:${{ github.ref_name }}-${{ matrix.arch }} + + # Re-tag the already-scanned local image for each enabled registry and + # push it there. No rebuild, no re-scan. + - name: Tag and push + run: | + LOCAL="${{ env.IMAGE_NAME }}:${{ github.ref_name }}-${{ matrix.arch }}" + TAG="${{ github.ref_name }}-${{ matrix.arch }}" + + if [ "$GHCR_ENABLED" = "true" ]; then + DEST="ghcr.io/${OWNER}/${{ env.IMAGE_NAME }}:${TAG}" + docker tag "$LOCAL" "$DEST" + docker push "$DEST" + fi + if [ "$DOCKERHUB_ENABLED" = "true" ]; then + DEST="${{ secrets.DOCKERHUB_NAMESPACE }}/${{ env.IMAGE_NAME }}:${TAG}" + docker tag "$LOCAL" "$DEST" + docker push "$DEST" + fi + if [ "$AZURE_CONTAINER_REGISTRY_ENABLED" = "true" ]; then + DEST="${{ secrets.AZURE_REGISTRY_NAME }}/${{ env.IMAGE_NAME }}:${TAG}" + docker tag "$LOCAL" "$DEST" + docker push "$DEST" + fi + if [ "$GOOGLE_ARTIFACT_REGISTRY_ENABLED" = "true" ]; then + DEST="${{ secrets.GCP_REGION }}-docker.pkg.dev/${{ secrets.GCP_PROJECT_ID }}/${{ secrets.GOOGLE_ARTIFACT_REPO }}/${{ env.IMAGE_NAME }}:${TAG}" + docker tag "$LOCAL" "$DEST" + docker push "$DEST" + fi + + manifest: + runs-on: ubuntu-latest + needs: build + steps: + - uses: docker/setup-buildx-action@v3 + + - name: Set registry flags + run: | + echo "GHCR_ENABLED=${{ vars.GHCR_ENABLED != 'false' }}" >> "$GITHUB_ENV" + echo "DOCKERHUB_ENABLED=${{ vars.DOCKERHUB_ENABLED == 'true' }}" >> "$GITHUB_ENV" + echo "AZURE_CONTAINER_REGISTRY_ENABLED=${{ vars.AZURE_CONTAINER_REGISTRY_ENABLED == 'true' }}" >> "$GITHUB_ENV" + echo "GOOGLE_ARTIFACT_REGISTRY_ENABLED=${{ vars.GOOGLE_ARTIFACT_REGISTRY_ENABLED == 'true' }}" >> "$GITHUB_ENV" + + - name: Log in to GHCR + if: env.GHCR_ENABLED == 'true' + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GHCR_PAT || secrets.GITHUB_TOKEN }} + + - name: Log in to Docker Hub + if: env.DOCKERHUB_ENABLED == 'true' + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} + + - name: Log in to Azure Container Registry + if: env.AZURE_CONTAINER_REGISTRY_ENABLED == 'true' + uses: docker/login-action@v3 + with: + registry: ${{ secrets.AZURE_REGISTRY_NAME }} + username: ${{ secrets.AZURE_REGISTRY_USERNAME }} + password: ${{ secrets.AZURE_REGISTRY_PASSWORD }} + + - name: Authenticate to Google Cloud + if: env.GOOGLE_ARTIFACT_REGISTRY_ENABLED == 'true' + uses: google-github-actions/auth@v3 + with: + token_format: "access_token" + workload_identity_provider: ${{ secrets.GOOGLE_WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_EMAIL }} + + - name: Configure Docker for Google Artifact Registry + if: env.GOOGLE_ARTIFACT_REGISTRY_ENABLED == 'true' + run: gcloud auth configure-docker ${{ secrets.GCP_REGION }}-docker.pkg.dev --quiet + + - name: Set image owner + run: echo "OWNER=$(echo '${{ github.repository_owner }}' | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_ENV" + + # imagetools create makes one manifest list from the two arch-specific + # images the build job just pushed to that registry — this is what + # lets `docker pull` resolve the right arch automatically on either + # amd64 or arm64. One create_manifest call per enabled registry. + - name: Create multi-arch manifests + run: | + create_manifest() { + local BASE="$1" + docker buildx imagetools create \ + -t "${BASE}:${{ github.ref_name }}" \ + -t "${BASE}:latest" \ + "${BASE}:${{ github.ref_name }}-amd64" \ + "${BASE}:${{ github.ref_name }}-arm64" + } + + if [ "$GHCR_ENABLED" = "true" ]; then + create_manifest "ghcr.io/${OWNER}/${{ env.IMAGE_NAME }}" + fi + if [ "$DOCKERHUB_ENABLED" = "true" ]; then + create_manifest "${{ secrets.DOCKERHUB_NAMESPACE }}/${{ env.IMAGE_NAME }}" + fi + if [ "$AZURE_CONTAINER_REGISTRY_ENABLED" = "true" ]; then + create_manifest "${{ secrets.AZURE_REGISTRY_NAME }}/${{ env.IMAGE_NAME }}" + fi + if [ "$GOOGLE_ARTIFACT_REGISTRY_ENABLED" = "true" ]; then + create_manifest "${{ secrets.GCP_REGION }}-docker.pkg.dev/${{ secrets.GCP_PROJECT_ID }}/${{ secrets.GOOGLE_ARTIFACT_REPO }}/${{ env.IMAGE_NAME }}" + fi diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml new file mode 100644 index 00000000..47f4136f --- /dev/null +++ b/.github/workflows/run-tests.yml @@ -0,0 +1,107 @@ +name: Run Tests + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + pull-requests: write + +concurrency: + group: test-${{ github.ref }} + cancel-in-progress: true + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + # continue-on-error rather than dropping the job: CLAUDE.md keeps CI + # lint as the safety net behind the pre-commit hook, which is bypassable + # (--no-verify). Advisory here, not a gate — a red lint check must never + # be the reason a PR can't merge, only a reason someone looks. + - run: make lint + continue-on-error: true + + test: + runs-on: ubuntu-latest + steps: + # fetch-depth: 0 — cover-diff below needs the base branch's history + # locally to compute the PR's changed files; 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 + + # continue-on-error so a coverage miss still posts its report; the last + # step in the job turns that outcome back into a failure. Always + # diff-scoped, never whole-repo — main only ever moves by PR merge + # (CLAUDE.md: never commit directly to main), so a PR's own diff is + # already the gate that matters. On a pull_request event that's the PR + # branch vs its target; on a push to main (the merge landing) it's + # github.event.before, the commit main pointed to right before this one. + - name: Check coverage + id: coverage + continue-on-error: true + run: | + if [ "${{ github.event_name }}" = "pull_request" ]; then + make cover-diff BASE_REF="origin/${{ github.event.pull_request.base.ref }}" + else + make cover-diff BASE_REF="${{ github.event.before }}" + fi + + # Marker goes into the file itself, not just the step summary — it's + # what body-path feeds the PR comment below, and find-comment matches + # on this exact string to update rather than duplicate that comment. + - name: Publish coverage summary + run: | + printf '\n%s\n' "$(cat coverage-report.md)" > coverage-report.md + cat coverage-report.md >> "$GITHUB_STEP_SUMMARY" + + # Always posted, pass or 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 just never trigger? Always-visible with an + # explicit ✅/❌ (cover-diff's own output) answers that without a + # reviewer having to open the job. Updated in place across runs, not + # reposted, via find-comment below. + # + # 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. A comment that can't post must never + # fail the job — coverage already gated above, this step is cosmetic. + - name: Find existing coverage comment + if: github.event_name == 'pull_request' + continue-on-error: true + uses: peter-evans/find-comment@v3 + id: find-coverage-comment + with: + issue-number: ${{ github.event.pull_request.number }} + comment-author: "github-actions[bot]" + body-includes: "" + + - name: Comment coverage on PR + if: github.event_name == 'pull_request' + continue-on-error: true + uses: peter-evans/create-or-update-comment@v4 + 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 + + - name: Gate on minimum coverage + if: steps.coverage.outcome == 'failure' + run: | + echo "::error::coverage is below the minimum — see the summary above" + exit 1 diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml new file mode 100644 index 00000000..f4a66049 --- /dev/null +++ b/.github/workflows/security.yml @@ -0,0 +1,215 @@ +name: Security + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + # Findings are uploaded as SARIF for GitHub's own code scanning UI (Security + # tab, diff annotations) AND posted as a PR comment — the SARIF path alone + # took three clicks (check -> run -> results) to see a finding that used to + # be one scroll away in the PR conversation. pull-requests: write is for + # the comment; security-events: write is for the SARIF upload. + pull-requests: write + security-events: write + +concurrency: + group: security-${{ github.ref }} + cancel-in-progress: true + +jobs: + security: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + # The trivy-action this replaced cached both the binary and its + # vulnerability DB across runs; `make trivy-deps`/`trivy-image` alone + # would re-download both (binary ~30MB, DB ~100MB+) on every run. + # 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 doesn't + # go stale for longer than a day. + - name: Trivy cache date + id: trivy-date + run: echo "date=$(date -u +%F)" >> "$GITHUB_OUTPUT" + + - name: Cache Trivy binary and vulnerability DB + uses: actions/cache@v4 + with: + path: | + bin/trivy + ~/.cache/trivy + key: trivy-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('Makefile') }}-${{ steps.trivy-date.outputs.date }} + restore-keys: | + trivy-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('Makefile') }}- + + # T4, half one: the dependency graph as written. This catches what the + # image scan structurally cannot — a vulnerable module that only the test + # suite imports, so it is never linked into the binary and never appears + # in a layer. + # + # Logic lives in the Makefile's trivy-deps target — same command a local + # `make trivy-deps` runs. + - name: Trivy dependency scan (report) + run: make trivy-deps + + # A category per scan: without it the second upload replaces the first, + # because GitHub keys an analysis on (ref, category). + - name: Upload dependency findings + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: trivy-deps.sarif + category: trivy-dependencies + + # Posted right after the report, on every run — pass or fail, and + # before the image is even built — so a PR shows the dependency finding + # immediately rather than after the whole job finishes. Reads the same + # SARIF just uploaded above; no second scan. + # + # Trivy's SARIF has no structured per-field severity/version columns; + # each finding's detail lives as prose in message.text ("Package: + # ...\nSeverity: ...\n..."), so the table columns below are pulled out + # of that text rather than from dedicated JSON fields. + # continue-on-error: a report-formatting step must never be able to + # fail the job — the gate at the bottom is what decides that. + - name: Assemble Trivy dependency PR comment + if: github.event_name == 'pull_request' + continue-on-error: true + run: | + { + echo "" + echo "### 🛡️ Trivy — Go dependency scan (HIGH,CRITICAL)" + echo "[View full run](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID})" + echo + jq -r ' + def val(re; default): (capture(re) // {v: default}).v; + [ .runs[].results[]? ] as $found + | if ($found | length) == 0 then + "No HIGH or CRITICAL findings." + 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 + ' trivy-deps.sarif + } > trivy-deps-comment.md + + - name: Find existing Trivy dependency comment + if: github.event_name == 'pull_request' + continue-on-error: true + uses: peter-evans/find-comment@v3 + id: find-trivy-deps-comment + with: + issue-number: ${{ github.event.pull_request.number }} + comment-author: "github-actions[bot]" + body-includes: "" + + # continue-on-error: a fork PR's default GITHUB_TOKEN is read-only, so + # this 403s there even with pull-requests: write declared. The gate at + # the bottom of this job is what actually decides pass/fail; a comment + # that can't post must never fail the job over that. + - name: Comment Trivy dependency report on PR + if: github.event_name == 'pull_request' + continue-on-error: true + uses: peter-evans/create-or-update-comment@v4 + with: + comment-id: ${{ steps.find-trivy-deps-comment.outputs.comment-id }} + issue-number: ${{ github.event.pull_request.number }} + body-path: trivy-deps-comment.md + edit-mode: replace + + - name: Log in to dhi.io + uses: ./.github/actions/dhi-login + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + token: ${{ secrets.DOCKERHUB_TOKEN }} + + # Both Dockerfiles default to dhi.io/* base images, and Trivy reads the + # base layers plus the Go build info embedded in the binary — including + # `stdlib`, so a Go toolchain CVE is reported here and nowhere else the + # dependency scan above cannot see. + - name: Build image + run: | + 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 network-adapter:${{ github.sha }} . + # Plain `docker build`, not build-push-action, matches security.yml's + # single-arch report scan — build-and-push.yml is where per-arch, + # buildx-driven builds actually matter. + + - name: Trivy image scan (report) + run: make trivy-image IMAGE=network-adapter:${{ github.sha }} + + - name: Upload image findings + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: trivy-image.sarif + category: trivy-image + + - name: Assemble Trivy image PR comment + if: github.event_name == 'pull_request' + continue-on-error: true + run: | + { + echo "" + echo "### 🛡️ Trivy — image scan (HIGH,CRITICAL)" + echo "[View full run](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID})" + echo + jq -r ' + def val(re; default): (capture(re) // {v: default}).v; + [ .runs[].results[]? ] as $found + | if ($found | length) == 0 then + "No HIGH or CRITICAL findings." + 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 + ' trivy-image.sarif + } > trivy-image-comment.md + + - name: Find existing Trivy image comment + if: github.event_name == 'pull_request' + continue-on-error: true + uses: peter-evans/find-comment@v3 + id: find-trivy-image-comment + with: + issue-number: ${{ github.event.pull_request.number }} + comment-author: "github-actions[bot]" + body-includes: "" + + - name: Comment Trivy image report on PR + if: github.event_name == 'pull_request' + continue-on-error: true + uses: peter-evans/create-or-update-comment@v4 + with: + comment-id: ${{ steps.find-trivy-image-comment.outputs.comment-id }} + issue-number: ${{ github.event.pull_request.number }} + body-path: trivy-image-comment.md + edit-mode: replace + + # Gate on the reports already produced rather than scanning a third and + # fourth time — logic lives in the Makefile's trivy-gate target so a + # local `make trivy-gate` checks the exact same two files this job + # just wrote. Last in the job, so a red build still leaves both reports + # behind. + - name: Gate on HIGH or CRITICAL findings + run: make trivy-gate || { echo "::error::HIGH or CRITICAL Trivy findings — see job log above"; exit 1; } diff --git a/.gitignore b/.gitignore index 618e9b42..48d66b76 100644 --- a/.gitignore +++ b/.gitignore @@ -143,6 +143,14 @@ create_benchmark_issues.sh # Ignore coverage output files coverage.out coverage.html +coverage-report.md + +# Makefile tool/build artifacts (golangci-lint, gotestsum, trivy binaries) +/bin/ +trivy-deps.sarif +trivy-image.sarif +trivy-deps-comment.md +trivy-image-comment.md # Ignore the schema directory used for testing /schemas/ diff --git a/Dockerfile.adapter b/Dockerfile.adapter index 41dd7ca8..0b1fd641 100644 --- a/Dockerfile.adapter +++ b/Dockerfile.adapter @@ -1,4 +1,23 @@ -FROM golang:1.26.1-bookworm AS builder +# syntax=docker/dockerfile:1 + +# DHI (Docker Hardened Images) requires `docker login dhi.io` first — a Docker +# subscription credential this build cannot supply. Both stages default to it +# because it's what CI and every shipped image use, but a local build without +# that subscription can override both build-args with public equivalents: +# +# docker build -f Dockerfile.adapter \ +# --build-arg BUILD_IMAGE=golang:1.26.1-bookworm \ +# --build-arg RUNTIME_IMAGE=cgr.dev/chainguard/wolfi-base:latest \ +# -t network-adapter:dev . +# +# Tags verified against github.com/docker-hardened-images/catalog +# image/golang/debian-12/1.26-dev.yaml and image/debian-base/debian-12/12.yaml. +# No cgo here, but the -dev variant's gcc/g++/make come bundled regardless — +# nothing extra to install for either the default or the public override. +ARG BUILD_IMAGE=dhi.io/golang:1.26-debian12-dev +ARG RUNTIME_IMAGE=dhi.io/debian-base:bookworm + +FROM ${BUILD_IMAGE} AS builder WORKDIR /workspace/app COPY cmd/adapter ./cmd/adapter @@ -15,16 +34,15 @@ ARG BUILD_DATE=unknown RUN go build -ldflags "-X github.com/beckn-one/beckn-onix/pkg/version.Version=${ONIX_VERSION} -X github.com/beckn-one/beckn-onix/pkg/version.GitCommit=${GIT_COMMIT} -X github.com/beckn-one/beckn-onix/pkg/version.GitTreeState=${GIT_TREE_STATE} -X github.com/beckn-one/beckn-onix/pkg/version.BuildDate=${BUILD_DATE}" -o server cmd/adapter/main.go -# Create a minimal runtime image -FROM cgr.dev/chainguard/wolfi-base:latest -# ✅ Alpine is removed; using minimal Debian +# Debian-based hardened runtime, run-as nonroot by default — kept explicit +# below since RUNTIME_IMAGE can be overridden to wolfi-base, which uses the +# same uid 65532 convention. A shell is required here: CMD below needs +# `sh -c` to expand $CONFIG_FILE. +FROM ${RUNTIME_IMAGE} WORKDIR /app -# Copy only the built binary and plugin COPY --from=builder /workspace/app/server . -# Expose port 8080 EXPOSE 8080 - -# Run the Go server with the config flag from environment variable. -CMD ["sh", "-c", "./server --config=${CONFIG_FILE}"] \ No newline at end of file +USER 65532:65532 +CMD ["sh", "-c", "./server --config=${CONFIG_FILE}"] diff --git a/Dockerfile.adapter-with-plugins b/Dockerfile.adapter-with-plugins index a7ba5e98..de97fcd6 100644 --- a/Dockerfile.adapter-with-plugins +++ b/Dockerfile.adapter-with-plugins @@ -1,11 +1,30 @@ -FROM golang:1.26.1-bookworm AS builder +# syntax=docker/dockerfile:1 + +# DHI (Docker Hardened Images) requires `docker login dhi.io` first — a Docker +# subscription credential this build cannot supply. Both stages default to it +# because it's what CI and every shipped image use, but a local build without +# that subscription can override both build-args with public equivalents: +# +# docker build -f Dockerfile.adapter-with-plugins \ +# --build-arg BUILD_IMAGE=golang:1.26.1-bookworm \ +# --build-arg RUNTIME_IMAGE=cgr.dev/chainguard/wolfi-base:latest \ +# -t network-adapter-plugins:dev . +# +# Tags verified against github.com/docker-hardened-images/catalog +# image/golang/debian-12/1.26-dev.yaml and image/debian-base/debian-12/12.yaml. +# -buildmode=plugin needs a C toolchain regardless of base — the -dev +# variant's gcc/g++/make come bundled, same as the public bookworm override. +ARG BUILD_IMAGE=dhi.io/golang:1.26-debian12-dev +ARG RUNTIME_IMAGE=dhi.io/debian-base:bookworm + +FROM ${BUILD_IMAGE} AS builder WORKDIR /workspace/app # Specifically copy only the necessary files and directories COPY cmd/adapter/ ./cmd/adapter/ -COPY core/ ./core/ -COPY pkg/ ./pkg/ +COPY core/ ./core +COPY pkg/ ./pkg COPY install/build-plugins.sh ./install/build-plugins.sh COPY install/scripts/version-vars.sh ./install/scripts/version-vars.sh COPY go.mod . @@ -30,8 +49,11 @@ RUN chmod +x install/build-plugins.sh && \ ONIX_VERSION="${ONIX_VERSION}" GIT_COMMIT="${GIT_COMMIT}" GIT_TREE_STATE="${GIT_TREE_STATE}" BUILD_DATE="${BUILD_DATE}" \ ./install/build-plugins.sh -# Create minimal runtime image -FROM cgr.dev/chainguard/wolfi-base:latest +# Debian-based hardened runtime, run-as nonroot by default — kept explicit +# below since RUNTIME_IMAGE can be overridden to wolfi-base, which uses the +# same uid 65532 convention. A shell is required here: CMD below needs +# `sh -c` to expand $CONFIG_FILE. +FROM ${RUNTIME_IMAGE} WORKDIR /app # Copy binary and plugins built with same Go version @@ -39,5 +61,5 @@ COPY --from=builder /workspace/app/server . COPY --from=builder /workspace/app/plugins ./plugins EXPOSE 8081 - +USER 65532:65532 CMD ["sh", "-c", "./server --config=${CONFIG_FILE}"] diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..0bd4ab21 --- /dev/null +++ b/Makefile @@ -0,0 +1,175 @@ +# OAN Network Adapter — build, test and toolchain targets. +# +# Mirrors discovery-service's Makefile shape (same target names, same +# run-tests/security/build-and-push workflows call these, not raw commands), +# adapted to this module: no DB, no sqlc/migrate, no separate tools/ module — +# golangci-lint, gotestsum and trivy install straight into bin/ via `go +# install`/curl, same as discovery-service does for gotestsum and trivy. + +GO ?= go +BIN_DIR := bin +IMAGE ?= network-adapter:dev + +# 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 +BASE_REF ?= origin/main +SEVERITY ?= HIGH,CRITICAL +GOLANGCI_LINT_VERSION := v2.5.0 +GOTESTSUM_VERSION := v1.13.0 +TRIVY_VERSION := v0.74.0 + +GOLANGCI_LINT := $(BIN_DIR)/golangci-lint +GOTESTSUM := $(BIN_DIR)/gotestsum +TRIVY := $(BIN_DIR)/trivy + +.DEFAULT_GOAL := help + +## help: list the available targets +help: + @grep -hE '^## ' $(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 +test: + $(GO) test -race ./... + +## cover: run the suites and write a coverage profile +cover: + $(GO) test -race -covermode=atomic -coverprofile=coverage.out ./... + +## test-ci: run the suites through gotestsum — one line per package, coverage +## profile written alongside. What run-tests.yml calls; `make test` +## stays the plain everyday entrypoint. +test-ci: $(GOTESTSUM) + $(GOTESTSUM) --format pkgname --format-hide-empty-pkg -- \ + -race -coverprofile=coverage.out -covermode=atomic ./... + +## cover-diff: coverage restricted to files changed vs BASE_REF — a PR review +## needs the diff's number, not the whole repo's. On failure, +## names the changed files dragging the number down (worst first). +cover-diff: coverage.out + @CHANGED=$$(git diff --name-only --diff-filter=ACMR "$(BASE_REF)...HEAD" -- '*.go' | grep -v '_test\.go$$' || true); \ + if [ -z "$$CHANGED" ]; then \ + echo "📊 **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 \ + echo "📊 **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}'); \ + 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)% |"; \ + } > coverage-report.md; \ + else \ + echo "📊 **Test Coverage: ✅ Passed** — $${PCT}% of changed lines covered, min $(MIN_COVERAGE)%" > coverage-report.md; \ + fi; \ + cat coverage-report.md; \ + [ "$$PCT" -ge "$(MIN_COVERAGE)" ] + +## trivy-deps: dependency graph scan (T4), SARIF report. Catches what the +## image scan structurally cannot — a vulnerable module only the +## test suite imports, so it's never linked into the binary. +trivy-deps: $(TRIVY) + $(TRIVY) fs . --severity $(SEVERITY) --exit-code 0 \ + --format sarif --output trivy-deps.sarif + +TRIVY_IMAGE_SCAN = $(TRIVY) image $(IMAGE) --severity $(SEVERITY) + +## trivy-image: shipped image scan (T4), SARIF report. IMAGE names the ref. +trivy-image: $(TRIVY) + $(TRIVY_IMAGE_SCAN) --exit-code 0 --format sarif --output trivy-image.sarif + +## trivy-release-gate: same image scan as trivy-image, but exit 1 on a +## finding instead of writing a report — the pre-push +## release gate build-and-push.yml runs once per +## arch-tagged local image, before anything is pushed. +trivy-release-gate: $(TRIVY) + $(TRIVY_IMAGE_SCAN) --exit-code 1 --format table + +## trivy-gate: fail if either SARIF report already produced by a scan step +## carries a finding. Reads the reports rather than rescanning. +trivy-gate: + @fail=0; \ + for report in trivy-deps.sarif trivy-image.sarif; do \ + count=$$(jq '[.runs[].results[]?] | length' "$$report"); \ + echo "$${report}: $${count} $(SEVERITY)"; \ + if [ "$$count" -gt 0 ]; then \ + jq -r '.runs[].results[]? | "\(.ruleId) \(.message.text)"' "$$report"; \ + fail=1; \ + fi; \ + done; \ + exit $$fail + +## lint: vet, format check and static analysis +lint: $(GOLANGCI_LINT) + $(GOLANGCI_LINT) run ./... + $(GOLANGCI_LINT) fmt --diff ./... + +## fmt: apply the formatters lint checks for +fmt: $(GOLANGCI_LINT) + $(GOLANGCI_LINT) fmt ./... + +## docker: build the adapter image +docker: + docker build -f Dockerfile.adapter -t $(IMAGE) . + +## tools: build the pinned toolchain into bin/ +tools: $(GOLANGCI_LINT) $(GOTESTSUM) $(TRIVY) + +## clean: remove build output and coverage/scan artifacts +clean: + rm -rf $(BIN_DIR) coverage.out coverage-report.md trivy-deps.sarif trivy-image.sarif + +$(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 run-tests.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) + +# 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. +$(TRIVY): + @mkdir -p $(BIN_DIR) + curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | \ + sh -s -- -b $(abspath $(BIN_DIR)) $(TRIVY_VERSION) + +.PHONY: help build test cover test-ci cover-diff lint fmt trivy-deps \ + trivy-image trivy-release-gate trivy-gate docker tools clean From 8f6092ded57dcfa46a56a3c2fa3a8254d4d8cc0c Mon Sep 17 00:00:00 2001 From: nisargabd Date: Thu, 3 Sep 2026 15:32:42 +0530 Subject: [PATCH 02/23] fix: switch adapter runtime to a glibc-only DHI image, drop the shell dependency [#5] security.yml's Trivy gate failed on unpatched CVEs in dhi.io/debian-base's bundled perl-base, ncurses, util-linux and zlib1g -- none of which the adapter uses. Neither can move to the fully static dhi.io/static image though: the adapter loads plugin .so files at runtime via Go's `plugin` package, which needs cgo and a real glibc dynamic linker to work at all. dhi.io/static's glibc variant is the middle ground -- keeps libc6 so plugin.Open still works, drops everything else the scan was flagging. That variant has no shell, so CMD's `sh -c "./server --config=$CONFIG_FILE"` can no longer expand the env var. Switched to an exec-form ENTRYPOINT and taught main.go to read CONFIG_FILE itself as the --config default. --- Dockerfile.adapter | 23 +++++++++++++++-------- Dockerfile.adapter-with-plugins | 20 ++++++++++++-------- cmd/adapter/main.go | 11 +++++++++-- 3 files changed, 36 insertions(+), 18 deletions(-) diff --git a/Dockerfile.adapter b/Dockerfile.adapter index 0b1fd641..b3df00e8 100644 --- a/Dockerfile.adapter +++ b/Dockerfile.adapter @@ -11,11 +11,15 @@ # -t network-adapter:dev . # # Tags verified against github.com/docker-hardened-images/catalog -# image/golang/debian-12/1.26-dev.yaml and image/debian-base/debian-12/12.yaml. -# No cgo here, but the -dev variant's gcc/g++/make come bundled regardless — -# nothing extra to install for either the default or the public override. +# image/golang/debian-12/1.26-dev.yaml and image/static/debian-12/static-glibc.yaml. +# No cgo *required* for the plain build, but the adapter loads plugin .so +# files at runtime via Go's `plugin` package, which needs cgo and a real +# glibc dynamic linker regardless — so this stays a normal dynamic build, not +# -extldflags '-static'. The -dev variant's gcc/g++/make come bundled +# regardless — nothing extra to install for either the default or the public +# override. ARG BUILD_IMAGE=dhi.io/golang:1.26-debian12-dev -ARG RUNTIME_IMAGE=dhi.io/debian-base:bookworm +ARG RUNTIME_IMAGE=dhi.io/static:20250419-glibc-debian12 FROM ${BUILD_IMAGE} AS builder @@ -34,10 +38,11 @@ ARG BUILD_DATE=unknown RUN go build -ldflags "-X github.com/beckn-one/beckn-onix/pkg/version.Version=${ONIX_VERSION} -X github.com/beckn-one/beckn-onix/pkg/version.GitCommit=${GIT_COMMIT} -X github.com/beckn-one/beckn-onix/pkg/version.GitTreeState=${GIT_TREE_STATE} -X github.com/beckn-one/beckn-onix/pkg/version.BuildDate=${BUILD_DATE}" -o server cmd/adapter/main.go -# Debian-based hardened runtime, run-as nonroot by default — kept explicit +# glibc-only runtime — no shell, no perl/ncurses/util-linux (the packages +# that were carrying unpatched CVEs on the fuller debian-base image), but +# keeps libc6 so plugin.Open's dynamic linking still works. Explicit USER # below since RUNTIME_IMAGE can be overridden to wolfi-base, which uses the -# same uid 65532 convention. A shell is required here: CMD below needs -# `sh -c` to expand $CONFIG_FILE. +# same uid 65532 convention. FROM ${RUNTIME_IMAGE} WORKDIR /app @@ -45,4 +50,6 @@ COPY --from=builder /workspace/app/server . EXPOSE 8080 USER 65532:65532 -CMD ["sh", "-c", "./server --config=${CONFIG_FILE}"] +# Exec form, not `sh -c` — this base has no shell. main.go reads CONFIG_FILE +# itself as the --config default, so no shell expansion is needed here. +ENTRYPOINT ["/app/server"] diff --git a/Dockerfile.adapter-with-plugins b/Dockerfile.adapter-with-plugins index de97fcd6..043a91e6 100644 --- a/Dockerfile.adapter-with-plugins +++ b/Dockerfile.adapter-with-plugins @@ -11,11 +11,12 @@ # -t network-adapter-plugins:dev . # # Tags verified against github.com/docker-hardened-images/catalog -# image/golang/debian-12/1.26-dev.yaml and image/debian-base/debian-12/12.yaml. -# -buildmode=plugin needs a C toolchain regardless of base — the -dev -# variant's gcc/g++/make come bundled, same as the public bookworm override. +# image/golang/debian-12/1.26-dev.yaml and image/static/debian-12/static-glibc.yaml. +# -buildmode=plugin, and plugin.Open at runtime, both need a C toolchain and +# a real glibc dynamic linker — the -dev variant's gcc/g++/make come +# bundled, same as the public bookworm override. ARG BUILD_IMAGE=dhi.io/golang:1.26-debian12-dev -ARG RUNTIME_IMAGE=dhi.io/debian-base:bookworm +ARG RUNTIME_IMAGE=dhi.io/static:20250419-glibc-debian12 FROM ${BUILD_IMAGE} AS builder @@ -49,10 +50,11 @@ RUN chmod +x install/build-plugins.sh && \ ONIX_VERSION="${ONIX_VERSION}" GIT_COMMIT="${GIT_COMMIT}" GIT_TREE_STATE="${GIT_TREE_STATE}" BUILD_DATE="${BUILD_DATE}" \ ./install/build-plugins.sh -# Debian-based hardened runtime, run-as nonroot by default — kept explicit +# glibc-only runtime — no shell, no perl/ncurses/util-linux (the packages +# that were carrying unpatched CVEs on the fuller debian-base image), but +# keeps libc6 so plugin.Open's dynamic linking still works. Explicit USER # below since RUNTIME_IMAGE can be overridden to wolfi-base, which uses the -# same uid 65532 convention. A shell is required here: CMD below needs -# `sh -c` to expand $CONFIG_FILE. +# same uid 65532 convention. FROM ${RUNTIME_IMAGE} WORKDIR /app @@ -62,4 +64,6 @@ COPY --from=builder /workspace/app/plugins ./plugins EXPOSE 8081 USER 65532:65532 -CMD ["sh", "-c", "./server --config=${CONFIG_FILE}"] +# Exec form, not `sh -c` — this base has no shell. main.go reads CONFIG_FILE +# itself as the --config default, so no shell expansion is needed here. +ENTRYPOINT ["/app/server"] diff --git a/cmd/adapter/main.go b/cmd/adapter/main.go index f74d6fb0..09a88226 100644 --- a/cmd/adapter/main.go +++ b/cmd/adapter/main.go @@ -63,8 +63,15 @@ var configPath string var runFunc = run func main() { - // Define and parse command-line flags. - flag.StringVar(&configPath, "config", "../../config/onix/adapter.yaml", "Path to the configuration file") + // Define and parse command-line flags. CONFIG_FILE seeds the default so + // a shell-less container entrypoint (no `sh -c` to expand it into + // --config) can still select a config file via env var; --config still + // overrides it explicitly when passed. + defaultConfig := "../../config/onix/adapter.yaml" + if v := os.Getenv("CONFIG_FILE"); v != "" { + defaultConfig = v + } + flag.StringVar(&configPath, "config", defaultConfig, "Path to the configuration file") flag.Parse() // Use custom log for initial setup messages. From 6e2f9b8542a9d81919c0905f4e5913392200001b Mon Sep 17 00:00:00 2001 From: nisargabd Date: Thu, 3 Sep 2026 15:39:33 +0530 Subject: [PATCH 03/23] fix: bump golang.org/x/crypto and google.golang.org/grpc to close known CVEs [#5] security.yml's trivy-deps scan flagged CVE-2026-56854 (x/crypto, CRITICAL) and CVE-2026-84304 (grpc, HIGH), both with fixed versions already published upstream. --- go.mod | 22 +++++++++++----------- go.sum | 54 ++++++++++++++++++++++++++++-------------------------- 2 files changed, 39 insertions(+), 37 deletions(-) diff --git a/go.mod b/go.mod index fe8b8670..63fed0f4 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.26.1 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 ) @@ -107,20 +107,20 @@ require ( 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..3f7b7a09 100644 --- a/go.sum +++ b/go.sum @@ -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= From 7acedd871c74447635f7668c17994b713271b75d Mon Sep 17 00:00:00 2001 From: nisargabd Date: Thu, 3 Sep 2026 15:56:01 +0530 Subject: [PATCH 04/23] fix: disable Terraform deploy's push trigger [#5] Ran on every push including this branch and always failed here -- missing Gerrit credentials for the private Terraform-CICD repo it clones. workflow_dispatch stays, so it's still runnable by hand; kept, not deleted, pending a decision on whether it's folded into build-and-push.yml or removed outright. --- .github/workflows/onix-gcp-terraform-deploy.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/onix-gcp-terraform-deploy.yml b/.github/workflows/onix-gcp-terraform-deploy.yml index 478979be..4d35fe94 100644 --- a/.github/workflows/onix-gcp-terraform-deploy.yml +++ b/.github/workflows/onix-gcp-terraform-deploy.yml @@ -1,7 +1,9 @@ name: Terraform Deploy to GCP on: - push: + # push trigger disabled — this always fails (missing Gerrit credentials in + # this repo) and ran on every push, including unrelated branches. Kept as + # manual-only until it's confirmed dead or folded into build-and-push.yml. workflow_dispatch: # Manual triggerr jobs: From 3edaf4236da86d6f1928e502fde3311db8d08748 Mon Sep 17 00:00:00 2001 From: nisargabd Date: Thu, 3 Sep 2026 16:37:32 +0530 Subject: [PATCH 05/23] chore: keep golang.org/x/crypto and google.golang.org/grpc at prior versions [#5] This is CI setup, not a dependency-bump PR -- the two CVEs trivy-deps flags (CVE-2026-56854, CVE-2026-84304) are real and the scan catching them is working as intended; the actual bump is deferred to its own change. --- go.mod | 22 +++++++++++----------- go.sum | 54 ++++++++++++++++++++++++++---------------------------- 2 files changed, 37 insertions(+), 39 deletions(-) diff --git a/go.mod b/go.mod index 63fed0f4..fe8b8670 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.26.1 require ( github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 - golang.org/x/crypto v0.55.0 + golang.org/x/crypto v0.54.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.41.0 // indirect +require golang.org/x/text v0.40.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.57.0 // indirect + golang.org/x/net v0.56.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-20260526163538-3dc84a4a5aaa // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // 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/protobuf v1.36.11 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) @@ -107,20 +107,20 @@ require ( 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.44.0 + go.opentelemetry.io/otel v1.43.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.44.0 - go.opentelemetry.io/otel/sdk v1.44.0 + go.opentelemetry.io/otel/metric v1.43.0 + go.opentelemetry.io/otel/sdk v1.43.0 go.opentelemetry.io/otel/sdk/log v0.19.0 - go.opentelemetry.io/otel/sdk/metric v1.44.0 - go.opentelemetry.io/otel/trace v1.44.0 + go.opentelemetry.io/otel/sdk/metric v1.43.0 + go.opentelemetry.io/otel/trace v1.43.0 go.uber.org/automaxprocs v1.6.0 golang.org/x/sync v0.22.0 - google.golang.org/grpc v1.83.1 + google.golang.org/grpc v1.82.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 3f7b7a09..055511f8 100644 --- a/go.sum +++ b/go.sum @@ -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.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= -go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= +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/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,20 +233,18 @@ 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.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/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/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.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/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/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= @@ -259,30 +257,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.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/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/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.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= -golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= +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/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= -golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE= -golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk= +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= 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-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/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/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= From e45b7bf91fa21c8627c6cb4063376f9cf15e8938 Mon Sep 17 00:00:00 2001 From: nisargabd Date: Thu, 3 Sep 2026 17:19:06 +0530 Subject: [PATCH 06/23] fix: scope the DHI runtime switch to the plugins image only [#5] Dockerfile.adapter goes back to the original public base images -- Dockerfile.adapter-with-plugins is the one actually deployed, so that's the one that needs the CVE fix. Keeps dhi.io/static's glibc-only variant (clears the unpatched perl-base/ncurses/util-linux/zlib1g CVEs, keeps libc6 so plugin.Open still works) but drops the main.go change entirely: Deployment/deployment.yaml now passes --config=$(CONFIG_FILE) via args, letting the kubelet do the substitution instead of a shell inside the container. --- Deployment/deployment.yaml | 5 +++++ Dockerfile.adapter | 23 ++++++++--------------- Dockerfile.adapter-with-plugins | 7 +++++-- cmd/adapter/main.go | 11 ++--------- 4 files changed, 20 insertions(+), 26 deletions(-) diff --git a/Deployment/deployment.yaml b/Deployment/deployment.yaml index 561fdb5a..443ac03c 100644 --- a/Deployment/deployment.yaml +++ b/Deployment/deployment.yaml @@ -19,6 +19,11 @@ spec: containers: - name: onix-adapter image: "asia-south1-docker.pkg.dev/trusty-relic-370809/onix-adapter-cicd/beckn-onix:latest" #------ + # $(CONFIG_FILE) is expanded by the kubelet against the env below + # before the container starts -- the image's shell-less DHI base + # has no `sh -c` to do that substitution itself. + args: + - "--config=$(CONFIG_FILE)" ports: - containerPort: 8080 env: diff --git a/Dockerfile.adapter b/Dockerfile.adapter index b3df00e8..0b1fd641 100644 --- a/Dockerfile.adapter +++ b/Dockerfile.adapter @@ -11,15 +11,11 @@ # -t network-adapter:dev . # # Tags verified against github.com/docker-hardened-images/catalog -# image/golang/debian-12/1.26-dev.yaml and image/static/debian-12/static-glibc.yaml. -# No cgo *required* for the plain build, but the adapter loads plugin .so -# files at runtime via Go's `plugin` package, which needs cgo and a real -# glibc dynamic linker regardless — so this stays a normal dynamic build, not -# -extldflags '-static'. The -dev variant's gcc/g++/make come bundled -# regardless — nothing extra to install for either the default or the public -# override. +# image/golang/debian-12/1.26-dev.yaml and image/debian-base/debian-12/12.yaml. +# No cgo here, but the -dev variant's gcc/g++/make come bundled regardless — +# nothing extra to install for either the default or the public override. ARG BUILD_IMAGE=dhi.io/golang:1.26-debian12-dev -ARG RUNTIME_IMAGE=dhi.io/static:20250419-glibc-debian12 +ARG RUNTIME_IMAGE=dhi.io/debian-base:bookworm FROM ${BUILD_IMAGE} AS builder @@ -38,11 +34,10 @@ ARG BUILD_DATE=unknown RUN go build -ldflags "-X github.com/beckn-one/beckn-onix/pkg/version.Version=${ONIX_VERSION} -X github.com/beckn-one/beckn-onix/pkg/version.GitCommit=${GIT_COMMIT} -X github.com/beckn-one/beckn-onix/pkg/version.GitTreeState=${GIT_TREE_STATE} -X github.com/beckn-one/beckn-onix/pkg/version.BuildDate=${BUILD_DATE}" -o server cmd/adapter/main.go -# glibc-only runtime — no shell, no perl/ncurses/util-linux (the packages -# that were carrying unpatched CVEs on the fuller debian-base image), but -# keeps libc6 so plugin.Open's dynamic linking still works. Explicit USER +# Debian-based hardened runtime, run-as nonroot by default — kept explicit # below since RUNTIME_IMAGE can be overridden to wolfi-base, which uses the -# same uid 65532 convention. +# same uid 65532 convention. A shell is required here: CMD below needs +# `sh -c` to expand $CONFIG_FILE. FROM ${RUNTIME_IMAGE} WORKDIR /app @@ -50,6 +45,4 @@ COPY --from=builder /workspace/app/server . EXPOSE 8080 USER 65532:65532 -# Exec form, not `sh -c` — this base has no shell. main.go reads CONFIG_FILE -# itself as the --config default, so no shell expansion is needed here. -ENTRYPOINT ["/app/server"] +CMD ["sh", "-c", "./server --config=${CONFIG_FILE}"] diff --git a/Dockerfile.adapter-with-plugins b/Dockerfile.adapter-with-plugins index 043a91e6..6f72a4c0 100644 --- a/Dockerfile.adapter-with-plugins +++ b/Dockerfile.adapter-with-plugins @@ -64,6 +64,9 @@ COPY --from=builder /workspace/app/plugins ./plugins EXPOSE 8081 USER 65532:65532 -# Exec form, not `sh -c` — this base has no shell. main.go reads CONFIG_FILE -# itself as the --config default, so no shell expansion is needed here. +# Exec form, not `sh -c` — this base has no shell, and no Go code reads +# CONFIG_FILE either. The deployment layer resolves it instead: Kubernetes +# expands $(CONFIG_FILE) in args itself before exec'ing the container (see +# Deployment/deployment.yaml) -- no shell involved on either side. A plain +# `docker run` without that substitution needs an explicit --config flag. ENTRYPOINT ["/app/server"] diff --git a/cmd/adapter/main.go b/cmd/adapter/main.go index 09a88226..f74d6fb0 100644 --- a/cmd/adapter/main.go +++ b/cmd/adapter/main.go @@ -63,15 +63,8 @@ var configPath string var runFunc = run func main() { - // Define and parse command-line flags. CONFIG_FILE seeds the default so - // a shell-less container entrypoint (no `sh -c` to expand it into - // --config) can still select a config file via env var; --config still - // overrides it explicitly when passed. - defaultConfig := "../../config/onix/adapter.yaml" - if v := os.Getenv("CONFIG_FILE"); v != "" { - defaultConfig = v - } - flag.StringVar(&configPath, "config", defaultConfig, "Path to the configuration file") + // Define and parse command-line flags. + flag.StringVar(&configPath, "config", "../../config/onix/adapter.yaml", "Path to the configuration file") flag.Parse() // Use custom log for initial setup messages. From 392e4bb46a9ead95ff022372e962f642e3376c85 Mon Sep 17 00:00:00 2001 From: nisargabd Date: Thu, 3 Sep 2026 17:25:16 +0530 Subject: [PATCH 07/23] fix: build and scan Dockerfile.adapter-with-plugins, not Dockerfile.adapter [#5] security.yml and build-and-push.yml still pointed at Dockerfile.adapter, which we just reverted back to the original, unpatched base images -- so the security gate was scanning the wrong Dockerfile and the CVE fix never took effect. Dockerfile.adapter-with-plugins is the one actually deployed and the one carrying the DHI glibc-only runtime fix. --- .github/workflows/build-and-push.yml | 2 +- .github/workflows/security.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-and-push.yml b/.github/workflows/build-and-push.yml index 4ff2479f..2d49bbcf 100644 --- a/.github/workflows/build-and-push.yml +++ b/.github/workflows/build-and-push.yml @@ -153,7 +153,7 @@ jobs: uses: docker/build-push-action@v6 with: context: . - file: Dockerfile.adapter + file: Dockerfile.adapter-with-plugins platforms: ${{ matrix.platform }} build-args: | ONIX_VERSION=${{ env.ONIX_VERSION }} diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index f4a66049..cfc42c5c 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -139,7 +139,7 @@ jobs: - name: Build image run: | source install/scripts/version-vars.sh - docker build -f Dockerfile.adapter \ + 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" \ From 3e88a458b624c77023916cd3793e33272c2a929c Mon Sep 17 00:00:00 2001 From: nisargabd Date: Fri, 4 Sep 2026 14:48:24 +0530 Subject: [PATCH 08/23] fix: keep the plugin-building test packages out of the full-module race+coverage run [#5] pkg/plugin and benchmarks/e2e each build a real .so via a subprocess `go build -buildmode=plugin`, then load it with plugin.Open in the same test run. Instrumenting the whole module for coverage/race in one ./... build gives a shared package a different build identity than the plain subprocess build produces, so plugin.Open rejects the .so. Split them into their own go test invocation and merge the coverage output. --- Makefile | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 0bd4ab21..4a9f1098 100644 --- a/Makefile +++ b/Makefile @@ -47,9 +47,25 @@ cover: ## test-ci: run the suites through gotestsum — one line per package, coverage ## profile written alongside. What run-tests.yml calls; `make test` ## stays the plain everyday entrypoint. +# +# 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. Instrumenting the *whole* module's coverage in one `./...` build +# gives shared packages (e.g. pkg/plugin/definition) a build identity that +# subprocess build doesn't share, so plugin.Open rejects the .so as built +# with a different version of that package. Splitting them into their own +# `go test` invocation keeps their build graph small enough to match. test-ci: $(GOTESTSUM) $(GOTESTSUM) --format pkgname --format-hide-empty-pkg -- \ - -race -coverprofile=coverage.out -covermode=atomic ./... + -race -coverprofile=coverage.out -covermode=atomic \ + $$(go list ./... | grep -vE '/pkg/plugin$$|/benchmarks/e2e$$') + # No -race here: these two packages build a plugin .so in a subprocess + # `go build` with no -race flag of its own, so a race-instrumented test + # binary and a non-race .so mismatch and plugin.Open refuses to load it. + $(GOTESTSUM) --format pkgname --format-hide-empty-pkg -- \ + -coverprofile=coverage-plugin.out -covermode=atomic \ + ./pkg/plugin ./benchmarks/e2e/... + @tail -n +2 coverage-plugin.out >> coverage.out && rm -f coverage-plugin.out ## cover-diff: coverage restricted to files changed vs BASE_REF — a PR review ## needs the diff's number, not the whole repo's. On failure, From a7bace36e9faeb6aa7c036d006def689be15bd70 Mon Sep 17 00:00:00 2001 From: nisargabd Date: Fri, 4 Sep 2026 14:50:29 +0530 Subject: [PATCH 09/23] docs: drop the discovery-service reference from the Makefile header [#5] --- Makefile | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 4a9f1098..20e437f3 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,9 @@ # OAN Network Adapter — build, test and toolchain targets. # -# Mirrors discovery-service's Makefile shape (same target names, same -# run-tests/security/build-and-push workflows call these, not raw commands), -# adapted to this module: no DB, no sqlc/migrate, no separate tools/ module — -# golangci-lint, gotestsum and trivy install straight into bin/ via `go -# install`/curl, same as discovery-service does for gotestsum and trivy. +# Single source of truth for the run-tests/security/build-and-push workflows, +# which call these targets rather than raw commands. 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 From 6220fc4ec80a2688c908ffffdc9e8e7fdc925e0f Mon Sep 17 00:00:00 2001 From: Manjunath Davanam Date: Mon, 7 Sep 2026 22:36:16 +0530 Subject: [PATCH 10/23] chore: drop the dead and deploy-only GitHub Actions workflows [#5] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit None of these six can run or need to run on this repo as it stands: beckn_ci.yml, beckn_ci_test.yml trigger on PRs to beckn-onix-v1.0-develop, a branch that does not exist here — they have never fired and cannot. deploy-to-gke.yml, deploy-to-gke-BS.yml, build-and-deploy-plugins.yml, onix-gcp-terraform-deploy.yml deployment, which is out of scope for now. Keeping them means keeping the GCP/registry secret surface and the maintenance cost of workflows nobody runs. What is left is CI, Security and sign-beckn-constants.yml. --- .github/workflows/beckn_ci.yml | 88 ------------- .github/workflows/beckn_ci_test.yml | 85 ------------- .../workflows/build-and-deploy-plugins.yml | 116 ------------------ .github/workflows/deploy-to-gke-BS.yml | 69 ----------- .github/workflows/deploy-to-gke.yml | 44 ------- .../workflows/onix-gcp-terraform-deploy.yml | 68 ---------- 6 files changed, 470 deletions(-) delete mode 100644 .github/workflows/beckn_ci.yml delete mode 100644 .github/workflows/beckn_ci_test.yml delete mode 100644 .github/workflows/build-and-deploy-plugins.yml delete mode 100644 .github/workflows/deploy-to-gke-BS.yml delete mode 100644 .github/workflows/deploy-to-gke.yml delete mode 100644 .github/workflows/onix-gcp-terraform-deploy.yml 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/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 4d35fe94..00000000 --- a/.github/workflows/onix-gcp-terraform-deploy.yml +++ /dev/null @@ -1,68 +0,0 @@ -name: Terraform Deploy to GCP - -on: - # push trigger disabled — this always fails (missing Gerrit credentials in - # this repo) and ran on every push, including unrelated branches. Kept as - # manual-only until it's confirmed dead or folded into build-and-push.yml. - 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 - From 56f7d8bc3a19223248087c201e3fd2d42043f377 Mon Sep 17 00:00:00 2001 From: Manjunath Davanam Date: Mon, 7 Sep 2026 22:36:37 +0530 Subject: [PATCH 11/23] chore: drop the release workflow and revert the DHI image migration [#5] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit build-and-push.yml is release/deploy, which is out of scope for now, so it goes with the other deploy workflows. That also retires the review findings that only existed inside it (the tag globs, id-token: write with no consumer, :latest on a -rc tag, the duplicated registry-login block, and the multi-arch manifest step). The Docker Hardened Images switch went with it, and it had to: Dockerfile.adapter was migrated to dhi.io in a PR whose stated scope is CI workflows. dhi.io is subscription-gated, so the image is not pullable without the DHI entitlement. Dockerfile.adapter-with-plugins same switch, plus trailing-slash churn. Back to golang:1.26.1-bookworm building and cgr.dev/chainguard/wolfi-base as the runtime — which has a shell and glibc, so plugin.Open works, and is publicly pullable, so a fork PR's image scan still builds. Deployment/deployment.yaml the added args: ["--config=$(CONFIG_FILE)"] block does not match the adapter's flags and would CrashLoopBackOff on rollout. .github/actions/dhi-login only existed to authenticate to dhi.io. All three files are now byte-identical to development. --- .github/actions/dhi-login/action.yml | 28 --- .github/workflows/build-and-push.yml | 276 --------------------------- Deployment/deployment.yaml | 5 - Dockerfile.adapter | 36 +--- Dockerfile.adapter-with-plugins | 43 +---- 5 files changed, 16 insertions(+), 372 deletions(-) delete mode 100644 .github/actions/dhi-login/action.yml delete mode 100644 .github/workflows/build-and-push.yml diff --git a/.github/actions/dhi-login/action.yml b/.github/actions/dhi-login/action.yml deleted file mode 100644 index 4dac2fdb..00000000 --- a/.github/actions/dhi-login/action.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Log in to dhi.io -description: > - Authenticates to dhi.io (Docker Hardened Images), gated on a Docker - subscription — anonymous pulls 401. Both Dockerfiles default to dhi.io/* - base images (see each Dockerfile's FROM comment), so every job that builds - or scans an image needs this first. Written directly to - ~/.docker/config.json rather than through docker/login-action, whose - username/password inputs don't accept a pre-combined blob — this matches - the format `docker login` itself writes. - -inputs: - username: - description: DHI credential username (org secret DOCKERHUB_USERNAME — DHI's subscription is tied to a Docker Hub account) - required: true - token: - description: DHI credential token (org secret DOCKERHUB_TOKEN) - required: true - -runs: - using: composite - steps: - - shell: bash - run: | - AUTH=$(printf '%s:%s' "${{ inputs.username }}" "${{ inputs.token }}" | base64 -w0) - mkdir -p ~/.docker - cat > ~/.docker/config.json <> "$GITHUB_OUTPUT" - - - name: Cache Trivy binary and vulnerability DB - uses: actions/cache@v4 - with: - path: | - bin/trivy - ~/.cache/trivy - key: trivy-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('Makefile') }}-${{ steps.trivy-date.outputs.date }} - restore-keys: | - trivy-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('Makefile') }}- - - - uses: docker/setup-buildx-action@v3 - - # One flag per registry, read from repo variables (Settings > Secrets - # and variables > Actions > Variables), so a registry is switched on - # without a workflow edit. GHCR is the only one we use today, so it - # defaults on; everything else defaults off until its secrets exist. - # Adding a registry later is: a flag here, a login step below, and one - # more branch in "Tag and push" and the manifest job's create_manifest - # calls — the build/scan steps never change. - - name: Set registry flags - run: | - echo "GHCR_ENABLED=${{ vars.GHCR_ENABLED != 'false' }}" >> "$GITHUB_ENV" - echo "DOCKERHUB_ENABLED=${{ vars.DOCKERHUB_ENABLED == 'true' }}" >> "$GITHUB_ENV" - echo "AZURE_CONTAINER_REGISTRY_ENABLED=${{ vars.AZURE_CONTAINER_REGISTRY_ENABLED == 'true' }}" >> "$GITHUB_ENV" - echo "GOOGLE_ARTIFACT_REGISTRY_ENABLED=${{ vars.GOOGLE_ARTIFACT_REGISTRY_ENABLED == 'true' }}" >> "$GITHUB_ENV" - - - name: Log in to dhi.io - uses: ./.github/actions/dhi-login - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - token: ${{ secrets.DOCKERHUB_TOKEN }} - - # GITHUB_TOKEN can be denied write_package when creating a brand-new - # package under a personal (non-org) namespace, even with this - # workflow's `packages: write` and the repo's default token permissions - # both set to write — a documented GHCR quirk. GHCR_PAT (classic PAT, - # write:packages) is the standard workaround; falls back to - # GITHUB_TOKEN when no such secret is set, which is enough once the - # package exists or for an org-owned repo. - - name: Log in to GHCR - if: env.GHCR_ENABLED == 'true' - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GHCR_PAT || secrets.GITHUB_TOKEN }} - - - name: Log in to Docker Hub - if: env.DOCKERHUB_ENABLED == 'true' - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_PASSWORD }} - - - name: Log in to Azure Container Registry - if: env.AZURE_CONTAINER_REGISTRY_ENABLED == 'true' - uses: docker/login-action@v3 - with: - registry: ${{ secrets.AZURE_REGISTRY_NAME }} - username: ${{ secrets.AZURE_REGISTRY_USERNAME }} - password: ${{ secrets.AZURE_REGISTRY_PASSWORD }} - - - name: Authenticate to Google Cloud - if: env.GOOGLE_ARTIFACT_REGISTRY_ENABLED == 'true' - uses: google-github-actions/auth@v3 - with: - token_format: "access_token" - workload_identity_provider: ${{ secrets.GOOGLE_WORKLOAD_IDENTITY_PROVIDER }} - service_account: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_EMAIL }} - - - name: Configure Docker for Google Artifact Registry - if: env.GOOGLE_ARTIFACT_REGISTRY_ENABLED == 'true' - run: gcloud auth configure-docker ${{ secrets.GCP_REGION }}-docker.pkg.dev --quiet - - # GHCR image refs must be lowercase; github.repository_owner preserves - # the owner's real case, so it can't be used as-is. This resolves to - # whichever account or org owns the repo the tag was pushed to — a - # fork pushes under the fork owner, no per-person edit needed. - - name: Set image owner - run: echo "OWNER=$(echo '${{ github.repository_owner }}' | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_ENV" - - # Single source of truth for the build-time identity vars (see - # pkg/version) — written to $GITHUB_ENV so the build-push-action step - # below can reference them as build-args the same way a local - # `source install/scripts/version-vars.sh` would. - - name: Resolve version vars - run: | - source install/scripts/version-vars.sh - { - echo "ONIX_VERSION=$ONIX_VERSION" - echo "GIT_COMMIT=$GIT_COMMIT" - echo "GIT_TREE_STATE=$GIT_TREE_STATE" - echo "BUILD_DATE=$BUILD_DATE" - } >> "$GITHUB_ENV" - - # One local, unpushed tag per arch — the manifest job below stitches - # amd64 and arm64 into the single tag users actually pull. - # - # load: true instead of push: true — the image lands in this runner's - # local Docker daemon first so Trivy can scan the exact bytes about to - # ship, before it is tagged for or pushed to any registry. - - name: Build (local, unpushed) - uses: docker/build-push-action@v6 - with: - context: . - file: Dockerfile.adapter-with-plugins - platforms: ${{ matrix.platform }} - build-args: | - ONIX_VERSION=${{ env.ONIX_VERSION }} - GIT_COMMIT=${{ env.GIT_COMMIT }} - GIT_TREE_STATE=${{ env.GIT_TREE_STATE }} - BUILD_DATE=${{ env.BUILD_DATE }} - load: true - tags: ${{ env.IMAGE_NAME }}:${{ github.ref_name }}-${{ matrix.arch }} - - # trivy-release-gate exits 1 on a finding — the release gate, not just - # a report — before any push below runs. One scan covers every registry. - - name: Trivy image scan (gate) - run: make trivy-release-gate IMAGE=${{ env.IMAGE_NAME }}:${{ github.ref_name }}-${{ matrix.arch }} - - # Re-tag the already-scanned local image for each enabled registry and - # push it there. No rebuild, no re-scan. - - name: Tag and push - run: | - LOCAL="${{ env.IMAGE_NAME }}:${{ github.ref_name }}-${{ matrix.arch }}" - TAG="${{ github.ref_name }}-${{ matrix.arch }}" - - if [ "$GHCR_ENABLED" = "true" ]; then - DEST="ghcr.io/${OWNER}/${{ env.IMAGE_NAME }}:${TAG}" - docker tag "$LOCAL" "$DEST" - docker push "$DEST" - fi - if [ "$DOCKERHUB_ENABLED" = "true" ]; then - DEST="${{ secrets.DOCKERHUB_NAMESPACE }}/${{ env.IMAGE_NAME }}:${TAG}" - docker tag "$LOCAL" "$DEST" - docker push "$DEST" - fi - if [ "$AZURE_CONTAINER_REGISTRY_ENABLED" = "true" ]; then - DEST="${{ secrets.AZURE_REGISTRY_NAME }}/${{ env.IMAGE_NAME }}:${TAG}" - docker tag "$LOCAL" "$DEST" - docker push "$DEST" - fi - if [ "$GOOGLE_ARTIFACT_REGISTRY_ENABLED" = "true" ]; then - DEST="${{ secrets.GCP_REGION }}-docker.pkg.dev/${{ secrets.GCP_PROJECT_ID }}/${{ secrets.GOOGLE_ARTIFACT_REPO }}/${{ env.IMAGE_NAME }}:${TAG}" - docker tag "$LOCAL" "$DEST" - docker push "$DEST" - fi - - manifest: - runs-on: ubuntu-latest - needs: build - steps: - - uses: docker/setup-buildx-action@v3 - - - name: Set registry flags - run: | - echo "GHCR_ENABLED=${{ vars.GHCR_ENABLED != 'false' }}" >> "$GITHUB_ENV" - echo "DOCKERHUB_ENABLED=${{ vars.DOCKERHUB_ENABLED == 'true' }}" >> "$GITHUB_ENV" - echo "AZURE_CONTAINER_REGISTRY_ENABLED=${{ vars.AZURE_CONTAINER_REGISTRY_ENABLED == 'true' }}" >> "$GITHUB_ENV" - echo "GOOGLE_ARTIFACT_REGISTRY_ENABLED=${{ vars.GOOGLE_ARTIFACT_REGISTRY_ENABLED == 'true' }}" >> "$GITHUB_ENV" - - - name: Log in to GHCR - if: env.GHCR_ENABLED == 'true' - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GHCR_PAT || secrets.GITHUB_TOKEN }} - - - name: Log in to Docker Hub - if: env.DOCKERHUB_ENABLED == 'true' - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_PASSWORD }} - - - name: Log in to Azure Container Registry - if: env.AZURE_CONTAINER_REGISTRY_ENABLED == 'true' - uses: docker/login-action@v3 - with: - registry: ${{ secrets.AZURE_REGISTRY_NAME }} - username: ${{ secrets.AZURE_REGISTRY_USERNAME }} - password: ${{ secrets.AZURE_REGISTRY_PASSWORD }} - - - name: Authenticate to Google Cloud - if: env.GOOGLE_ARTIFACT_REGISTRY_ENABLED == 'true' - uses: google-github-actions/auth@v3 - with: - token_format: "access_token" - workload_identity_provider: ${{ secrets.GOOGLE_WORKLOAD_IDENTITY_PROVIDER }} - service_account: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_EMAIL }} - - - name: Configure Docker for Google Artifact Registry - if: env.GOOGLE_ARTIFACT_REGISTRY_ENABLED == 'true' - run: gcloud auth configure-docker ${{ secrets.GCP_REGION }}-docker.pkg.dev --quiet - - - name: Set image owner - run: echo "OWNER=$(echo '${{ github.repository_owner }}' | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_ENV" - - # imagetools create makes one manifest list from the two arch-specific - # images the build job just pushed to that registry — this is what - # lets `docker pull` resolve the right arch automatically on either - # amd64 or arm64. One create_manifest call per enabled registry. - - name: Create multi-arch manifests - run: | - create_manifest() { - local BASE="$1" - docker buildx imagetools create \ - -t "${BASE}:${{ github.ref_name }}" \ - -t "${BASE}:latest" \ - "${BASE}:${{ github.ref_name }}-amd64" \ - "${BASE}:${{ github.ref_name }}-arm64" - } - - if [ "$GHCR_ENABLED" = "true" ]; then - create_manifest "ghcr.io/${OWNER}/${{ env.IMAGE_NAME }}" - fi - if [ "$DOCKERHUB_ENABLED" = "true" ]; then - create_manifest "${{ secrets.DOCKERHUB_NAMESPACE }}/${{ env.IMAGE_NAME }}" - fi - if [ "$AZURE_CONTAINER_REGISTRY_ENABLED" = "true" ]; then - create_manifest "${{ secrets.AZURE_REGISTRY_NAME }}/${{ env.IMAGE_NAME }}" - fi - if [ "$GOOGLE_ARTIFACT_REGISTRY_ENABLED" = "true" ]; then - create_manifest "${{ secrets.GCP_REGION }}-docker.pkg.dev/${{ secrets.GCP_PROJECT_ID }}/${{ secrets.GOOGLE_ARTIFACT_REPO }}/${{ env.IMAGE_NAME }}" - fi diff --git a/Deployment/deployment.yaml b/Deployment/deployment.yaml index 443ac03c..561fdb5a 100644 --- a/Deployment/deployment.yaml +++ b/Deployment/deployment.yaml @@ -19,11 +19,6 @@ spec: containers: - name: onix-adapter image: "asia-south1-docker.pkg.dev/trusty-relic-370809/onix-adapter-cicd/beckn-onix:latest" #------ - # $(CONFIG_FILE) is expanded by the kubelet against the env below - # before the container starts -- the image's shell-less DHI base - # has no `sh -c` to do that substitution itself. - args: - - "--config=$(CONFIG_FILE)" ports: - containerPort: 8080 env: diff --git a/Dockerfile.adapter b/Dockerfile.adapter index 0b1fd641..41dd7ca8 100644 --- a/Dockerfile.adapter +++ b/Dockerfile.adapter @@ -1,23 +1,4 @@ -# syntax=docker/dockerfile:1 - -# DHI (Docker Hardened Images) requires `docker login dhi.io` first — a Docker -# subscription credential this build cannot supply. Both stages default to it -# because it's what CI and every shipped image use, but a local build without -# that subscription can override both build-args with public equivalents: -# -# docker build -f Dockerfile.adapter \ -# --build-arg BUILD_IMAGE=golang:1.26.1-bookworm \ -# --build-arg RUNTIME_IMAGE=cgr.dev/chainguard/wolfi-base:latest \ -# -t network-adapter:dev . -# -# Tags verified against github.com/docker-hardened-images/catalog -# image/golang/debian-12/1.26-dev.yaml and image/debian-base/debian-12/12.yaml. -# No cgo here, but the -dev variant's gcc/g++/make come bundled regardless — -# nothing extra to install for either the default or the public override. -ARG BUILD_IMAGE=dhi.io/golang:1.26-debian12-dev -ARG RUNTIME_IMAGE=dhi.io/debian-base:bookworm - -FROM ${BUILD_IMAGE} AS builder +FROM golang:1.26.1-bookworm AS builder WORKDIR /workspace/app COPY cmd/adapter ./cmd/adapter @@ -34,15 +15,16 @@ ARG BUILD_DATE=unknown RUN go build -ldflags "-X github.com/beckn-one/beckn-onix/pkg/version.Version=${ONIX_VERSION} -X github.com/beckn-one/beckn-onix/pkg/version.GitCommit=${GIT_COMMIT} -X github.com/beckn-one/beckn-onix/pkg/version.GitTreeState=${GIT_TREE_STATE} -X github.com/beckn-one/beckn-onix/pkg/version.BuildDate=${BUILD_DATE}" -o server cmd/adapter/main.go -# Debian-based hardened runtime, run-as nonroot by default — kept explicit -# below since RUNTIME_IMAGE can be overridden to wolfi-base, which uses the -# same uid 65532 convention. A shell is required here: CMD below needs -# `sh -c` to expand $CONFIG_FILE. -FROM ${RUNTIME_IMAGE} +# Create a minimal runtime image +FROM cgr.dev/chainguard/wolfi-base:latest +# ✅ Alpine is removed; using minimal Debian WORKDIR /app +# Copy only the built binary and plugin COPY --from=builder /workspace/app/server . +# Expose port 8080 EXPOSE 8080 -USER 65532:65532 -CMD ["sh", "-c", "./server --config=${CONFIG_FILE}"] + +# Run the Go server with the config flag from environment variable. +CMD ["sh", "-c", "./server --config=${CONFIG_FILE}"] \ No newline at end of file diff --git a/Dockerfile.adapter-with-plugins b/Dockerfile.adapter-with-plugins index 6f72a4c0..a7ba5e98 100644 --- a/Dockerfile.adapter-with-plugins +++ b/Dockerfile.adapter-with-plugins @@ -1,31 +1,11 @@ -# syntax=docker/dockerfile:1 - -# DHI (Docker Hardened Images) requires `docker login dhi.io` first — a Docker -# subscription credential this build cannot supply. Both stages default to it -# because it's what CI and every shipped image use, but a local build without -# that subscription can override both build-args with public equivalents: -# -# docker build -f Dockerfile.adapter-with-plugins \ -# --build-arg BUILD_IMAGE=golang:1.26.1-bookworm \ -# --build-arg RUNTIME_IMAGE=cgr.dev/chainguard/wolfi-base:latest \ -# -t network-adapter-plugins:dev . -# -# Tags verified against github.com/docker-hardened-images/catalog -# image/golang/debian-12/1.26-dev.yaml and image/static/debian-12/static-glibc.yaml. -# -buildmode=plugin, and plugin.Open at runtime, both need a C toolchain and -# a real glibc dynamic linker — the -dev variant's gcc/g++/make come -# bundled, same as the public bookworm override. -ARG BUILD_IMAGE=dhi.io/golang:1.26-debian12-dev -ARG RUNTIME_IMAGE=dhi.io/static:20250419-glibc-debian12 - -FROM ${BUILD_IMAGE} AS builder +FROM golang:1.26.1-bookworm AS builder WORKDIR /workspace/app # Specifically copy only the necessary files and directories COPY cmd/adapter/ ./cmd/adapter/ -COPY core/ ./core -COPY pkg/ ./pkg +COPY core/ ./core/ +COPY pkg/ ./pkg/ COPY install/build-plugins.sh ./install/build-plugins.sh COPY install/scripts/version-vars.sh ./install/scripts/version-vars.sh COPY go.mod . @@ -50,12 +30,8 @@ RUN chmod +x install/build-plugins.sh && \ ONIX_VERSION="${ONIX_VERSION}" GIT_COMMIT="${GIT_COMMIT}" GIT_TREE_STATE="${GIT_TREE_STATE}" BUILD_DATE="${BUILD_DATE}" \ ./install/build-plugins.sh -# glibc-only runtime — no shell, no perl/ncurses/util-linux (the packages -# that were carrying unpatched CVEs on the fuller debian-base image), but -# keeps libc6 so plugin.Open's dynamic linking still works. Explicit USER -# below since RUNTIME_IMAGE can be overridden to wolfi-base, which uses the -# same uid 65532 convention. -FROM ${RUNTIME_IMAGE} +# Create minimal runtime image +FROM cgr.dev/chainguard/wolfi-base:latest WORKDIR /app # Copy binary and plugins built with same Go version @@ -63,10 +39,5 @@ COPY --from=builder /workspace/app/server . COPY --from=builder /workspace/app/plugins ./plugins EXPOSE 8081 -USER 65532:65532 -# Exec form, not `sh -c` — this base has no shell, and no Go code reads -# CONFIG_FILE either. The deployment layer resolves it instead: Kubernetes -# expands $(CONFIG_FILE) in args itself before exec'ing the container (see -# Deployment/deployment.yaml) -- no shell involved on either side. A plain -# `docker run` without that substitution needs an explicit --config flag. -ENTRYPOINT ["/app/server"] + +CMD ["sh", "-c", "./server --config=${CONFIG_FILE}"] From 26e013f19b8c0fe05b39819f1b0c10f672c5f58d Mon Sep 17 00:00:00 2001 From: Manjunath Davanam Date: Mon, 7 Sep 2026 22:36:47 +0530 Subject: [PATCH 12/23] fix: bump crypto, grpc and amqp091-go past the HIGH/CRITICAL advisories [#5] The Security workflow this PR adds is only worth adding if it can be green, and on development it is not: trivy fs flags three HIGH/CRITICAL advisories in the dependency graph. golang.org/x/crypto v0.54.0 -> v0.55.0 google.golang.org/grpc v1.82.1 -> v1.83.1 github.com/rabbitmq/amqp091-go v1.11.0 -> v1.13.0 `go mod tidy` carried the transitive minimums along with them (otel 1.43 -> 1.44, x/net 0.56 -> 0.57, x/text 0.40 -> 0.41, genproto). No source change was needed: `go build -trimpath ./cmd/adapter/...` and `go vet ./...` are both clean, and the full suite passes. --- go.mod | 24 ++++++++++++------------ go.sum | 58 ++++++++++++++++++++++++++++++---------------------------- 2 files changed, 42 insertions(+), 40 deletions(-) diff --git a/go.mod b/go.mod index fe8b8670..10145dc5 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.26.1 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= From e99044b944515949838d096a18fe638301befb04 Mon Sep 17 00:00:00 2001 From: Manjunath Davanam Date: Mon, 7 Sep 2026 22:37:15 +0530 Subject: [PATCH 13/23] refactor: move the CI implementation into the Makefile [#5] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review feedback was that the workflows carried the logic and the Makefile carried aliases — 132 lines of inline shell across the three workflows, none of it runnable locally. That is inverted here: the Makefile is the single source of truth and every CI step becomes a one-line `make ` call, so a red check reproduces by running the command the log shows. Fixes, not just relocation: test / cover both failed on development. pkg/plugin and benchmarks/e2e build a real .so and plugin.Open it in the same run, and both -race and whole-module coverage instrumentation make that .so unloadable. They now run as their own invocation. The carve-out is named once (PLUGIN_PKGS/MAIN_PKGS) and shared by test, cover and test-ci, so the three cannot disagree about it. -coverpkg answered in a comment rather than added: benchmarks/e2e is the only test-only package in the module and it is in the carve-out precisely because whole-module instrumentation is what breaks plugin.Open. It cannot be added without reintroducing the failure. cover-diff no longer swallows a broken base. An unresolvable BASE_REF and a failed `git diff` are both hard errors with an ::error:: annotation — an unknown changed-file set is not an empty one. It writes coverage-report.md on every exit path, including those two, so the workflow can read the file unconditionally. BASE_REF defaults to origin/development, the branch PRs actually target. merge-coverage `;` not `&&`, and always removes coverage-plugin.out, so a failed merge cannot leave a half-profile behind to be misread. coverage-plugin.out is gitignored as well. coverage.out a file rule, so cover-diff has a buildable prerequisite: it reuses the profile test-ci already wrote in CI, and runs the suites once on a clean local checkout. trivy-gate a missing or unparsable SARIF report is now a failure. It used to pass vacuously when a scan wrote nothing, which is the one case a security gate must not be green for. docker builds Dockerfile.adapter-with-plugins — the image that ships and that security.yml scans — not Dockerfile.adapter. Version build args come from install/scripts/version-vars.sh so they are spelled out in one place. trivy-deps --skip-dirs bin, so the scanner stops reporting on the trivy binary the cache restored next to it. help one line per target from the `## ` docstrings, instead of a wall of text. tools/trivy-comment.jq is the 16-line jq program that was pasted into four places. As a file it is lintable (`jq -n -f`), diffable, and free of Makefile `$$`/backslash escaping, and one copy means the dependency and image reports cannot drift apart. Removed as dead: trivy-release-gate (its only caller was build-and-push.yml, now deleted) and the `tools` alias (nothing invoked it — every tool is already built on demand as a prerequisite). Verified locally: build, test, cover, cover-diff (pass, no-changed-files, below-threshold and unresolvable-BASE_REF paths), trivy-deps, trivy-image, trivy-comment, all five trivy-gate states, and help. --- .gitignore | 1 + Makefile | 238 ++++++++++++++++++++++++++++------------- tools/trivy-comment.jq | 32 ++++++ 3 files changed, 197 insertions(+), 74 deletions(-) create mode 100644 tools/trivy-comment.jq diff --git a/.gitignore b/.gitignore index 48d66b76..4a9de6ae 100644 --- a/.gitignore +++ b/.gitignore @@ -143,6 +143,7 @@ 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) diff --git a/Makefile b/Makefile index 20e437f3..b28b905d 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,13 @@ -# OAN Network Adapter — build, test and toolchain targets. +# OAN Network Adapter — build, test, lint and security-scan targets. # -# Single source of truth for the run-tests/security/build-and-push workflows, -# which call these targets rather than raw commands. No DB, no sqlc/migrate, -# no separate tools/ module — golangci-lint, gotestsum and trivy install -# straight into bin/ via `go install`/curl. +# Single source of truth for the ci.yml and security.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 the workflows +# is GitHub context (`${{ }}` expressions, $GITHUB_ENV / $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 @@ -12,7 +16,9 @@ IMAGE ?= network-adapter:dev # 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 -BASE_REF ?= origin/main +# 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 ?= HIGH,CRITICAL GOLANGCI_LINT_VERSION := v2.5.0 GOTESTSUM_VERSION := v1.13.0 @@ -22,11 +28,41 @@ GOLANGCI_LINT := $(BIN_DIR)/golangci-lint GOTESTSUM := $(BIN_DIR)/gotestsum TRIVY := $(BIN_DIR)/trivy +# 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 '^## ' $(MAKEFILE_LIST) | sed 's/^## / /' | sort + @grep -hE '^## [a-z]' $(MAKEFILE_LIST) | sed 's/^## / /' | sort ## build: compile the adapter binary # Scoped to cmd/adapter, not ./... — pkg/plugin/implementation/*/cmd holds @@ -35,44 +71,60 @@ help: build: $(GO) build -trimpath -o $(BIN_DIR)/ ./cmd/adapter/... -## test: run the unit and integration suites +## test: run the unit and integration suites (plugin packages without -race) test: - $(GO) test -race ./... + $(GO) test -race $(MAIN_PKGS) + $(GO) test $(PLUGIN_PKGS) -## cover: run the suites and write a coverage profile +## cover: run the suites and write a merged coverage profile to coverage.out cover: - $(GO) test -race -covermode=atomic -coverprofile=coverage.out ./... + $(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: run the suites through gotestsum — one line per package, coverage -## profile written alongside. What run-tests.yml calls; `make test` -## stays the plain everyday entrypoint. -# -# 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. Instrumenting the *whole* module's coverage in one `./...` build -# gives shared packages (e.g. pkg/plugin/definition) a build identity that -# subprocess build doesn't share, so plugin.Open rejects the .so as built -# with a different version of that package. Splitting them into their own -# `go test` invocation keeps their build graph small enough to match. +## 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 \ - $$(go list ./... | grep -vE '/pkg/plugin$$|/benchmarks/e2e$$') - # No -race here: these two packages build a plugin .so in a subprocess - # `go build` with no -race flag of its own, so a race-instrumented test - # binary and a non-race .so mismatch and plugin.Open refuses to load it. + -race -coverprofile=coverage.out -covermode=atomic $(MAIN_PKGS) $(GOTESTSUM) --format pkgname --format-hide-empty-pkg -- \ - -coverprofile=coverage-plugin.out -covermode=atomic \ - ./pkg/plugin ./benchmarks/e2e/... - @tail -n +2 coverage-plugin.out >> coverage.out && rm -f coverage-plugin.out + -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 -## cover-diff: coverage restricted to files changed vs BASE_REF — a PR review -## needs the diff's number, not the whole repo's. On failure, -## names the changed files dragging the number down (worst first). +# 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 := + +## 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 - @CHANGED=$$(git diff --name-only --diff-filter=ACMR "$(BASE_REF)...HEAD" -- '*.go' | grep -v '_test\.go$$' || true); \ + @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 \ - echo "📊 **Test Coverage: ✅ Passed** — not applicable, no changed Go files vs $(BASE_REF)" | tee coverage-report.md; \ + 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); \ @@ -92,59 +144,87 @@ cover-diff: coverage.out print "TOTAL\t" int(C * 100 / T) \ }' - coverage.out); \ if echo "$$RESULT" | grep -q '^EMPTY$$'; then \ - echo "📊 **Test Coverage: ✅ Passed** — not applicable, changed files carry no coverable statements" | tee coverage-report.md; \ + 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}'); \ - 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 "$(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)% |"; \ - } > coverage-report.md; \ - else \ - echo "📊 **Test Coverage: ✅ Passed** — $${PCT}% of changed lines covered, min $(MIN_COVERAGE)%" > coverage-report.md; \ - fi; \ + else \ + echo "📊 **Test Coverage: ✅ Passed** — $${PCT}% of changed lines covered, min $(MIN_COVERAGE)%"; \ + fi; \ + } > coverage-report.md; \ cat coverage-report.md; \ - [ "$$PCT" -ge "$(MIN_COVERAGE)" ] + if [ "$$PCT" -lt "$(MIN_COVERAGE)" ]; then \ + echo "::error::changed-file coverage is $${PCT}%, below the $(MIN_COVERAGE)% minimum"; \ + exit 1; \ + fi -## trivy-deps: dependency graph scan (T4), SARIF report. Catches what the -## image scan structurally cannot — a vulnerable module only the -## test suite imports, so it's never linked into the binary. +## 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 . --severity $(SEVERITY) --exit-code 0 \ + $(TRIVY) fs . --skip-dirs $(BIN_DIR) --severity $(SEVERITY) --exit-code 0 \ --format sarif --output trivy-deps.sarif -TRIVY_IMAGE_SCAN = $(TRIVY) image $(IMAGE) --severity $(SEVERITY) - -## trivy-image: shipped image scan (T4), SARIF report. IMAGE names the ref. +## 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_SCAN) --exit-code 0 --format sarif --output trivy-image.sarif + $(TRIVY) image $(IMAGE) --severity $(SEVERITY) --exit-code 0 \ + --format sarif --output trivy-image.sarif -## trivy-release-gate: same image scan as trivy-image, but exit 1 on a -## finding instead of writing a report — the pre-push -## release gate build-and-push.yml runs once per -## arch-tagged local image, before anything is pushed. -trivy-release-gate: $(TRIVY) - $(TRIVY_IMAGE_SCAN) --exit-code 1 --format table +## trivy-comment: render SARIF as a markdown PR comment — SARIF, TITLE, OUT +# 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. One target serves both scans, so the two reports cannot drift. +trivy-comment: + @test -n "$(SARIF)" -a -n "$(TITLE)" -a -n "$(OUT)" || \ + { echo "::error::trivy-comment needs SARIF, TITLE and OUT"; exit 1; } + @test -s "$(SARIF)" || \ + { echo "::error::$(SARIF) missing or empty — no scan produced it"; exit 1; } + @{ \ + echo ""; \ + echo "### 🛡️ Trivy — $(TITLE) ($(SEVERITY))"; \ + echo "[View full run]($(RUN_URL))"; \ + echo; \ + jq -r -f tools/trivy-comment.jq "$(SARIF)"; \ + } > $(OUT) -## trivy-gate: fail if either SARIF report already produced by a scan step -## carries a finding. Reads the reports rather than rescanning. +## trivy-gate: fail if either SARIF report carries a finding, or is missing +# Reads the reports the scan jobs 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 trivy-deps.sarif trivy-image.sarif; do \ - count=$$(jq '[.runs[].results[]?] | length' "$$report"); \ - echo "$${report}: $${count} $(SEVERITY)"; \ + 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::HIGH or CRITICAL Trivy findings, or a missing report — see the log above"; \ exit $$fail ## lint: vet, format check and static analysis @@ -152,27 +232,37 @@ lint: $(GOLANGCI_LINT) $(GOLANGCI_LINT) run ./... $(GOLANGCI_LINT) fmt --diff ./... -## fmt: apply the formatters lint checks for +## fmt: apply the formatters that lint checks for fmt: $(GOLANGCI_LINT) $(GOLANGCI_LINT) fmt ./... -## docker: build the adapter image +## 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 security.yml scans and what deploys, so a local `make docker && +# make trivy-image` has to scan the same thing CI gates on. The version vars +# come from the script rather than being named again here, so ONIX_VERSION and +# friends are spelled out in exactly one place. docker: - docker build -f Dockerfile.adapter -t $(IMAGE) . - -## tools: build the pinned toolchain into bin/ -tools: $(GOLANGCI_LINT) $(GOTESTSUM) $(TRIVY) + . 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) . ## clean: remove build output and coverage/scan artifacts clean: - rm -rf $(BIN_DIR) coverage.out coverage-report.md trivy-deps.sarif trivy-image.sarif + rm -rf $(BIN_DIR) coverage.out coverage-plugin.out coverage-report.md \ + trivy-deps.sarif trivy-image.sarif \ + trivy-deps-comment.md trivy-image-comment.md $(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 run-tests.yml), so it doesn't belong in the -# adapter's or the linter's dependency graph either one. +# 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) @@ -186,5 +276,5 @@ $(TRIVY): curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | \ sh -s -- -b $(abspath $(BIN_DIR)) $(TRIVY_VERSION) -.PHONY: help build test cover test-ci cover-diff lint fmt trivy-deps \ - trivy-image trivy-release-gate trivy-gate docker tools clean +.PHONY: help build test cover test-ci merge-coverage cover-diff lint fmt \ + trivy-deps trivy-image trivy-comment trivy-gate docker clean diff --git a/tools/trivy-comment.jq b/tools/trivy-comment.jq new file mode 100644 index 00000000..54371ac1 --- /dev/null +++ b/tools/trivy-comment.jq @@ -0,0 +1,32 @@ +# Render a Trivy SARIF report as a markdown table for a PR comment. +# +# Used by the Makefile's trivy-comment target for both the dependency and the +# image scan — one program, not a copy per 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 -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 HIGH or CRITICAL findings." + 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 From 82366234e129a75a2e67de58cb5e811cbb7a355c Mon Sep 17 00:00:00 2001 From: Manjunath Davanam Date: Mon, 7 Sep 2026 22:40:42 +0530 Subject: [PATCH 14/23] refactor: rename Run Tests to CI and reduce it to make calls [#5] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit run-tests.yml becomes ci.yml, replacing the dead ci.yml that triggered on `beck-onix-v1.0-develop` (a typo of a branch that does not exist, so it never fired). Names now read as what they gate: the workflow is `CI` and the jobs are `Lint` and `Test`, so branch protection matches `CI / Lint` and `CI / Test` rather than the old `Run Tests / lint`. The job bodies are four `make` calls plus GitHub plumbing: make lint advisory, continue-on-error. There is no .golangci.yml in this repo yet, so `run ./...` uses tool defaults against a codebase that has never been linted — enough pre-existing findings to block every PR on day one. This surfaces them. (The comment this replaced cited a CLAUDE.md and a pre-commit hook, neither of which exists here.) make build make test-ci make cover-diff push: added `development`, the branch PRs target and merge into, so a merge landing there runs the same gates the PR did. It previously only ran on main and PRs, leaving merges into development ungated. BASE_REF goes through `env:`, not string-interpolated into the run script — `?=` in the Makefile means the environment wins, so nothing untrusted ends up inside a shell command. permissions are least-privilege per job: the workflow grants contents: read and only the Test job adds pull-requests: write for the coverage comment. the coverage comment steps are continue-on-error — a fork PR's default GITHUB_TOKEN is read-only, so create-or-update-comment 403s there. Coverage is still gated by the last step; a comment that cannot post must not fail the job over it. find-comment v3 -> v4 and create-or-update-comment v4 -> v5. The final gate step is `exit 1`, nothing more: cover-diff already emitted the ::error:: annotation and the per-file breakdown. --- .github/workflows/ci.yml | 142 +++++++++++++++++++++++++------- .github/workflows/run-tests.yml | 107 ------------------------ 2 files changed, 114 insertions(+), 135 deletions(-) delete mode 100644 .github/workflows/run-tests.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cef2efdb..9e6a53e7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,36 +1,122 @@ -name: Go CI +name: CI + +# Every step below is either a one-line `make` call or GitHub plumbing +# (`${{ }}` expressions, $GITHUB_STEP_SUMMARY). The build/test/coverage logic +# lives in the Makefile, so a red check here 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 has to run the same gates the PR did. + branches: [main, development] + pull_request: + +# Least privilege at the workflow level; the one job that needs more declares +# it for itself. +permissions: + contents: read + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + jobs: + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + # Advisory, not a gate. There is no .golangci.yml in this repo yet, so + # `run ./...` uses the tool's defaults against a codebase that has never + # been linted — enough pre-existing findings to block every PR on day + # one. This surfaces them; turn continue-on-error off once the baseline + # is clean and the config is pinned. + - run: make lint + continue-on-error: true + test: + name: Test runs-on: ubuntu-latest + # pull-requests: write is for the coverage comment below — only this job + # writes to the PR, so the lint job above stays read-only. + permissions: + contents: read + pull-requests: write steps: - - uses: actions/checkout@v2 - - name: Set up Go - uses: actions/setup-go@v2 + # 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: Publish coverage 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 existing coverage comment + if: github.event_name == 'pull_request' + continue-on-error: true + uses: peter-evans/find-comment@v4 + id: find-coverage-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: Comment coverage on 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-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 diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml deleted file mode 100644 index 47f4136f..00000000 --- a/.github/workflows/run-tests.yml +++ /dev/null @@ -1,107 +0,0 @@ -name: Run Tests - -on: - push: - branches: [main] - pull_request: - -permissions: - contents: read - pull-requests: write - -concurrency: - group: test-${{ github.ref }} - cancel-in-progress: true - -jobs: - lint: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 - with: - go-version-file: go.mod - - # continue-on-error rather than dropping the job: CLAUDE.md keeps CI - # lint as the safety net behind the pre-commit hook, which is bypassable - # (--no-verify). Advisory here, not a gate — a red lint check must never - # be the reason a PR can't merge, only a reason someone looks. - - run: make lint - continue-on-error: true - - test: - runs-on: ubuntu-latest - steps: - # fetch-depth: 0 — cover-diff below needs the base branch's history - # locally to compute the PR's changed files; 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 - - # continue-on-error so a coverage miss still posts its report; the last - # step in the job turns that outcome back into a failure. Always - # diff-scoped, never whole-repo — main only ever moves by PR merge - # (CLAUDE.md: never commit directly to main), so a PR's own diff is - # already the gate that matters. On a pull_request event that's the PR - # branch vs its target; on a push to main (the merge landing) it's - # github.event.before, the commit main pointed to right before this one. - - name: Check coverage - id: coverage - continue-on-error: true - run: | - if [ "${{ github.event_name }}" = "pull_request" ]; then - make cover-diff BASE_REF="origin/${{ github.event.pull_request.base.ref }}" - else - make cover-diff BASE_REF="${{ github.event.before }}" - fi - - # Marker goes into the file itself, not just the step summary — it's - # what body-path feeds the PR comment below, and find-comment matches - # on this exact string to update rather than duplicate that comment. - - name: Publish coverage summary - run: | - printf '\n%s\n' "$(cat coverage-report.md)" > coverage-report.md - cat coverage-report.md >> "$GITHUB_STEP_SUMMARY" - - # Always posted, pass or 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 just never trigger? Always-visible with an - # explicit ✅/❌ (cover-diff's own output) answers that without a - # reviewer having to open the job. Updated in place across runs, not - # reposted, via find-comment below. - # - # 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. A comment that can't post must never - # fail the job — coverage already gated above, this step is cosmetic. - - name: Find existing coverage comment - if: github.event_name == 'pull_request' - continue-on-error: true - uses: peter-evans/find-comment@v3 - id: find-coverage-comment - with: - issue-number: ${{ github.event.pull_request.number }} - comment-author: "github-actions[bot]" - body-includes: "" - - - name: Comment coverage on PR - if: github.event_name == 'pull_request' - continue-on-error: true - uses: peter-evans/create-or-update-comment@v4 - 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 - - - name: Gate on minimum coverage - if: steps.coverage.outcome == 'failure' - run: | - echo "::error::coverage is below the minimum — see the summary above" - exit 1 From 4f7961419cf8d075e0de3c833fe22062e95bdc0c Mon Sep 17 00:00:00 2001 From: Manjunath Davanam Date: Mon, 7 Sep 2026 22:42:15 +0530 Subject: [PATCH 15/23] refactor: split Security into dependency-scan, image-scan and security-gate [#5] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The single `security` job scanned dependencies, then built the image, then scanned that, then gated — serially, in one job, so a dependency finding waited on an image build that has nothing to do with it, and the PR could not say which scan found what without opening a log. Three jobs now, named for what they do: `Dependency scan` and `Image scan` run in parallel (the dependency scan needs nothing but a checkout), and `Security gate` fans in with needs + if: always(). Branch protection matches `Security / Security gate`. Every step in the file is a one-line `make` call or GitHub plumbing — zero lines of inline shell, down from 132 across the three original workflows: make trivy-deps make trivy-comment SARIF=... TITLE=... OUT=... make docker IMAGE=network-adapter: make trivy-image IMAGE=network-adapter: make trivy-gate Also fixed: the gate now reads the SARIF the scan jobs produced, handed over as artifacts, rather than scanning a third and fourth time. if-no-files-found is deliberately `ignore`: a scan that produced nothing surfaces as the gate reporting MISSING — which now fails — with both verdicts visible in one place, rather than as a second red check on the scan job. push: added `development`, so a merge landing there runs the same scans the PR did. permissions are least-privilege per job: contents: read at the workflow level, with pull-requests: write and security-events: write only on the two scan jobs. The gate stays read-only. both upload-sarif steps are continue-on-error — a fork PR's GITHUB_TOKEN has no security-events scope, so they 403 there. The gate decides pass/fail; an upload that cannot happen must not fail the scan over it. a `category` per scan, so the image upload no longer replaces the dependency analysis (GitHub keys an analysis on ref + category). find-comment v3 -> v4, create-or-update-comment v4 -> v5. .github/actions/trivy-cache is the `Trivy cache date` + actions/cache pair, extracted now that two jobs need it — both jobs stay a one-line `uses:`. It keeps the trivy binary (~30MB) and the vulnerability DB (~100MB+) out of every run, which the trivy-action it replaced did for us. --- .github/actions/trivy-cache/action.yml | 26 +++ .github/workflows/security.yml | 278 +++++++++++-------------- 2 files changed, 152 insertions(+), 152 deletions(-) create mode 100644 .github/actions/trivy-cache/action.yml 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/security.yml b/.github/workflows/security.yml index cfc42c5c..bfe1e174 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -1,215 +1,189 @@ name: Security +# Three jobs, so the PR says *which* scan found something without anyone +# opening a log: the dependency scan needs nothing but a checkout, so it runs +# alongside the image build instead of queueing behind it, and the gate reads +# both reports once they land. +# +# Every step is a one-line `make` call or GitHub plumbing — the scan, report +# and gate logic lives in the Makefile, so `make trivy-deps` / `make docker && +# make trivy-image` / `make trivy-gate` reproduce any red check locally. on: push: - branches: [main] + # development as well as main: this is the branch PRs target and merge + # into, so a merge landing there has to run the same scans the PR did. + branches: [main, development] pull_request: +# Least privilege at the workflow level; the scan jobs declare the extra +# scopes they need, so the gate job stays read-only. permissions: contents: read - # Findings are uploaded as SARIF for GitHub's own code scanning UI (Security - # tab, diff annotations) AND posted as a PR comment — the SARIF path alone - # took three clicks (check -> run -> results) to see a finding that used to - # be one scroll away in the PR conversation. pull-requests: write is for - # the comment; security-events: write is for the SARIF upload. - pull-requests: write - security-events: write concurrency: group: security-${{ github.ref }} cancel-in-progress: true jobs: - security: + dependency-scan: + name: Dependency scan runs-on: ubuntu-latest + # Findings go to GitHub's code scanning UI as SARIF (Security tab, diff + # annotations) AND to a PR comment — the SARIF path alone took three clicks + # (check -> run -> results) to see a finding that is one scroll away in the + # PR conversation. pull-requests: write is the comment, security-events: + # write is the SARIF upload. + permissions: + contents: read + pull-requests: write + security-events: write steps: - uses: actions/checkout@v4 + - uses: ./.github/actions/trivy-cache - # The trivy-action this replaced cached both the binary and its - # vulnerability DB across runs; `make trivy-deps`/`trivy-image` alone - # would re-download both (binary ~30MB, DB ~100MB+) on every run. - # 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 doesn't - # go stale for longer than a day. - - name: Trivy cache date - id: trivy-date - run: echo "date=$(date -u +%F)" >> "$GITHUB_OUTPUT" - - - name: Cache Trivy binary and vulnerability DB - uses: actions/cache@v4 - with: - path: | - bin/trivy - ~/.cache/trivy - key: trivy-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('Makefile') }}-${{ steps.trivy-date.outputs.date }} - restore-keys: | - trivy-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('Makefile') }}- - - # T4, half one: the dependency graph as written. This catches what the - # image scan structurally cannot — a vulnerable module that only the test - # suite imports, so it is never linked into the binary and never appears - # in a layer. + # 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 + + # continue-on-error: a fork PR's GITHUB_TOKEN has no security-events + # write, so this 403s there. The gate job is what decides pass/fail; an + # upload that can't happen must not fail the scan over it. # - # Logic lives in the Makefile's trivy-deps target — same command a local - # `make trivy-deps` runs. - - name: Trivy dependency scan (report) - run: make trivy-deps - - # A category per scan: without it the second upload replaces the first, - # because GitHub keys an analysis on (ref, category). - - name: Upload dependency findings + # A category per scan — GitHub keys an analysis on (ref, category), so + # without it the image upload would replace this one. + - name: Upload findings to code scanning + continue-on-error: true uses: github/codeql-action/upload-sarif@v3 with: sarif_file: trivy-deps.sarif category: trivy-dependencies - # Posted right after the report, on every run — pass or fail, and - # before the image is even built — so a PR shows the dependency finding - # immediately rather than after the whole job finishes. Reads the same - # SARIF just uploaded above; no second scan. - # - # Trivy's SARIF has no structured per-field severity/version columns; - # each finding's detail lives as prose in message.text ("Package: - # ...\nSeverity: ...\n..."), so the table columns below are pulled out - # of that text rather than from dedicated JSON fields. - # continue-on-error: a report-formatting step must never be able to - # fail the job — the gate at the bottom is what decides that. - - name: Assemble Trivy dependency PR comment + # Rendered from the SARIF just uploaded, no second scan. Posted before + # the image scan has even finished, so a PR shows a dependency finding + # immediately rather than after the whole workflow completes. + - name: Assemble the PR comment if: github.event_name == 'pull_request' continue-on-error: true - run: | - { - echo "" - echo "### 🛡️ Trivy — Go dependency scan (HIGH,CRITICAL)" - echo "[View full run](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID})" - echo - jq -r ' - def val(re; default): (capture(re) // {v: default}).v; - [ .runs[].results[]? ] as $found - | if ($found | length) == 0 then - "No HIGH or CRITICAL findings." - 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 - ' trivy-deps.sarif - } > trivy-deps-comment.md - - - name: Find existing Trivy dependency comment + run: make trivy-comment SARIF=trivy-deps.sarif TITLE="Go dependency scan" OUT=trivy-deps-comment.md + + - name: Find the existing PR comment if: github.event_name == 'pull_request' continue-on-error: true - uses: peter-evans/find-comment@v3 - id: find-trivy-deps-comment + uses: peter-evans/find-comment@v4 + id: find-comment with: issue-number: ${{ github.event.pull_request.number }} comment-author: "github-actions[bot]" body-includes: "" # continue-on-error: a fork PR's default GITHUB_TOKEN is read-only, so - # this 403s there even with pull-requests: write declared. The gate at - # the bottom of this job is what actually decides pass/fail; a comment - # that can't post must never fail the job over that. - - name: Comment Trivy dependency report on PR + # this 403s there even with pull-requests: write declared. + - name: Comment the report on the PR if: github.event_name == 'pull_request' continue-on-error: true - uses: peter-evans/create-or-update-comment@v4 + uses: peter-evans/create-or-update-comment@v5 with: - comment-id: ${{ steps.find-trivy-deps-comment.outputs.comment-id }} + comment-id: ${{ steps.find-comment.outputs.comment-id }} issue-number: ${{ github.event.pull_request.number }} body-path: trivy-deps-comment.md edit-mode: replace - - name: Log in to dhi.io - uses: ./.github/actions/dhi-login + # Handed to the gate job, which runs on a different runner. if-no-files-found + # is deliberately `ignore` and not `error`: a scan that produced nothing + # should surface as the gate reporting a MISSING report, with both scans' + # verdicts visible, rather than as a second red check here. + - name: Hand the report to the gate + if: always() + uses: actions/upload-artifact@v4 with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - token: ${{ secrets.DOCKERHUB_TOKEN }} - - # Both Dockerfiles default to dhi.io/* base images, and Trivy reads the - # base layers plus the Go build info embedded in the binary — including - # `stdlib`, so a Go toolchain CVE is reported here and nowhere else the - # dependency scan above cannot see. - - name: Build image - run: | - source 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 network-adapter:${{ github.sha }} . - # Plain `docker build`, not build-push-action, matches security.yml's - # single-arch report scan — build-and-push.yml is where per-arch, - # buildx-driven builds actually matter. - - - name: Trivy image scan (report) - run: make trivy-image IMAGE=network-adapter:${{ github.sha }} - - - name: Upload image findings + name: trivy-deps-sarif + path: trivy-deps.sarif + if-no-files-found: ignore + + image-scan: + name: Image scan + runs-on: ubuntu-latest + # Findings go to GitHub's code scanning UI as SARIF (Security tab, diff + # annotations) AND to a PR comment — the SARIF path alone took three clicks + # (check -> run -> results) to see a finding that is one scroll away in the + # PR conversation. pull-requests: write is the comment, security-events: + # write is the SARIF upload. + permissions: + contents: read + pull-requests: write + security-events: write + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/trivy-cache + + # The image the adapter actually ships — `make docker` builds + # Dockerfile.adapter-with-plugins with the same version build args, so a + # local `make docker && make trivy-image` scans what this gates on. + # + # Trivy 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 the dependency scan can see. + - run: make docker IMAGE=network-adapter:${{ github.sha }} + - run: make trivy-image IMAGE=network-adapter:${{ github.sha }} + + - name: Upload findings to code scanning + continue-on-error: true uses: github/codeql-action/upload-sarif@v3 with: sarif_file: trivy-image.sarif category: trivy-image - - name: Assemble Trivy image PR comment + - name: Assemble the PR comment if: github.event_name == 'pull_request' continue-on-error: true - run: | - { - echo "" - echo "### 🛡️ Trivy — image scan (HIGH,CRITICAL)" - echo "[View full run](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID})" - echo - jq -r ' - def val(re; default): (capture(re) // {v: default}).v; - [ .runs[].results[]? ] as $found - | if ($found | length) == 0 then - "No HIGH or CRITICAL findings." - 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 - ' trivy-image.sarif - } > trivy-image-comment.md - - - name: Find existing Trivy image comment + run: make trivy-comment SARIF=trivy-image.sarif TITLE="Image scan" OUT=trivy-image-comment.md + + - name: Find the existing PR comment if: github.event_name == 'pull_request' continue-on-error: true - uses: peter-evans/find-comment@v3 - id: find-trivy-image-comment + uses: peter-evans/find-comment@v4 + id: find-comment with: issue-number: ${{ github.event.pull_request.number }} comment-author: "github-actions[bot]" body-includes: "" - - name: Comment Trivy image report on PR + - name: Comment the report on the PR if: github.event_name == 'pull_request' continue-on-error: true - uses: peter-evans/create-or-update-comment@v4 + uses: peter-evans/create-or-update-comment@v5 with: - comment-id: ${{ steps.find-trivy-image-comment.outputs.comment-id }} + comment-id: ${{ steps.find-comment.outputs.comment-id }} issue-number: ${{ github.event.pull_request.number }} body-path: trivy-image-comment.md edit-mode: replace - # Gate on the reports already produced rather than scanning a third and - # fourth time — logic lives in the Makefile's trivy-gate target so a - # local `make trivy-gate` checks the exact same two files this job - # just wrote. Last in the job, so a red build still leaves both reports - # behind. - - name: Gate on HIGH or CRITICAL findings - run: make trivy-gate || { echo "::error::HIGH or CRITICAL Trivy findings — see job log above"; exit 1; } + - name: Hand the report to the gate + if: always() + uses: actions/upload-artifact@v4 + with: + name: trivy-image-sarif + path: trivy-image.sarif + if-no-files-found: ignore + + security-gate: + name: Security gate + needs: [dependency-scan, image-scan] + # always(): the gate has to run even when a scan job failed, so a failed + # scan is reported as a missing report rather than skipping the gate + # entirely. make trivy-gate fails on a missing or unparsable report for + # exactly this reason. + if: always() + runs-on: ubuntu-latest + steps: + # Checkout is for the Makefile, not the source — this job only reads the + # two SARIF reports the scan jobs produced. + - uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + pattern: trivy-*-sarif + merge-multiple: true + + - run: make trivy-gate From 6e684133132bb30634814d969c2fa562e6c8a3ae Mon Sep 17 00:00:00 2001 From: Manjunath Davanam Date: Mon, 7 Sep 2026 22:43:30 +0530 Subject: [PATCH 16/23] ci: cancel a PR's superseded runs, keep the verdict on trunk commits [#5] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both workflows already grouped on github.ref with cancel-in-progress, so a new commit on a PR did supersede the previous one's runs — but the intent was undocumented and the flag was a bare `true`, which also cancelled runs on main and development. cancel-in-progress is now scoped to pull_request. On a PR, pushing a new commit cancels the CI and Security runs still in flight for the commit before it: github.ref is refs/pull//merge for the whole life of the PR, so every push lands in the same group. On main and development nothing is cancelled — each commit there keeps its own recorded verdict, instead of being left with a grey check and no way to tell afterwards whether it was ever green. The two workflows keep separate groups, so cancelling a superseded CI run does not take that commit's Security run down with it. --- .github/workflows/ci.yml | 10 +++++++++- .github/workflows/security.yml | 7 ++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9e6a53e7..868d6e84 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,9 +16,17 @@ on: 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; a push to main or development gets +# its own group and never collides with a PR's. concurrency: group: ci-${{ github.ref }} - cancel-in-progress: true + # PRs only. On main and development every commit keeps its own recorded + # verdict — cancelling there would leave trunk commits with a grey check and + # no way to tell afterwards whether that commit was ever green. + cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: lint: diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index bfe1e174..d4b3ea64 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -20,9 +20,14 @@ on: permissions: contents: read +# Same rule as ci.yml: a new commit on a PR supersedes the scans of the one +# before it. Its own group, not shared with ci.yml, so cancelling a superseded +# CI run does not take the Security run for that commit down with it. concurrency: group: security-${{ github.ref }} - cancel-in-progress: true + # PRs only — see ci.yml. A scan verdict on a trunk commit is a record worth + # keeping. + cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: dependency-scan: From c62e192affc4e35f201e35c89d13fa3da57a2f98 Mon Sep 17 00:00:00 2001 From: Manjunath Davanam Date: Mon, 7 Sep 2026 22:51:49 +0530 Subject: [PATCH 17/23] build: add lint-actions, lint-staged and a versioned pre-commit hook [#5] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nothing in this repo validated the workflow files. A bad ${{ }} expression, a mistyped `uses:` ref or a `needs:` pointing at a job that does not exist was only discoverable by running the workflow — or by running actionlint by hand. Deliberately not a CI step. The gate is a pre-commit hook, so the feedback lands before the commit exists rather than after a push: lint-actions actionlint (pinned v1.7.12, go-installable like the other tools) over the whole .github tree. Whole-repo rather than per-file because actionlint resolves `needs:` across a workflow's jobs and checks `uses: ./.github/actions/...` against the action on disk. lint-staged what the hook runs. Fires actionlint only when a workflow or composite action is staged, and gofmt only over the staged .go files. hooks sets core.hooksPath to .githooks. Run once per clone. .githooks/pre-commit is a one-line `exec make lint-staged`: the logic stays in the Makefile, so `make lint-staged` reproduces exactly what blocked a commit, and there is a single place to change what runs. core.hooksPath rather than copying into .git/hooks means the hook is versioned and reviewable, and a change to it reaches everyone on their next pull. Scope is what can pass today, so the hook never becomes something people reflexively --no-verify past: - actionlint reports nothing on the current tree, so it blocks from day one. - 22 files in the repo are not gofmt-clean. Staged-only means that history stays someone else's problem until you touch one of those files. - `golangci-lint run` is excluded: with no .golangci.yml it uses tool defaults against a codebase that has never been linted, so it would reject every commit. - the test suite is excluded: a pre-commit hook has to stay in seconds, and `make test-ci` in CI is where that belongs. Verified all six paths: nothing staged, staged Go/workflow with a clean tree, actionlint catching a bad steps. reference, gofmt rejecting and then accepting a staged file, a real `git commit` blocked by the installed hook, and --no-verify still getting through. --- .githooks/pre-commit | 13 ++++++++ Makefile | 75 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100755 .githooks/pre-commit 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/Makefile b/Makefile index b28b905d..ec5405ba 100644 --- a/Makefile +++ b/Makefile @@ -23,10 +23,17 @@ SEVERITY ?= HIGH,CRITICAL 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 @@ -236,6 +243,68 @@ lint: $(GOLANGCI_LINT) 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 security.yml scans and what deploys, so a local `make docker && @@ -267,6 +336,11 @@ $(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 @@ -277,4 +351,5 @@ $(TRIVY): 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-comment trivy-gate docker clean From 00140c9bbd7e7418e11d0268cce7d8730fe1af1a Mon Sep 17 00:00:00 2001 From: Manjunath Davanam Date: Mon, 7 Sep 2026 23:11:31 +0530 Subject: [PATCH 18/23] chore: cut CI down to four checks and publish the image on a tag [#5] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two checks on a PR, two on a tag, one workflow. CI / End to End verification build, test, changed-line coverage, result posted to the PR and the job summary CI / Security Scan trivy on the dependency graph and on the shipped image, one comment, one gate CI / Build Artifact (amd64) push the image to ghcr.io by digest, CI / Build Artifact (arm64) natively on a runner of that arch CI / Publish Artifact bind the version tag (and :latest for a plain vX.Y.Z) to both digests Dropped the Lint check. It was continue-on-error, so it gated nothing: there is no .golangci.yml yet and `golangci-lint run` against a never-linted codebase reports enough to block every PR, which is why it was advisory in the first place. `make lint` still exists for local use, and the .githooks pre-commit hook is what actually enforces gofmt on every commit. Folded security.yml into ci.yml. Dependency scan, image scan and the gate were three jobs and three checks to answer one question; they are one job now, which also removes the SARIF artifact handoff between runners and turns the two per-scan PR comments into one. trivy-comment's SARIF/TITLE/OUT parameters existed only to serve two jobs and are gone with them — trivy-report renders both reports from the SARIF_REPORTS list that trivy-gate reads, so the comment and the gate cannot disagree. Tag triggers accept 1.0.0 and 1.0.0-RC1 as well as the v-prefixed spelling every existing tag in this repo uses. Each arch builds on a runner of that arch rather than under QEMU, because the image compiles every plugin with `go build -buildmode=plugin` and a plugin .so has to match the adapter binary's GOARCH. Build Artifact pushes by digest with no tag, so only Publish Artifact creates something visible, and a tag can never resolve to one architecture. Job conditions test startsWith(github.ref, 'refs/tags/') rather than github.ref_type: on a pull_request the ref is refs/pull//merge and ref_type is not documented to be 'branch' there, so a ref_type test risked skipping both PR checks and reporting green for a run that did nothing. --- .github/workflows/ci.yml | 243 ++++++++++++++++++++++++++++----- .github/workflows/security.yml | 194 -------------------------- Makefile | 140 +++++++++++++++---- 3 files changed, 322 insertions(+), 255 deletions(-) delete mode 100644 .github/workflows/security.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 868d6e84..5b0cd5e7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,56 +1,60 @@ name: CI +# Four checks and no more. On a PR or a trunk push: "End to End verification" +# and "Security Scan". On a version tag: "Build Artifact" and +# "Publish Artifact". Nothing runs on both event kinds, so a PR never shows a +# publish check and a tag never re-runs the tests the PR already gated on. +# # Every step below is either a one-line `make` call or GitHub plumbing -# (`${{ }}` expressions, $GITHUB_STEP_SUMMARY). The build/test/coverage logic -# lives in the Makefile, so a red check here reproduces locally by running the -# exact command the log shows. +# (`${{ }}` 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: push: # development as well as main: this is the branch PRs target and merge - # into, so a merge landing there has to run the same gates the PR did. + # into, so a merge landing there runs the same gates the PR did. branches: [main, development] + # 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 too 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]+-*" pull_request: -# Least privilege at the workflow level; the one job that needs more declares -# it for itself. +# Least privilege at the workflow level; the three 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; a push to main or development gets -# its own group and never collides with a PR's. +# 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 trunk commits with a grey check and - # no way to tell afterwards whether that commit was ever green. + # PRs only. On main, development and tags 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, and would abort a + # half-finished publish. cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: - lint: - name: Lint + verify: + name: End to End verification + # Everything except a tag push, i.e. PRs and trunk pushes. Tested on + # github.ref rather than github.ref_type: on a pull_request the ref is + # refs/pull//merge, which is neither refs/heads/ nor refs/tags/, and + # ref_type is not documented to be 'branch' there. Guessing wrong would + # skip this job on every PR and show a green check for a run that did + # nothing, so the condition names the one case it means to exclude. + if: ${{ !startsWith(github.ref, 'refs/tags/') }} runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 - with: - go-version-file: go.mod - - # Advisory, not a gate. There is no .golangci.yml in this repo yet, so - # `run ./...` uses the tool's defaults against a codebase that has never - # been linted — enough pre-existing findings to block every PR on day - # one. This surfaces them; turn continue-on-error off once the baseline - # is clean and the config is pinned. - - run: make lint - continue-on-error: true - - test: - name: Test - runs-on: ubuntu-latest - # pull-requests: write is for the coverage comment below — only this job - # writes to the PR, so the lint job above stays read-only. + # pull-requests: write is for the coverage comment below. permissions: contents: read pull-requests: write @@ -90,7 +94,7 @@ jobs: # cover-diff writes coverage-report.md on every exit path, including its # own error paths, so this needs no existence guard. - - name: Publish coverage to the job summary + - 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 @@ -102,7 +106,7 @@ jobs: # 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 existing coverage comment + - name: Find the existing result comment if: github.event_name == 'pull_request' continue-on-error: true uses: peter-evans/find-comment@v4 @@ -112,7 +116,7 @@ jobs: comment-author: "github-actions[bot]" body-includes: "" - - name: Comment coverage on the PR + - 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 @@ -128,3 +132,172 @@ jobs: - name: Gate on minimum coverage if: steps.coverage.outcome == 'failure' run: exit 1 + + security-scan: + name: Security Scan + # Same condition as End to End verification above — see the note there. + if: ${{ !startsWith(github.ref, 'refs/tags/') }} + 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, security-events: write is the + # SARIF upload to the Security tab. + permissions: + contents: read + pull-requests: write + security-events: write + steps: + - 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 }} + + # continue-on-error: a fork PR's GITHUB_TOKEN has no security-events + # write, so these 403 there. make trivy-gate below is what decides + # pass/fail; an upload that can't happen must not fail the job over it. + # + # A category per scan — GitHub keys an analysis on (ref, category), so + # without them the second upload would replace the first. + - name: Upload dependency findings to code scanning + continue-on-error: true + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: trivy-deps.sarif + category: trivy-dependencies + + - name: Upload image findings to code scanning + continue-on-error: true + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: trivy-image.sarif + category: trivy-image + + # 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: + 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: + 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 a HIGH/CRITICAL + # 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 + + 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 }}) + if: startsWith(github.ref, 'refs/tags/') + 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 + if: startsWith(github.ref, 'refs/tags/') + # 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/security.yml b/.github/workflows/security.yml deleted file mode 100644 index d4b3ea64..00000000 --- a/.github/workflows/security.yml +++ /dev/null @@ -1,194 +0,0 @@ -name: Security - -# Three jobs, so the PR says *which* scan found something without anyone -# opening a log: the dependency scan needs nothing but a checkout, so it runs -# alongside the image build instead of queueing behind it, and the gate reads -# both reports once they land. -# -# Every step is a one-line `make` call or GitHub plumbing — the scan, report -# and gate logic lives in the Makefile, so `make trivy-deps` / `make docker && -# make trivy-image` / `make trivy-gate` reproduce any red check locally. -on: - push: - # development as well as main: this is the branch PRs target and merge - # into, so a merge landing there has to run the same scans the PR did. - branches: [main, development] - pull_request: - -# Least privilege at the workflow level; the scan jobs declare the extra -# scopes they need, so the gate job stays read-only. -permissions: - contents: read - -# Same rule as ci.yml: a new commit on a PR supersedes the scans of the one -# before it. Its own group, not shared with ci.yml, so cancelling a superseded -# CI run does not take the Security run for that commit down with it. -concurrency: - group: security-${{ github.ref }} - # PRs only — see ci.yml. A scan verdict on a trunk commit is a record worth - # keeping. - cancel-in-progress: ${{ github.event_name == 'pull_request' }} - -jobs: - dependency-scan: - name: Dependency scan - runs-on: ubuntu-latest - # Findings go to GitHub's code scanning UI as SARIF (Security tab, diff - # annotations) AND to a PR comment — the SARIF path alone took three clicks - # (check -> run -> results) to see a finding that is one scroll away in the - # PR conversation. pull-requests: write is the comment, security-events: - # write is the SARIF upload. - permissions: - contents: read - pull-requests: write - security-events: write - steps: - - 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 - - # continue-on-error: a fork PR's GITHUB_TOKEN has no security-events - # write, so this 403s there. The gate job is what decides pass/fail; an - # upload that can't happen must not fail the scan over it. - # - # A category per scan — GitHub keys an analysis on (ref, category), so - # without it the image upload would replace this one. - - name: Upload findings to code scanning - continue-on-error: true - uses: github/codeql-action/upload-sarif@v3 - with: - sarif_file: trivy-deps.sarif - category: trivy-dependencies - - # Rendered from the SARIF just uploaded, no second scan. Posted before - # the image scan has even finished, so a PR shows a dependency finding - # immediately rather than after the whole workflow completes. - - name: Assemble the PR comment - if: github.event_name == 'pull_request' - continue-on-error: true - run: make trivy-comment SARIF=trivy-deps.sarif TITLE="Go dependency scan" OUT=trivy-deps-comment.md - - - name: Find the existing PR comment - if: github.event_name == 'pull_request' - continue-on-error: true - uses: peter-evans/find-comment@v4 - id: find-comment - with: - issue-number: ${{ github.event.pull_request.number }} - comment-author: "github-actions[bot]" - body-includes: "" - - # continue-on-error: a fork PR's default GITHUB_TOKEN is read-only, so - # this 403s there even with pull-requests: write declared. - - name: Comment the report on 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-comment.outputs.comment-id }} - issue-number: ${{ github.event.pull_request.number }} - body-path: trivy-deps-comment.md - edit-mode: replace - - # Handed to the gate job, which runs on a different runner. if-no-files-found - # is deliberately `ignore` and not `error`: a scan that produced nothing - # should surface as the gate reporting a MISSING report, with both scans' - # verdicts visible, rather than as a second red check here. - - name: Hand the report to the gate - if: always() - uses: actions/upload-artifact@v4 - with: - name: trivy-deps-sarif - path: trivy-deps.sarif - if-no-files-found: ignore - - image-scan: - name: Image scan - runs-on: ubuntu-latest - # Findings go to GitHub's code scanning UI as SARIF (Security tab, diff - # annotations) AND to a PR comment — the SARIF path alone took three clicks - # (check -> run -> results) to see a finding that is one scroll away in the - # PR conversation. pull-requests: write is the comment, security-events: - # write is the SARIF upload. - permissions: - contents: read - pull-requests: write - security-events: write - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/trivy-cache - - # The image the adapter actually ships — `make docker` builds - # Dockerfile.adapter-with-plugins with the same version build args, so a - # local `make docker && make trivy-image` scans what this gates on. - # - # Trivy 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 the dependency scan can see. - - run: make docker IMAGE=network-adapter:${{ github.sha }} - - run: make trivy-image IMAGE=network-adapter:${{ github.sha }} - - - name: Upload findings to code scanning - continue-on-error: true - uses: github/codeql-action/upload-sarif@v3 - with: - sarif_file: trivy-image.sarif - category: trivy-image - - - name: Assemble the PR comment - if: github.event_name == 'pull_request' - continue-on-error: true - run: make trivy-comment SARIF=trivy-image.sarif TITLE="Image scan" OUT=trivy-image-comment.md - - - name: Find the existing PR comment - if: github.event_name == 'pull_request' - continue-on-error: true - uses: peter-evans/find-comment@v4 - id: find-comment - with: - issue-number: ${{ github.event.pull_request.number }} - comment-author: "github-actions[bot]" - body-includes: "" - - - name: Comment the report on 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-comment.outputs.comment-id }} - issue-number: ${{ github.event.pull_request.number }} - body-path: trivy-image-comment.md - edit-mode: replace - - - name: Hand the report to the gate - if: always() - uses: actions/upload-artifact@v4 - with: - name: trivy-image-sarif - path: trivy-image.sarif - if-no-files-found: ignore - - security-gate: - name: Security gate - needs: [dependency-scan, image-scan] - # always(): the gate has to run even when a scan job failed, so a failed - # scan is reported as a missing report rather than skipping the gate - # entirely. make trivy-gate fails on a missing or unparsable report for - # exactly this reason. - if: always() - runs-on: ubuntu-latest - steps: - # Checkout is for the Makefile, not the source — this job only reads the - # two SARIF reports the scan jobs produced. - - uses: actions/checkout@v4 - - - uses: actions/download-artifact@v4 - with: - pattern: trivy-*-sarif - merge-multiple: true - - - run: make trivy-gate diff --git a/Makefile b/Makefile index ec5405ba..1f102355 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,10 @@ # OAN Network Adapter — build, test, lint and security-scan targets. # -# Single source of truth for the ci.yml and security.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 the workflows -# is GitHub context (`${{ }}` expressions, $GITHUB_ENV / $GITHUB_STEP_SUMMARY -# writes) that has no meaning outside a runner. +# Single source of truth for the ci.yml workflow: 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 the 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. @@ -13,6 +13,20 @@ 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 @@ -112,6 +126,11 @@ coverage.out: # 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 @@ -192,30 +211,42 @@ trivy-image: $(TRIVY) $(TRIVY) image $(IMAGE) --severity $(SEVERITY) --exit-code 0 \ --format sarif --output trivy-image.sarif -## trivy-comment: render SARIF as a markdown PR comment — SARIF, TITLE, OUT +## 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. One target serves both scans, so the two reports cannot drift. -trivy-comment: - @test -n "$(SARIF)" -a -n "$(TITLE)" -a -n "$(OUT)" || \ - { echo "::error::trivy-comment needs SARIF, TITLE and OUT"; exit 1; } - @test -s "$(SARIF)" || \ - { echo "::error::$(SARIF) missing or empty — no scan produced it"; exit 1; } +# escaping. +trivy-report: @{ \ - echo ""; \ - echo "### 🛡️ Trivy — $(TITLE) ($(SEVERITY))"; \ + echo "$(SEC_MARKER)"; \ + echo "## 🛡️ Trivy security scan ($(SEVERITY))"; \ echo "[View full run]($(RUN_URL))"; \ - echo; \ - jq -r -f tools/trivy-comment.jq "$(SARIF)"; \ - } > $(OUT) + 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 -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 scan jobs 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. +# 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 trivy-deps.sarif trivy-image.sarif; do \ + for report in $(SARIF_REPORTS); do \ if [ ! -s "$$report" ]; then \ echo "$$report: MISSING — no scan produced it"; \ fail=1; continue; \ @@ -307,8 +338,8 @@ hooks: ## 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 security.yml scans and what deploys, so a local `make docker && -# make trivy-image` has to scan the same thing CI gates on. The version vars +# 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: @@ -320,11 +351,67 @@ docker: --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. +# +# 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 \ - trivy-deps.sarif trivy-image.sarif \ - trivy-deps-comment.md trivy-image-comment.md + $(SARIF_REPORTS) trivy-report.md \ + image-metadata-*.json digest-*.txt $(GOLANGCI_LINT): @mkdir -p $(BIN_DIR) @@ -352,4 +439,5 @@ $(TRIVY): .PHONY: help build test cover test-ci merge-coverage cover-diff lint fmt \ lint-actions lint-staged hooks \ - trivy-deps trivy-image trivy-comment trivy-gate docker clean + trivy-deps trivy-image trivy-report trivy-gate \ + docker image-build image-publish require-image-repo clean From 3ab7fc79dc33ecac062e2e2b63edc26df9ca0ea5 Mon Sep 17 00:00:00 2001 From: Manjunath Davanam Date: Mon, 7 Sep 2026 23:14:35 +0530 Subject: [PATCH 19/23] ci: move the tag jobs to their own workflow so PRs stop showing them [#5] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gating Build Artifact and Publish Artifact with `if: startsWith(github.ref, 'refs/tags/')` inside ci.yml put two permanently skipped checks on every PR: CI / Build Artifact (${{ matrix.arch }}) (pull_request) Skipped CI / Publish Artifact (pull_request) Skipped A job whose `if:` is false is still reported as a check, and its `name:` comes through with `${{ matrix.arch }}` unexpanded because the matrix leg never materialised — so the skipped-check noise and the raw expression in the name were the same bug. A workflow whose trigger does not match the event does not exist on that event at all, which is the only way to get zero checks. So: ci.yml is PR/trunk only (push to main and development, plus pull_request) and carries no `if:` on either job, and ci-release.yml is tag-only. Both are `name: CI`, so the check names still read "CI / Build Artifact (amd64)" and "CI / Publish Artifact". The one cost is two entries called "CI" in the Actions sidebar. A PR now shows exactly two checks, and a tag exactly three. --- .github/workflows/ci-release.yml | 121 +++++++++++++++++++++++++++++ .github/workflows/ci.yml | 129 ++++--------------------------- Makefile | 10 +-- 3 files changed, 141 insertions(+), 119 deletions(-) create mode 100644 .github/workflows/ci-release.yml 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 5b0cd5e7..1b1504e3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,9 +1,14 @@ name: CI -# Four checks and no more. On a PR or a trunk push: "End to End verification" -# and "Security Scan". On a version tag: "Build Artifact" and -# "Publish Artifact". Nothing runs on both event kinds, so a PR never shows a -# publish check and a tag never re-runs the tests the PR already gated on. +# 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 @@ -12,22 +17,13 @@ name: CI on: push: # 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. + # 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] - # 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 too 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]+-*" pull_request: -# Least privilege at the workflow level; the three jobs that need more declare -# it for themselves. +# Least privilege at the workflow level; the jobs that need more declare it for +# themselves. permissions: contents: read @@ -37,22 +33,14 @@ permissions: # group and supersedes the run before it. concurrency: group: ci-${{ github.ref }} - # PRs only. On main, development and tags every commit keeps its own recorded + # 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, and would abort a - # half-finished publish. + # no way to tell afterwards whether it was ever green. cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: verify: name: End to End verification - # Everything except a tag push, i.e. PRs and trunk pushes. Tested on - # github.ref rather than github.ref_type: on a pull_request the ref is - # refs/pull//merge, which is neither refs/heads/ nor refs/tags/, and - # ref_type is not documented to be 'branch' there. Guessing wrong would - # skip this job on every PR and show a green check for a run that did - # nothing, so the condition names the one case it means to exclude. - if: ${{ !startsWith(github.ref, 'refs/tags/') }} runs-on: ubuntu-latest # pull-requests: write is for the coverage comment below. permissions: @@ -135,8 +123,6 @@ jobs: security-scan: name: Security Scan - # Same condition as End to End verification above — see the note there. - if: ${{ !startsWith(github.ref, 'refs/tags/') }} 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" — @@ -216,88 +202,3 @@ jobs: # 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 - - 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 }}) - if: startsWith(github.ref, 'refs/tags/') - 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 - if: startsWith(github.ref, 'refs/tags/') - # 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/Makefile b/Makefile index 1f102355..a023cdfa 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,10 @@ # OAN Network Adapter — build, test, lint and security-scan targets. # -# Single source of truth for the ci.yml workflow: 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 the workflow is GitHub context -# (`${{ }}` expressions, $GITHUB_STEP_SUMMARY writes) that has no meaning -# outside a runner. +# 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. From 5a0f1bd2e499e00efa65f7b1c0bf011ca9b76dd3 Mon Sep 17 00:00:00 2001 From: Manjunath Davanam Date: Mon, 7 Sep 2026 23:25:27 +0530 Subject: [PATCH 20/23] =?UTF-8?q?ci:=20drop=20the=20SARIF=20upload=20?= =?UTF-8?q?=E2=80=94=20it=20was=20a=20third=20check,=20and=20it=20contradi?= =?UTF-8?q?cted=20the=20gate=20[#5]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A PR reported three checks, not two: CI / End to End verification success CI / Security Scan failure Code scanning results / Trivy success — "No new alerts in code change" The third is not a job. github/codeql-action/upload-sarif registers its own check named after the SARIF tool driver, so it cannot be renamed, given an `if:`, or suppressed while the upload happens. And on that run it was green while `make trivy-gate` failed on the same SARIF: 20 HIGH stdlib CVEs, none of them *new to the diff*, which is the only thing the code-scanning check measures. Two checks disagreeing about one scan is worse than one check, and the gate is the one that is right. Every finding is already in the PR comment with its severity, installed version, fixed version and advisory link, and `make trivy-gate` decides pass/fail. `security-events: write` goes with the upload. The cost, stated: no Security-tab history or diff annotations for Trivy findings. The comment and the gate are the surface now. --- .github/workflows/ci.yml | 35 ++++++++++++++--------------------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1b1504e3..c5fd9dbf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -130,12 +130,11 @@ jobs: # checkout, so it runs first and its findings are in the log before the # image finishes building. # - # pull-requests: write is the PR comment, security-events: write is the - # SARIF upload to the Security tab. + # pull-requests: write is the PR comment. No security-events: write — the + # SARIF upload is gone, see below. permissions: contents: read pull-requests: write - security-events: write steps: - uses: actions/checkout@v4 - uses: ./.github/actions/trivy-cache @@ -151,25 +150,19 @@ jobs: - run: make docker IMAGE=network-adapter:${{ github.sha }} - run: make trivy-image IMAGE=network-adapter:${{ github.sha }} - # continue-on-error: a fork PR's GITHUB_TOKEN has no security-events - # write, so these 403 there. make trivy-gate below is what decides - # pass/fail; an upload that can't happen must not fail the job over it. + # 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. # - # A category per scan — GitHub keys an analysis on (ref, category), so - # without them the second upload would replace the first. - - name: Upload dependency findings to code scanning - continue-on-error: true - uses: github/codeql-action/upload-sarif@v3 - with: - sarif_file: trivy-deps.sarif - category: trivy-dependencies - - - name: Upload image findings to code scanning - continue-on-error: true - uses: github/codeql-action/upload-sarif@v3 - with: - sarif_file: trivy-image.sarif - category: trivy-image + # 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. From eec90fdc9d3ba9f6846f811a8fa5e158e254da8c Mon Sep 17 00:00:00 2001 From: Manjunath Davanam Date: Mon, 7 Sep 2026 23:25:27 +0530 Subject: [PATCH 21/23] fix: bump the Go toolchain to 1.26.8 to clear 20 HIGH stdlib CVEs [#5] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The image scan the Security Scan job runs found 20 HIGH findings, all of them the same package: `stdlib` at v1.26.1. Zero came from application dependencies — `trivy-deps` is clean. Trivy reads the Go build info embedded in the binary, which is why these show up in the image scan and nowhere the dependency scan can see. The advisories' fixed versions top out at 1.26.6; 1.26.8 is the current 1.26 patch, so it clears all 20 with room before the next one lands. Bumped in three places that have to agree: go.mod's `go` directive (which is also what setup-go resolves through `go-version-file`, so the test toolchain follows) and both Dockerfiles' builder base. Leaving Dockerfile.adapter at 1.26.1 would have kept the two images on different toolchains for no reason. This re-touches the Dockerfiles, which this PR had otherwise reverted to byte-identical with development as out of scope. Different case: a one-token patch bump to the base image tag, made because the Security gate this PR introduces cannot go green without it — not the base-image migration that was reverted. Verified: `make build` and the full `make test` (every package, including pkg/plugin and benchmarks/e2e, which build and plugin.Open a real .so) pass on 1.26.8 with zero failures. --- Dockerfile.adapter | 2 +- Dockerfile.adapter-with-plugins | 2 +- go.mod | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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/go.mod b/go.mod index 10145dc5..5715fa56 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ 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 From 84cb6b5fc200ca8b5462d67b13df0caa42b8a846 Mon Sep 17 00:00:00 2001 From: Manjunath Davanam Date: Mon, 7 Sep 2026 23:45:45 +0530 Subject: [PATCH 22/23] chore: widen the Trivy gate to CRITICAL,HIGH,MEDIUM,LOW [#5] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The gate scanned at HIGH,CRITICAL only, so "0 findings" in the check meant "0 in that band" rather than "clean". Unfiltered, the tree has three findings in golang.org/x/crypto v0.55.0 that the gate never saw. UNKNOWN stays out of the band deliberately. All three of those findings carry no vendor severity, and one (GO-2026-5932, x/crypto/openpgp is unmaintained) has no fix at all, so including UNKNOWN would pin the gate red with no action available. None is reachable here: the only x/crypto import in the tree is blake2b, and the build closure of the adapter plus every plugin contains no x/crypto/ssh or x/crypto/openpgp. SEVERITY stays the single source of truth. The gate already counted rows in the SARIF rather than re-filtering, so widening the scan band was enough; the three places that restated "HIGH or CRITICAL" in prose now derive it, including the jq renderer, which takes the band via --arg so the "nothing found" line cannot drift from the scan that produced it. Verified with trivy v0.74.0, the version CI pins: deps 0 at the new band (the 3 UNKNOWN correctly excluded), wolfi-base runtime layer 0, shipped binary 0. The band change has teeth — the pre-bump go1.26.1 binary goes from 20 to 28 findings, the 8 extra all MEDIUM. Gate exit codes: 2 on findings, 2 on a missing report, 0 only when clean. --- .github/workflows/ci.yml | 2 +- Makefile | 6 +++--- tools/trivy-comment.jq | 10 +++++++--- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c5fd9dbf..5aee8d29 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -191,7 +191,7 @@ jobs: body-path: trivy-report.md edit-mode: replace - # The gate, last: it reads both reports and fails on a HIGH/CRITICAL + # 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/Makefile b/Makefile index a023cdfa..5a2104a7 100644 --- a/Makefile +++ b/Makefile @@ -33,7 +33,7 @@ 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 ?= HIGH,CRITICAL +SEVERITY ?= CRITICAL,HIGH,MEDIUM,LOW GOLANGCI_LINT_VERSION := v2.5.0 GOTESTSUM_VERSION := v1.13.0 TRIVY_VERSION := v0.74.0 @@ -233,7 +233,7 @@ trivy-report: esac; \ echo; echo "### $$title"; echo; \ if [ -s "$$report" ]; then \ - jq -r -f tools/trivy-comment.jq "$$report"; \ + jq -r --arg severity "$(SEVERITY)" -f tools/trivy-comment.jq "$$report"; \ else \ echo "⚠️ No report — the scan did not produce $$report."; \ fi; \ @@ -262,7 +262,7 @@ trivy-gate: fail=1; \ fi; \ done; \ - [ "$$fail" -eq 0 ] || echo "::error::HIGH or CRITICAL Trivy findings, or a missing report — see the log above"; \ + [ "$$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 diff --git a/tools/trivy-comment.jq b/tools/trivy-comment.jq index 54371ac1..b56ba9fd 100644 --- a/tools/trivy-comment.jq +++ b/tools/trivy-comment.jq @@ -1,14 +1,18 @@ # Render a Trivy SARIF report as a markdown table for a PR comment. # -# Used by the Makefile's trivy-comment target for both the dependency and the +# 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 -f tools/trivy-comment.jq +# 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". @@ -16,7 +20,7 @@ def val(re; default): (capture(re) // {v: default}).v; [ .runs[]?.results[]? ] as $found | if ($found | length) == 0 then - "No HIGH or CRITICAL findings." + "No findings at " + $severity + "." else "| Package | Severity | Installed | Fixed in | Advisory |", "|---|---|---|---|---|", From 138ffbe6d451a8037471243c485c7aee170d9800 Mon Sep 17 00:00:00 2001 From: Manjunath Davanam Date: Tue, 8 Sep 2026 00:05:44 +0530 Subject: [PATCH 23/23] fix: fail image-publish on a failed create, and pin the trivy installer [#5] image-publish joined its steps with `;`, so the recipe exit status came from `imagetools inspect` alone. A `create` that failed against an already-existing tag left inspect reporting the previous index and the job green, publishing a tag bound to the wrong digests with nothing red to say so. Every step is now `&&`-chained. The trivy installer was fetched from the repo default branch while only the version it installs was pinned, so a job holding the runner GITHUB_TOKEN piped an unreviewed remote script into sh on every cache miss. It now comes from the $(TRIVY_VERSION) tag. .gitignore also drifted from the target set: trivy-report.md, image-metadata-*.json and digest-*.txt are produced but were not ignored, while trivy-deps-comment.md and trivy-image-comment.md outlived the trivy-comment target that wrote them. --- .gitignore | 10 ++++++++-- Makefile | 19 +++++++++++++++---- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 4a9de6ae..6292bb58 100644 --- a/.gitignore +++ b/.gitignore @@ -148,10 +148,16 @@ 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-deps-comment.md -trivy-image-comment.md +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/Makefile b/Makefile index 5a2104a7..98ceb306 100644 --- a/Makefile +++ b/Makefile @@ -383,6 +383,12 @@ image-build: require-image-repo # 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 @@ -392,13 +398,13 @@ 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"; \ + 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; \ + esac && \ docker buildx imagetools create $$tags \ - $$(for d in digest-*.txt; do echo "$(IMAGE_REPO)@$$(cat $$d)"; done); \ + $$(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, @@ -432,9 +438,14 @@ $(ACTIONLINT): # 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/main/contrib/install.sh | \ + 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 \