fix(docker): pin 4 CLI tools to exact versions (#12576) (#12703) #2675
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Publish to Docker Hub | |
| on: | |
| push: | |
| branches: | |
| - main | |
| - "release/v*" | |
| tags: | |
| - "v*" | |
| paths-ignore: | |
| - ".github/workflows/**" | |
| # Use 'released' instead of 'published' so editing/re-publishing old releases | |
| # does NOT re-trigger this workflow. 'released' fires only on the initial | |
| # release publication (and pre-release → release transition). | |
| release: | |
| types: [released] | |
| workflow_dispatch: | |
| inputs: | |
| version: | |
| description: "Version tag to build (e.g. 3.8.4)" | |
| required: true | |
| type: string | |
| promote_latest: | |
| description: "Also tag :latest (only if this is the highest semver)" | |
| required: false | |
| type: boolean | |
| default: false | |
| # One publish per ref. A merge storm used to fan out 8 concurrent hosted builds, | |
| # every one OOM-killing `npm run build` inside BuildKit (#11976). The :next | |
| # channel only needs the newest SHA; cancel-in-progress is the same pattern as | |
| # quality.yml / nightly-release-green. | |
| concurrency: | |
| group: docker-publish-${{ github.ref }} | |
| cancel-in-progress: true | |
| # Least-privilege default: read-only at the top level; the build and merge jobs that | |
| # push to GHCR grant packages: write themselves (Scorecard TokenPermissions). | |
| permissions: | |
| contents: read | |
| jobs: | |
| prepare: | |
| name: Resolve Docker release metadata | |
| runs-on: ubuntu-latest | |
| outputs: | |
| version: ${{ steps.version.outputs.version }} | |
| promote_latest: ${{ steps.version.outputs.promote_latest }} | |
| skip: ${{ steps.version.outputs.skip }} | |
| env: | |
| IMAGE_NAME: diegosouzapw/omniroute | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v7 | |
| with: | |
| persist-credentials: false | |
| ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/v{0}', inputs.version) || '' }} | |
| # Need full tag history for semver comparison when deciding :latest. | |
| fetch-depth: 0 | |
| - name: Resolve version, latest-promotion, and skip flag | |
| id: version | |
| env: | |
| EVENT_NAME: ${{ github.event_name }} | |
| REF_NAME: ${{ github.ref_name }} | |
| REF_TYPE: ${{ github.ref_type }} | |
| INPUT_VERSION: ${{ inputs.version }} | |
| PROMOTE_INPUT: ${{ inputs.promote_latest }} | |
| DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} | |
| run: | | |
| set -euo pipefail | |
| # 1) Resolve version/channel from the trigger. Only the current default | |
| # release branch publishes the mutable `next` channel; main keeps `main`. | |
| VERSION=$(bash scripts/ci/resolve-docker-publish-version.sh \ | |
| "$EVENT_NAME" "$REF_TYPE" "$REF_NAME" "$INPUT_VERSION" "$DEFAULT_BRANCH") | |
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | |
| # Frozen release branches keep receiving coordination commits after the | |
| # next cycle becomes the default branch. They must not overwrite :next, | |
| # but that expected no-op is not a workflow failure. | |
| if [ "$VERSION" = "skip" ]; then | |
| echo "promote_latest=false" >> "$GITHUB_OUTPUT" | |
| echo "skip=true" >> "$GITHUB_OUTPUT" | |
| echo "Skipping Docker publish from non-default release branch: $REF_NAME" | |
| exit 0 | |
| fi | |
| # 2) Decide whether to promote :latest. Floating channels are never | |
| # eligible, and the helper independently fails closed for non-semver. | |
| PROMOTE="false" | |
| if [ "$VERSION" = "main" ] || [ "$VERSION" = "next" ]; then | |
| PROMOTE="false" | |
| elif printf '%s' "$VERSION" | grep -qE -- '-(rc|alpha|beta|pre|next)'; then | |
| echo "Pre-release identifier detected — skipping :latest." | |
| PROMOTE="false" | |
| elif [ "$EVENT_NAME" = "workflow_dispatch" ]; then | |
| PROMOTE="${PROMOTE_INPUT:-false}" | |
| else | |
| git fetch --tags --quiet || true | |
| # Decide via the extracted helper, which folds VERSION into the | |
| # candidate set so the result is independent of git-tag sync timing | |
| # on `release` events (#5301). Without that, the freshly-created tag | |
| # is often not yet visible here and :latest stays a release behind. | |
| PROMOTE=$(git tag -l 'v[0-9]*' | bash scripts/ci/should-promote-latest.sh "$VERSION") | |
| if [ "$PROMOTE" != "true" ]; then | |
| echo "Version $VERSION is not the highest stable semver. Not promoting :latest." | |
| fi | |
| fi | |
| echo "promote_latest=$PROMOTE" >> "$GITHUB_OUTPUT" | |
| # 3) Skip immutable version tags that already exist. Floating `main` | |
| # and `next` channels are intentionally rebuilt on every matching push. | |
| SKIP="false" | |
| if [ "$VERSION" != "main" ] && [ "$VERSION" != "next" ]; then | |
| if docker manifest inspect "diegosouzapw/omniroute:${VERSION}" >/dev/null 2>&1; then | |
| echo "Image diegosouzapw/omniroute:${VERSION} already exists on Docker Hub — skipping rebuild." | |
| SKIP="true" | |
| fi | |
| fi | |
| echo "skip=$SKIP" >> "$GITHUB_OUTPUT" | |
| echo "Publishing diegosouzapw/omniroute:$VERSION (promote_latest=$PROMOTE, skip=$SKIP)" | |
| build: | |
| name: Build Docker (${{ matrix.platform }}) | |
| needs: prepare | |
| if: needs.prepare.outputs.skip != 'true' | |
| # amd64: the .113 omni-build pool (31 GB / 32 cores, ONE listener since | |
| # #12048). Hosted ubuntu-24.04 is ~7 GB and dies ResourceExhausted (#11976). | |
| # Falls back to hosted when USE_VPS_RUNNER is off. arm64: no ARM box — stay | |
| # on GitHub's ubuntu-24.04-arm. | |
| # Webpack on BOTH arches: Turbopack on omniroute-113-6 hit | |
| # TurbopackInternalError "there must be a path to a root" after 26 min | |
| # (run 33253576569). The same tree's arm64 webpack build on hosted ARM | |
| # succeeded (run 33264823398). Dockerfile already documents webpack as the | |
| # Docker escape hatch (OMNIROUTE_USE_TURBOPACK=0). | |
| runs-on: ${{ matrix.arch == 'amd64' && (vars.USE_VPS_RUNNER == 'true' && fromJSON('["self-hosted","omni-build"]') || 'ubuntu-24.04') || 'ubuntu-24.04-arm' }} | |
| # Share the 1-slot omni-build ceiling (#12048) with ci.yml `Build` / | |
| # npm-publish. Same group as main's Build so a :next publish waits beside | |
| # the artefact instead of sitting next to it. arm64 is hosted — its own | |
| # group, cancelled by the workflow-level concurrency. | |
| concurrency: | |
| group: ${{ matrix.arch == 'amd64' && 'heavy-build-main' || format('docker-publish-arm-{0}', github.ref) }} | |
| cancel-in-progress: ${{ matrix.arch != 'amd64' }} | |
| permissions: | |
| contents: read | |
| packages: write | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - platform: linux/amd64 | |
| arch: amd64 | |
| - platform: linux/arm64 | |
| arch: arm64 | |
| env: | |
| IMAGE_NAME: diegosouzapw/omniroute | |
| GHCR_IMAGE_NAME: ghcr.io/diegosouzapw/omniroute | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v7 | |
| with: | |
| persist-credentials: false | |
| ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/v{0}', inputs.version) || '' }} | |
| fetch-depth: 0 | |
| - name: Assert Docker Engine | |
| run: docker info | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v4 | |
| - name: Login to Docker Hub | |
| uses: docker/login-action@v4.6.0 | |
| with: | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: Login to GitHub Container Registry | |
| uses: docker/login-action@v4.6.0 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Build and push platform image by digest | |
| id: build | |
| uses: docker/build-push-action@v7 | |
| with: | |
| context: . | |
| target: runner-base | |
| platforms: ${{ matrix.platform }} | |
| build-args: | | |
| OMNIROUTE_USE_TURBOPACK=0 | |
| outputs: type=image,push-by-digest=true,name-canonical=true,push=true | |
| tags: | | |
| ${{ env.IMAGE_NAME }} | |
| ${{ env.GHCR_IMAGE_NAME }} | |
| cache-from: type=gha,scope=docker-${{ matrix.arch }} | |
| cache-to: type=gha,scope=docker-${{ matrix.arch }},mode=max,ignore-error=true | |
| no-cache: false | |
| env: | |
| DOCKER_BUILDKIT_INLINE_CACHE: 1 | |
| - name: Build and push WEB platform image by digest | |
| id: build-web | |
| uses: docker/build-push-action@v7 | |
| with: | |
| context: . | |
| target: runner-web | |
| platforms: ${{ matrix.platform }} | |
| build-args: | | |
| OMNIROUTE_USE_TURBOPACK=0 | |
| outputs: type=image,push-by-digest=true,name-canonical=true,push=true | |
| tags: | | |
| ${{ env.IMAGE_NAME }} | |
| ${{ env.GHCR_IMAGE_NAME }} | |
| cache-from: type=gha,scope=docker-web-${{ matrix.arch }} | |
| cache-to: type=gha,scope=docker-web-${{ matrix.arch }},mode=max,ignore-error=true | |
| no-cache: false | |
| env: | |
| DOCKER_BUILDKIT_INLINE_CACHE: 1 | |
| - name: Build and push BUN base platform image by digest | |
| id: build-bun-base | |
| # Bun is a best-effort compatibility target, not a supported runtime | |
| # (AGENTS.md -> Environment). Its `bun run build` has been OOM-killing on | |
| # both arches; letting that sink the whole publish means the SUPPORTED | |
| # runner-base / runner-web images never reach the registry either. The | |
| # image is still built and pushed whenever it succeeds — only its power to | |
| # block the release is removed. | |
| continue-on-error: true | |
| uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7 | |
| with: | |
| context: . | |
| file: Dockerfile.bun | |
| target: runner-base | |
| platforms: ${{ matrix.platform }} | |
| build-args: | | |
| OMNIROUTE_USE_TURBOPACK=0 | |
| outputs: type=image,push-by-digest=true,name-canonical=true,push=true | |
| tags: | | |
| ${{ env.IMAGE_NAME }} | |
| ${{ env.GHCR_IMAGE_NAME }} | |
| cache-from: type=gha,scope=docker-bun-base-${{ matrix.arch }} | |
| cache-to: type=gha,scope=docker-bun-base-${{ matrix.arch }},mode=max,ignore-error=true | |
| no-cache: false | |
| env: | |
| DOCKER_BUILDKIT_INLINE_CACHE: 1 | |
| - name: Build and push BUN web platform image by digest | |
| id: build-bun-web | |
| # Bun is a best-effort compatibility target, not a supported runtime | |
| # (AGENTS.md -> Environment). Its `bun run build` has been OOM-killing on | |
| # both arches; letting that sink the whole publish means the SUPPORTED | |
| # runner-base / runner-web images never reach the registry either. The | |
| # image is still built and pushed whenever it succeeds — only its power to | |
| # block the release is removed. | |
| continue-on-error: true | |
| uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7 | |
| with: | |
| context: . | |
| file: Dockerfile.bun | |
| target: runner-web | |
| platforms: ${{ matrix.platform }} | |
| build-args: | | |
| OMNIROUTE_USE_TURBOPACK=0 | |
| outputs: type=image,push-by-digest=true,name-canonical=true,push=true | |
| tags: | | |
| ${{ env.IMAGE_NAME }} | |
| ${{ env.GHCR_IMAGE_NAME }} | |
| cache-from: type=gha,scope=docker-bun-web-${{ matrix.arch }} | |
| cache-to: type=gha,scope=docker-bun-web-${{ matrix.arch }},mode=max,ignore-error=true | |
| no-cache: false | |
| env: | |
| DOCKER_BUILDKIT_INLINE_CACHE: 1 | |
| - name: Export digests | |
| env: | |
| DIGEST_BASE: ${{ steps.build.outputs.digest }} | |
| DIGEST_WEB: ${{ steps.build-web.outputs.digest }} | |
| DIGEST_BUN_BASE: ${{ steps.build-bun-base.outputs.digest }} | |
| DIGEST_BUN_WEB: ${{ steps.build-bun-web.outputs.digest }} | |
| run: | | |
| set -euo pipefail | |
| mkdir -p /tmp/digests/base /tmp/digests/web /tmp/digests/bun-base /tmp/digests/bun-web | |
| touch "/tmp/digests/base/${DIGEST_BASE#sha256:}" | |
| touch "/tmp/digests/web/${DIGEST_WEB#sha256:}" | |
| # Empty when the (non-blocking) bun build produced no image. `if` blocks, | |
| # not `[ -n ] && touch`: under `set -e` a failing AND-list aborts the step, | |
| # which is precisely the case being handled here. | |
| if [ -n "$DIGEST_BUN_BASE" ]; then | |
| touch "/tmp/digests/bun-base/${DIGEST_BUN_BASE#sha256:}" | |
| fi | |
| if [ -n "$DIGEST_BUN_WEB" ]; then | |
| touch "/tmp/digests/bun-web/${DIGEST_BUN_WEB#sha256:}" | |
| fi | |
| - name: Upload base digests | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: digests-base-${{ matrix.arch }} | |
| path: /tmp/digests/base/* | |
| if-no-files-found: error | |
| retention-days: 1 | |
| - name: Upload web digests | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: digests-web-${{ matrix.arch }} | |
| path: /tmp/digests/web/* | |
| if-no-files-found: error | |
| retention-days: 1 | |
| - name: Upload bun-base digests | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| with: | |
| name: digests-bun-base-${{ matrix.arch }} | |
| path: /tmp/digests/bun-base/* | |
| # `ignore`, not `error`: the bun build is non-blocking, so an absent | |
| # digest is the expected outcome of a failed/skipped bun image — the | |
| # manifest step already treats these tags as optional. Leaving `error` | |
| # here just relocates the blocker from the manifest to the upload. | |
| if-no-files-found: ignore | |
| retention-days: 1 | |
| - name: Upload bun-web digests | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| with: | |
| name: digests-bun-web-${{ matrix.arch }} | |
| path: /tmp/digests/bun-web/* | |
| # `ignore`, not `error`: the bun build is non-blocking, so an absent | |
| # digest is the expected outcome of a failed/skipped bun image — the | |
| # manifest step already treats these tags as optional. Leaving `error` | |
| # here just relocates the blocker from the manifest to the upload. | |
| if-no-files-found: ignore | |
| retention-days: 1 | |
| merge: | |
| name: Publish multi-arch manifests | |
| needs: | |
| - prepare | |
| - build | |
| if: needs.prepare.outputs.skip != 'true' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| packages: write | |
| security-events: write | |
| env: | |
| IMAGE_NAME: diegosouzapw/omniroute | |
| GHCR_IMAGE_NAME: ghcr.io/diegosouzapw/omniroute | |
| VERSION: ${{ needs.prepare.outputs.version }} | |
| PROMOTE_LATEST: ${{ needs.prepare.outputs.promote_latest }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v7 | |
| with: | |
| persist-credentials: false | |
| ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/v{0}', inputs.version) || '' }} | |
| fetch-depth: 0 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v4 | |
| - name: Login to Docker Hub | |
| uses: docker/login-action@v4.6.0 | |
| with: | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: Login to GitHub Container Registry | |
| uses: docker/login-action@v4.6.0 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Download base digests | |
| uses: actions/download-artifact@v8 | |
| with: | |
| pattern: digests-base-* | |
| path: /tmp/digests/base | |
| merge-multiple: true | |
| - name: Download web digests | |
| uses: actions/download-artifact@v8 | |
| with: | |
| pattern: digests-web-* | |
| path: /tmp/digests/web | |
| merge-multiple: true | |
| - name: Download bun-base digests | |
| # Non-blocking: the bun image is best-effort, so its artifact may not | |
| # exist at all. The manifest step treats these tags as optional. | |
| continue-on-error: true | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 | |
| with: | |
| pattern: digests-bun-base-* | |
| path: /tmp/digests/bun-base | |
| merge-multiple: true | |
| - name: Download bun-web digests | |
| # Non-blocking: the bun image is best-effort, so its artifact may not | |
| # exist at all. The manifest step treats these tags as optional. | |
| continue-on-error: true | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 | |
| with: | |
| pattern: digests-bun-web-* | |
| path: /tmp/digests/bun-web | |
| merge-multiple: true | |
| - name: Create Docker Hub manifest | |
| run: | | |
| set -euo pipefail | |
| create_manifest() { | |
| local image="$1" suffix="$2" dir="$3" optional="${4:-}" | |
| local tags=(-t "${image}:${VERSION}${suffix}") | |
| if [ "$PROMOTE_LATEST" = "true" ]; then | |
| tags+=(-t "${image}:latest${suffix}") | |
| fi | |
| local refs=() | |
| while IFS= read -r digest_file; do | |
| refs+=("${image}@sha256:$(basename "$digest_file")") | |
| done < <(find "$dir" -type f | sort) | |
| if [ "${#refs[@]}" -eq 0 ]; then | |
| if [ -n "$optional" ]; then | |
| echo "::warning::No image digests in $dir — skipping optional tag ${image}:${VERSION}${suffix}" >&2 | |
| return 0 | |
| fi | |
| echo "No image digests in $dir" >&2 | |
| exit 1 | |
| fi | |
| docker buildx imagetools create "${tags[@]}" "${refs[@]}" | |
| } | |
| create_manifest "${IMAGE_NAME}" "" /tmp/digests/base | |
| create_manifest "${IMAGE_NAME}" "-web" /tmp/digests/web | |
| create_manifest "${IMAGE_NAME}" "-bun" /tmp/digests/bun-base optional | |
| create_manifest "${IMAGE_NAME}" "-web-bun" /tmp/digests/bun-web optional | |
| - name: Create GHCR manifest | |
| run: | | |
| set -euo pipefail | |
| create_manifest() { | |
| local image="$1" suffix="$2" dir="$3" optional="${4:-}" | |
| local tags=(-t "${image}:${VERSION}${suffix}") | |
| if [ "$PROMOTE_LATEST" = "true" ]; then | |
| tags+=(-t "${image}:latest${suffix}") | |
| fi | |
| local refs=() | |
| while IFS= read -r digest_file; do | |
| refs+=("${image}@sha256:$(basename "$digest_file")") | |
| done < <(find "$dir" -type f | sort) | |
| if [ "${#refs[@]}" -eq 0 ]; then | |
| if [ -n "$optional" ]; then | |
| echo "::warning::No image digests in $dir — skipping optional tag ${image}:${VERSION}${suffix}" >&2 | |
| return 0 | |
| fi | |
| echo "No image digests in $dir" >&2 | |
| exit 1 | |
| fi | |
| docker buildx imagetools create "${tags[@]}" "${refs[@]}" | |
| } | |
| create_manifest "${GHCR_IMAGE_NAME}" "" /tmp/digests/base | |
| create_manifest "${GHCR_IMAGE_NAME}" "-web" /tmp/digests/web | |
| create_manifest "${GHCR_IMAGE_NAME}" "-bun" /tmp/digests/bun-base optional | |
| create_manifest "${GHCR_IMAGE_NAME}" "-web-bun" /tmp/digests/bun-web optional | |
| - name: Inspect image | |
| if: needs.prepare.outputs.version != 'main' | |
| run: | | |
| docker buildx imagetools inspect "${IMAGE_NAME}:${VERSION}" | |
| - name: Generate CycloneDX SBOM (image, advisory) | |
| if: needs.prepare.outputs.version != 'main' | |
| continue-on-error: true | |
| uses: anchore/sbom-action@v0 | |
| with: | |
| image: ${{ env.GHCR_IMAGE_NAME }}:${{ env.VERSION }} | |
| format: cyclonedx-json | |
| output-file: sbom-image.cdx.json | |
| artifact-name: sbom-image.cdx.json | |
| # Visibility scan: reports HIGH + CRITICAL into the SARIF (Security tab) but | |
| # never blocks (exit-code 0). The blocking gate below narrows to CRITICAL. | |
| # | |
| # ignore-unfixed mirrors the blocking gate: the Security tab must surface only | |
| # ACTIONABLE vulnerabilities — ones with a published fix we can pull by rebuilding | |
| # on a patched base or bumping the dep. Without it the advisory upload floods the | |
| # tab with unfixable base-image OS CVEs (Debian trixie packages with no upstream | |
| # patch yet, overwhelmingly local-only and not reachable from the proxy request | |
| # surface), which is noise an operator cannot act on. trivyignores points at the | |
| # repo-root .trivyignore so accepted-risk fixable CVEs have one auditable home. | |
| # See docs/security/SUPPLY_CHAIN.md. | |
| - name: Trivy image scan (SARIF, advisory) | |
| if: needs.prepare.outputs.version != 'main' | |
| continue-on-error: true | |
| uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 | |
| with: | |
| image-ref: ${{ env.GHCR_IMAGE_NAME }}:${{ env.VERSION }} | |
| format: sarif | |
| output: trivy-results.sarif | |
| severity: HIGH,CRITICAL | |
| ignore-unfixed: true | |
| trivyignores: .trivyignore | |
| exit-code: "0" | |
| # BLOCKING gate (v3.8.27 cycle-end): fail the release on a CRITICAL CVE in the | |
| # published image. Narrowed to severity CRITICAL (HIGH stays visible in the | |
| # SARIF step above, not blocking). ignore-unfixed:true so an unfixable base-image | |
| # CVE with no upstream patch does not red the release (reduces false-blocks); | |
| # a fixable CRITICAL still blocks. Per docs/security/SUPPLY_CHAIN.md. NB: Trivy | |
| # scans against a CVE DB that grows continuously — a newly-disclosed CRITICAL on | |
| # an unchanged base image can red this gate; the fix is to rebuild on a patched | |
| # base, bump the dep, or add a justified .trivyignore entry (see the CVE-variance | |
| # note in docs/security/SUPPLY_CHAIN.md). | |
| - name: Trivy CRITICAL gate (blocking) | |
| if: needs.prepare.outputs.version != 'main' | |
| uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 | |
| with: | |
| image-ref: ${{ env.GHCR_IMAGE_NAME }}:${{ env.VERSION }} | |
| format: table | |
| severity: CRITICAL | |
| ignore-unfixed: true | |
| exit-code: "1" | |
| # Explicit: the advisory scan above already points at it, and the blocking | |
| # gate must honour the same accepted-risk list (#12084). | |
| trivyignores: .trivyignore | |
| - name: Upload Trivy SARIF to Security tab | |
| if: needs.prepare.outputs.version != 'main' | |
| continue-on-error: true | |
| uses: github/codeql-action/upload-sarif@v4.37.9 | |
| with: | |
| sarif_file: trivy-results.sarif | |
| category: trivy-image | |
| - name: Update Docker Hub description | |
| # Only refresh README/description when we actually promote :latest | |
| # (avoids overwriting from main, next, or back-fill builds). | |
| if: needs.prepare.outputs.promote_latest == 'true' | |
| uses: peter-evans/dockerhub-description@v5 | |
| with: | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| repository: diegosouzapw/omniroute | |
| short-description: "OmniRoute — Unified AI proxy. Route any LLM through one endpoint." | |
| readme-filepath: ./README.md |