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.
Pipeline stages
Section titled “Pipeline stages”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.
-
quality-check — checkout, then run the security gates (below) and language lint/compile against a
ciDocker stage. Runs on every push and PR. -
build — on pull requests only, does a validation Docker build (no push) and posts a PR comment. This proves the image builds before merge.
-
push — on
pushevents to release refs only: assume the AWS role via OIDC, log in to ECR, compute tags withdocker/metadata-action, build and push withdocker/build-push-action@v6, scan the image, sign it, and notify the release repo.
Security gates
Section titled “Security gates”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 (-llon prod/main,-lllelsewhere) — blocking.nexa-snowflake-api: the only repo with an activepytestgate —uv run ruff check,ruff format --check,pytest tests/unit -m "not integration"with coverage.nexa-agents-api/nexa-slackbot-service:python -m compileallplus an app-import / OpenAPI smoke test.nexa-web: Node 20make buildplusnjsscan(non-blocking SAST); a CodeQL job exists but is commented out.nexa-backend: TypeScript;tsc --noEmittype-check (invalidate.yml).
Registry, tags, and signing by target
Section titled “Registry, tags, and signing by target”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@v4assumes theAWS_ROLE_ARNrepo/environment variable (referenced in the workflow asvars.AWS_ROLE_ARN; no static keys; the job hasid-token: write). Login viaaws-actions/amazon-ecr-login@v2. - Build:
docker/build-push-action@v6,platforms: linux/amd64, GitHub Actions cache (type=gha).provenance: trueandsbom: trueare toggled on only for main/release/tag refs — the SBOM is BuildKit-native, there is no separate syft step. - Signing:
sigstore/cosign-installer@v3thencosign sign --yes REGISTRY/REPO@DIGEST— keyless / OIDC (Sigstore), no--key. Runs only on main/release/tag refs after a successful push. - Release notify: on main/tag,
peter-evans/repository-dispatch@v3firesimage-version-updatetodataready-ai/drai-customer-releases(tokenRELEASE_DISPATCH_TOKEN).
The parallel build-push-acr.yml publishes to Azure Container Registry (acrnexadev001.azurecr.io).
- Auth:
azure/login@v2OIDC withAZURE_CLIENT_ID,AZURE_TENANT_ID,AZURE_SUBSCRIPTION_ID, thenaz acr login. - Scanning uses the
aquasecurity/trivy-actioninstead 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.
Databricks
Section titled “Databricks”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).
Snowflake
Section titled “Snowflake”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.
Dockerfile conventions
Section titled “Dockerfile conventions”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 (deps → runtime) |
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 |
Named secrets and variables
Section titled “Named secrets and variables”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 todrai-customer-releases.secrets.APP_DEPLOY_REPO_TOKEN— dispatch tonexa-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.