Skip to content

Publish a gRPC-only agent image with a -grpc tag suffix - #177

Closed
keithfz wants to merge 2 commits into
mainfrom
grpc-only-image
Closed

keithfz wants to merge 2 commits into
mainfrom
grpc-only-image

Conversation

@keithfz

@keithfz keithfz commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Summary

This PR adds a second variant of the agent image that has no Node.js and no snyk-broker. The variant publishes to the same package, ghcr.io/cortexapps/cortex-axon-agent, with a -grpc suffix on every tag. The default image does not change.

Tag Contents Relay transport
:1.4.0, :main, :latest same as today (Node + snyk-broker) AXON_RELAY_TRANSPORT selects it; default snyk-broker
:1.4.0-grpc, :main-grpc no Node, no snyk-broker always grpc-tunnel

Changes

  • docker/Dockerfile: There are now four stages: builder → base (OS packages, Go, static env) → files (agent binary, accept files, plugins, scaffolds, scripts) → grpc / agent. agent is the last stage, so a build without --target still produces the default image. The agent files are copied in the final layer, so a code change no longer rebuilds the snyk-broker layer.
  • curl is now installed explicitly. The github-app-token plugin calls curl. Before, curl came into the image only through the NodeSource setup script, so without this change the gRPC image would break GitHub App auth.
  • docker/start, docker/entrypoint.sh: When snyk-broker is not in the image, the scripts force AXON_RELAY_TRANSPORT=grpc-tunnel.
  • CI: The build and scan jobs moved to the reusable docker-image.yml. docker.yml calls it once for each variant. The gRPC variant uses latest=false, so it never moves latest. Each variant has its own Trivy SARIF category, its own buildx cache key, and its own PR artifact name. trivy.yml also scans :main-grpc every day.
  • Makefile: Adds a docker-build-grpc target.
  • README.relay.md: Documents the -grpc tags.
  • Tests: docker-tests-grpc and the tunnel load test now build the grpc target, tagged cortex-axon-agent:local-grpc. So the gRPC e2e scenarios run on the image without Node. docker-tests still runs the snyk-broker scenarios on the default image.

Notes for reviewers

  • Check names change. For example, Run Trivy PR scan (local image) becomes Agent image / Run Trivy PR scan (local image). If branch protection requires the old names, update it.
  • The first build after merge will not hit the cache. The cache key now includes the target, so the old cache entries do not match.
  • I did not build the images locally. CI on this PR builds and scans both variants.

Test plan

  • CI builds both targets, and both Trivy PR scans run with separate categories.
  • docker-tests-grpc passes on the -grpc image (with and without proxy).
  • docker-tests passes on the default image (snyk-broker).
  • docker run --rm --entrypoint sh <grpc image> -c 'command -v node snyk-broker npm' prints nothing.
  • The -grpc image runs the gRPC tunnel when AXON_RELAY_TRANSPORT=snyk-broker is set.
  • The default image behaves the same as before in both modes.

🤖 Generated with Claude Code

The Dockerfile now has two runtime targets that share one base:
- agent (default, last stage): same image as before, with Node and snyk-broker.
- grpc: no Node and no snyk-broker. AXON_RELAY_TRANSPORT is forced to
  grpc-tunnel when snyk-broker is not on the image.

Agent files are copied in a final layer, so a code change no longer rebuilds
the snyk-broker layer. curl is now installed explicitly; the github-app-token
plugin needs it, and before it came only from the NodeSource setup script.

CI builds both targets through a reusable workflow. The grpc variant publishes
to the same package with a -grpc suffix on every tag, never moves latest, and
has its own Trivy SARIF category and daily scan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment on lines +29 to +115
runs-on: ubuntu-latest
env:
IS_DEFAULT_BRANCH: ${{ github.ref == format('refs/heads/{0}', 'main') }}
IS_TAG: ${{ startsWith(github.ref, 'refs/tags/') }}
IS_PUBLISH: ${{ github.event_name == 'workflow_dispatch' || github.ref == format('refs/heads/{0}', 'main') || startsWith(github.ref, 'refs/tags/') }}
# ARM64 builds via QEMU are slow - only build on main/tags, not workflow_dispatch
IS_ARM64_BUILD: ${{ github.ref == format('refs/heads/{0}', 'main') || startsWith(github.ref, 'refs/tags/') }}

steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Cache Docker layers
uses: actions/cache@v5
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ inputs.target }}-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-${{ inputs.target }}-

- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
ghcr.io/${{ github.repository_owner }}/cortex-axon-agent
flavor: |
latest=${{ inputs.latest }}
suffix=${{ inputs.tag-suffix }}
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha

- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
file: ./docker/Dockerfile
target: ${{ inputs.target }}
push: ${{ env.IS_PUBLISH == 'true' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
BUILD_VERSION=${{ github.ref_name }}-${{ github.sha }}
# arm64 via QEMU is slow, so only build it on main branch or tags (not workflow_dispatch)
platforms: ${{ env.IS_ARM64_BUILD == 'true' && 'linux/amd64,linux/arm64' || 'linux/amd64' }}
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache
load: ${{ env.IS_PUBLISH != 'true' }}

- name: Determine local image tag
if: env.IS_PUBLISH != 'true'
id: local-tag
run: echo "tag=$(echo '${{ steps.meta.outputs.tags }}' | head -n1)" >> "$GITHUB_OUTPUT"

- name: Save Docker image for scanning
if: env.IS_PUBLISH != 'true'
run: docker save '${{ steps.local-tag.outputs.tag }}' -o /tmp/docker-image.tar

- name: Upload Docker image artifact
if: env.IS_PUBLISH != 'true'
uses: actions/upload-artifact@v4
with:
name: docker-image-${{ inputs.target }}
path: /tmp/docker-image.tar
retention-days: 1

outputs:
image-tag: ${{ steps.local-tag.outputs.tag }}
# The primary tag metadata-action resolved -- the semver version on a
# tag build, the branch name on main. The release scan targets this
# rather than a hardcoded :main, so a tag build reports on the image
# that tag actually published.
published-ref: ghcr.io/${{ github.repository_owner }}/cortex-axon-agent:${{ steps.meta.outputs.version }}

trivy-pr:
Comment on lines +116 to +169
name: Run Trivy PR scan (local image)
needs: build
if: ${{ !(github.event_name == 'workflow_dispatch' || github.ref == format('refs/heads/{0}', 'main') || startsWith(github.ref, 'refs/tags/')) }}
runs-on: ubuntu-latest
steps:
- name: Download Docker image artifact
uses: actions/download-artifact@v4
with:
name: docker-image-${{ inputs.target }}
path: /tmp

- name: Load Docker image
run: docker load -i /tmp/docker-image.tar

- name: Run Trivy vulnerability scanner (SARIF)
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ needs.build.outputs.image-tag }}
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
ignore-unfixed: true
exit-code: '0'
skip-dirs: 'usr/local/go'

- name: Upload Trivy scan results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v4.32.2
with:
sarif_file: 'trivy-results.sarif'
# Matches the release scan's category so PR and post-merge findings
# for this image track one series instead of churning against
# each other in the Security tab.
category: ${{ inputs.category }}

# The bypass-trivy-fail label lets a PR merge past CVEs it did not
# introduce. The findings are still uploaded above and listed here.
- name: Fail on CRITICAL/HIGH vulnerabilities
env:
BYPASS: ${{ contains(github.event.pull_request.labels.*.name, 'bypass-trivy-fail') }}
run: |
COUNT=$(jq '[.runs[].results[] | select(.level == "error")] | length' trivy-results.sarif)
if [ "$COUNT" -gt 0 ]; then
LEVEL=error
if [ "$BYPASS" = "true" ]; then LEVEL=warning; fi
echo "::$LEVEL::Found $COUNT CRITICAL/HIGH fixable vulnerabilities:"
jq -r '.runs[].results[] | select(.level == "error") | "\(.ruleId): \(.message.text | split("\n") | .[0])"' trivy-results.sarif
if [ "$BYPASS" = "true" ]; then
echo "::warning::Not failing: the PR has the bypass-trivy-fail label."
exit 0
fi
exit 1
fi

trivy:
Comment on lines +18 to +25
name: Agent image
uses: ./.github/workflows/docker-image.yml
with:
image-ref: ${{ needs.build.outputs.published-ref }}
target: agent
category: trivy-cortex-axon-agent
secrets: inherit

grpc:
Comment on lines +26 to +33
name: Agent image (gRPC only)
uses: ./.github/workflows/docker-image.yml
with:
target: grpc
tag-suffix: -grpc
latest: 'false'
category: trivy-cortex-axon-agent-grpc
secrets: inherit
@github-advanced-security

Copy link
Copy Markdown
Contributor

You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool.

What Enabling Code Scanning Means:

  • The 'Security' tab will display more code scanning analysis results (e.g., for the default branch).
  • Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results.
  • You will be able to see the analysis results for the pull request's branch on this overview once the scans have completed and the checks have passed.

For more information about GitHub Code Scanning, check out the documentation.

docker-tests-grpc and the tunnel load test built the default image, so no
test exercised the image without Node and snyk-broker. They now build the
grpc target, tagged cortex-axon-agent:local-grpc so it does not replace the
default local image.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@keithfz keithfz closed this Oct 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants