Skip to content

CI/CD

This page documents the build/scan/publish pipeline shared by the Nexa service repos. Each service builds its own container image in GitHub Actions, scans it, and pushes branch-per-environment tags to a container registry. GitOps (ArgoCD) picks the image up from there — see GitOps & Deployments.

Every service repo carries a near-identical build-push-ecr.yml workflow. The differences are mostly language (TypeScript vs Python) and which registries a service targets; the AWS/ECR path is the canonical template and what this page describes.

Where workflows run and what triggers them

Section titled “Where workflows run and what triggers them”

Jobs run on the self-hosted runner labelled [self-hosted, linux, x64, drai-ec2-runner] (the Azure/ACR jobs use ubuntu-latest). The primary workflow is .github/workflows/build-push-ecr.yml in each repo.

Triggers are push and pull_request on develop, release (and release/**), and main, plus semver tags v*.*.*. The target environment is derived from the ref by the job’s environment: expression, and a matching GitHub Environment (dev/test/prod) gates the deploy:

Git ref Environment Image tags produced
develop dev develop, develop-SHA
release / release/** test release, release-latest, release-SHA
main prod latest, main-SHA
tag v*.*.* prod VERSION, MAJOR.MINOR, MAJOR

This branch-per-env tagging is the contract with the GitOps layer: argocd-image-updater watches the develop / release / latest tags per environment.

The workflow is three jobs — quality-check → build → push — with the scanning and signing steps gated so that only real publishes (push events on release refs) sign and ship.

  1. quality-check — checkout, then run the security gates (below) and language lint/compile against a ci Docker stage. Runs on every push and PR.

  2. build — on pull requests only, does a validation Docker build (no push) and posts a PR comment. This proves the image builds before merge.

  3. push — on push events to release refs only: assume the AWS role via OIDC, log in to ECR, compute tags with docker/metadata-action, build and push with docker/build-push-action@v6, scan the image, sign it, and notify the release repo.

Three gates run in quality-check, and all three block the pipeline on findings:

Gate Tool What it checks Blocking
Secret scan gitleaks/gitleaks-action@v2 Committed secrets (uses GITLEAKS_LICENSE) Yes
Dependency/SCA trivy fs --scanners vuln --severity CRITICAL,HIGH --exit-code 1 --ignore-unfixed Vulnerable libraries in the source tree Yes
Image scan trivy image --exit-code 1 (in the push job) Vulnerabilities in the built image Yes

Trivy runs from a pinned image (aquasec/trivy:0.69.3 on most repos; nexa-databricks-api pins 0.56.1). Some repos pass --ignorefile .trivyignore.

Language-specific gates on top:

  • nexa-databricks-api: ruff check + ruff format --check, and Bandit SAST with a graduated severity gate (-ll on prod/main, -lll elsewhere) — blocking.
  • nexa-snowflake-api: the only repo with an active pytest gate — uv run ruff check, ruff format --check, pytest tests/unit -m "not integration" with coverage.
  • nexa-agents-api / nexa-slackbot-service: python -m compileall plus an app-import / OpenAPI smoke test.
  • nexa-web: Node 20 make build plus njsscan (non-blocking SAST); a CodeQL job exists but is commented out.
  • nexa-backend: TypeScript; tsc --noEmit type-check (in validate.yml).

Images push to Amazon ECR in ap-southeast-1. Repository names are prefixed aws-iac-, e.g. aws-iac-nexa-backend, aws-iac-nexa-web, aws-iac-nexa-databricks-api, aws-iac-nexa-agents-api, aws-iac-nexa-slackbot-service.

  • Auth: OIDC only — aws-actions/configure-aws-credentials@v4 assumes the AWS_ROLE_ARN repo/environment variable (referenced in the workflow as vars.AWS_ROLE_ARN; no static keys; the job has id-token: write). Login via aws-actions/amazon-ecr-login@v2.
  • Build: docker/build-push-action@v6, platforms: linux/amd64, GitHub Actions cache (type=gha). provenance: true and sbom: true are toggled on only for main/release/tag refs — the SBOM is BuildKit-native, there is no separate syft step.
  • Signing: sigstore/cosign-installer@v3 then cosign sign --yes REGISTRY/REPO@DIGESTkeyless / OIDC (Sigstore), no --key. Runs only on main/release/tag refs after a successful push.
  • Release notify: on main/tag, peter-evans/repository-dispatch@v3 fires image-version-update to dataready-ai/drai-customer-releases (token RELEASE_DISPATCH_TOKEN).

The parallel build-push-acr.yml publishes to Azure Container Registry (acrnexadev001.azurecr.io).

  • Auth: azure/login@v2 OIDC with AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_SUBSCRIPTION_ID, then az acr login.
  • Scanning uses the aquasecurity/trivy-action instead of the pinned Docker image.
  • The ACR path does not run Gitleaks, Cosign signing, or SBOM/provenance — it is a lighter build than the ECR path.

nexa-databricks-api builds a 3-stage Dockerfile (builder → ci → runtime) on python:3.12-slim, Poetry, non-root appuser (uid 10001), port 8000. It also has a sync-secrets-to-databricks.yml workflow that pushes connector secrets into a Databricks secret scope using OAuth M2M (DATABRICKS_HOST, DATABRICKS_CLIENT_ID, DATABRICKS_CLIENT_SECRET, auth_type = oauth-m2m).

nexa-snowflake-api is the outlier: a single build-deploy.yml using uv (not Poetry), git-sha image tags plus latest, pushing to the Snowflake SPCS image registry (snow spcs image-registry login). Auth is key-pair JWT (SNOWFLAKE_ACCOUNT, SNOWFLAKE_USER, SNOWFLAKE_PRIVATE_KEY, authenticator = SNOWFLAKE_JWT). A deploy job then dispatches deploy-service to dataready-ai/nexa-aws-sf-deployments.

The Python services use a three-stage Dockerfile — builder (install deps into a venv) → ci (the target the quality-check job builds and runs lint/compile/tests inside) → runtime (slim, non-root). All base images are digest-pinned and run as a non-root user on port 8000.

Repo Language / base Stages Runtime user / port
nexa-backend Node 20 (node:20-bookworm-slim) 2 (depsruntime) node, 4000
nexa-web Node 20 build → nginx-unprivileged 2 uid 101, 3000
nexa-databricks-api python:3.12-slim 3 appuser 10001, 8000
nexa-agents-api Amazon Linux 2023, python 3.13 3 appuser 10001, 8000
nexa-slackbot-service python:3.14-slim 3 appuser 10001, 8000
nexa-snowflake-api python:3.11-slim 2 appuser 1000, 8000

Do not hard-code credentials; the pipeline reads them from repo/environment settings:

  • vars.AWS_ROLE_ARN — the OIDC role for ECR push (a variable, not a secret; some repos fail fast if empty).
  • secrets.GITLEAKS_LICENSE — Gitleaks Action license.
  • secrets.RELEASE_DISPATCH_TOKEN — dispatch to drai-customer-releases.
  • secrets.APP_DEPLOY_REPO_TOKEN — dispatch to nexa-aws-sf-deployments.
  • Azure: AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_SUBSCRIPTION_ID.
  • Snowflake: SNOWFLAKE_ACCOUNT, SNOWFLAKE_USER, SNOWFLAKE_PRIVATE_KEY, SNOWFLAKE_ROLE.
  • Databricks (secret sync): DATABRICKS_HOST, DATABRICKS_CLIENT_ID, DATABRICKS_CLIENT_SECRET, DATABRICKS_SECRET_SCOPE.