Skip to content

TradAI CI/CD Pipeline Architecture

Version: 1.0.0 | Date: 2026-03-28 | Status: CURRENT Source: .github/workflows/, justfile

TL;DR: GitHub Actions (sole CI/CD). 16 workflows with path-based change detection and dynamic test matrix. Deployment gates: a green dev CI run publishes libraries (#942); v* tags additionally trigger docker-build and Lambda deploy -- all gated behind CI success via workflow_run. 4-stack Pulumi deployment via just infra-bootstrap. Manual workflow_dispatch available for emergency bypasses.


1. Pipeline Overview

flowchart LR
    subgraph Triggers
        PR[Pull Request]
        Push[Push to main]
        Tag["Tag v*"]
        Manual[workflow_dispatch]
        Schedule[Weekly / Monday 06:00 UTC]
    end

    subgraph CI["CI Gate (ci.yml)"]
        Changes[Detect Changes]
        Lint[Lint & Format]
        Type[Type Check]
        Test[Test Matrix]
        Security[Security Scan]
        Perf[Performance Tests]
        Contract[Contract Tests]
    end

    subgraph Deploy["Deployment Workflows"]
        Docker[Docker Build & Push]
        Lambda[Deploy Lambdas]
        Publish[Publish Libraries]
        Infra[Deploy Infrastructure]
        Docs[Deploy Documentation]
    end

    subgraph Targets
        ECR[Amazon ECR]
        ECS[ECS Services]
        LambdaFn[Lambda Functions]
        CA[CodeArtifact]
        CF[Cloudflare Pages]
        Pulumi[Pulumi Stacks]
    end

    PR --> CI
    Push --> CI
    Tag --> CI
    Schedule --> CI

    CI -->|workflow_run + v* tag| Docker
    CI -->|workflow_run + v* tag| Lambda
    CI -->|workflow_run: dev or v* tag| Publish
    Manual --> Infra
    Manual --> Lambda
    Push -->|docs paths| Docs

    Docker --> ECR --> ECS
    Lambda --> ECR --> LambdaFn
    Publish --> CA
    Infra --> Pulumi
    Docs --> CF

2. GitHub Actions Workflows

16 workflow files in .github/workflows/:

Workflow File Trigger Purpose Timeout
CI ci.yml push (dev + main, paths-ignore docs), PR, tag v*, weekly, dispatch Orchestrator: change detection, lint, typecheck, test matrix, security, perf, contract varies
Lint _lint.yml workflow_call (reusable) Ruff check + format (called by CI) 5 min
Test Package _test.yml workflow_call (reusable) Per-package pytest with coverage (called by CI matrix) 20 min
Docker Build & Push docker-build.yml workflow_run (CI success) Build 4 service images, push to ECR, redeploy ECS --
Deploy Lambdas deploy-lambdas.yml workflow_run (CI success), dispatch 5-stage: version, wheel, base image, individual lambdas, update functions --
Publish Libraries publish-libs.yml workflow_run (CI success on dev or a v* tag), validated dispatch Build all seven TradAI wheels, publish to CodeArtifact, read the registry back and verify by sha256, then notify tradai-bot/strategies. Authenticates by OIDC. 45 min
Deploy Infrastructure deploy-infra.yml PR (infra/**), dispatch Validate, preview (PR), deploy (manual) via pulumi-ci.sh 60 min
Deploy Documentation docs.yml push (main, docs paths), dispatch Build MkDocs, deploy to Cloudflare Pages --
Devcontainer CI devcontainer-ci.yml weekly (Sunday 02:00), dispatch Full test suite inside devcontainer image 30 min
Devcontainer Prebuild devcontainer-prebuild.yml push (main, .devcontainer/**), weekly Build and push devcontainer image to GHCR 30 min
Docs Freshness docs-freshness.yml scheduled, dispatch Check documentation freshness against codebase --
Registry Drift registry-drift.yml daily 07:17 UTC, dispatch Compare CodeArtifact versions against dev; file/update/close a drift issue (#942) 10 min
Publish API Types publish-npm-types.yml push (dev, contract paths), dispatch Generate + publish @tradai/api-types to CodeArtifact npm --
API Types Typecheck api-types-typecheck.yml PR (contract paths) Drift gate: committed specs vs generated types --
Performance performance.yml scheduled, dispatch Benchmark suite; files a regression issue --
Renovate renovate.yml scheduled, dispatch Self-hosted dependency updates --

3. CI Gate Structure

The CI workflow (ci.yml) is the quality gate. Since #942, publish-libs fires whenever CI passes on dev (or on a v* tag); docker-build and deploy-lambdas also deploy on every dev push.

sequenceDiagram
    participant Dev as Developer
    participant GH as GitHub
    participant CI as CI Workflow
    participant Deploy as Deployment Workflows

    Dev->>GH: Push PR
    GH->>CI: Trigger CI (PR)
    CI->>CI: Detect Changes (dorny/paths-filter)
    CI->>CI: Build Test Matrix
    par Parallel Jobs
        CI->>CI: Lint & Format (Ruff)
        CI->>CI: Type Check (MyPy, 7 packages)
        CI->>CI: Security Scan (pip-audit + Bandit)
        CI->>CI: Performance Tests (PR only)
        CI->>CI: Contract Tests (PR + schedule)
    end
    CI->>CI: Test Matrix (affected legs only, parallel)
    CI-->>Dev: Status checks on PR

    Dev->>GH: Merge + Tag v1.2.3
    GH->>CI: Trigger CI (tag)
    CI->>CI: Full matrix (all 10 legs)
    CI-->>GH: CI completed (success)

    GH->>Deploy: workflow_run event
    par Tag Deployments
        Deploy->>Deploy: Docker Build & Push (4 services)
        Deploy->>Deploy: Deploy Lambdas (23 Dockerfile-based functions + shared base)
        Deploy->>Deploy: Publish Libraries (CodeArtifact)
    end
    Deploy->>Deploy: Redeploy ECS Services

Tag-Based Gating

Deployment workflows use workflow_run gated on conclusion == 'success'. Since #942 publish-libs fires on head_branch == 'dev' as well as v* tags, so a green dev CI run publishes; docker-build and deploy-lambdas additionally have their own push: branches: [dev] triggers.


4. Change Detection and Test Matrix

The CI workflow uses path-based change detection (dorny/paths-filter@v3) to avoid running all 8 package test suites on every PR. Each filter includes the package's own source plus the specific tradai-common submodules it imports. Heavy consumers (backend, strategy-service, cli) watch all of tradai-common/**; light consumers (data, strategy, data-collection) watch only their specific deps.

28 filters. Eight drive the package legs (deps — workspace config, triggers all — plus common, data, strategy, backend, data-collection, strategy-service, cli); the rest gate the suite legs (lambdas, root-unit, integration-tests, mlflow-service, root-suites-{financial,regression,data-quality,backtest-validation}) and the non-matrix jobs (any-code, api-contract, version-guard, infra, aws-emulator, guard-surface, security-inputs, image-inputs, ci-config, renovate-config, ledger, lock-inputs).

On PRs, only affected packages run. On push/schedule/dispatch, all 10 matrix entries run: six packages at a 60% coverage threshold, cli at 45%, mlflow-service at 0, and two test-suite legs at 0 (integration, and root-suites — one leg covering tests/unit/*.py, tests/unit/lambdas, tests/financial, tests/regression, tests/data_quality and tests/backtest_validation, which used to be six separate legs). Integration tests run when common changes or 2+ packages are affected.


5. Deployment Flows

5.1 Lambda Deployment (5-Stage Pipeline)

The deploy-lambdas.yml workflow builds and deploys 22 Lambda functions as container images.

flowchart TB
    subgraph Stage1["Stage 1: Version"]
        V[Calculate Version<br/>tag or manual-YYYYMMDDHHMMSS]
    end

    subgraph Stage2["Stage 2: Wheel"]
        W[Build tradai-common wheel<br/>uv build libs/tradai-common]
    end

    subgraph Stage3["Stage 3: Base Image"]
        B[Build lambda-base<br/>lambdas/base/Dockerfile]
    end

    subgraph Stage4["Stage 4: Individual Lambdas"]
        direction LR
        L1[backtest-consumer]
        L2[drift-monitor]
        L3[health-check]
        L4[update-status]
        LN["... 14 more"]
    end

    subgraph Stage5["Stage 5: Update Functions"]
        U[aws lambda update-function-code<br/>for each function]
    end

    V --> Stage2
    Stage2 --> Stage3
    Stage3 --> Stage4
    Stage4 --> Stage5

22 Lambda functions auto-discovered from lambdas/*/Dockerfile (backtest-consumer, drift-monitor, health-check, update-status, validate-strategy, and 17 more -- plus base shared image). The 23rd Lambda (update-nat-routes) is an inline Python handler deployed directly via Pulumi (no Dockerfile), so it is not part of the lambda-bootstrap pipeline.

Local equivalent:

just lambda-bootstrap          # Full pipeline: wheel -> base -> all lambdas -> ECR push
just lambda-build-all          # Build only (no push)
just lambda-push-all           # Push pre-built images to ECR

5.2 Service Deployment (Docker Build & Push)

The docker-build.yml workflow builds 4 service images and redeploys ECS.

flowchart LR
    subgraph Build["Parallel Builds"]
        B1[backend<br/>services/backend/Dockerfile]
        B2[data-collection<br/>services/data-collection/Dockerfile]
        B3[strategy-service<br/>services/strategy-service/Dockerfile]
        B4[mlflow<br/>services/mlflow/Dockerfile]
    end

    subgraph Push["ECR Push"]
        ECR["ECR Registry<br/>:version + :latest tags"]
    end

    subgraph Redeploy["ECS Redeploy"]
        ECS["aws ecs update-service<br/>--force-new-deployment"]
    end

    Build --> Push --> Redeploy

ECS service names follow the pattern tradai-{service}-{env} with backend-api (not backend) matching infra/shared/tradai_infra_shared/config.py.

Local equivalent:

just docker-build              # Build all 4 service images (linux/amd64)
just service-push-all          # Build + push all to ECR
just ecs-force-deploy-all      # Force ECS redeployment (strategy-service)

CI vs Local Redeployment Targets

The docker-build.yml CI workflow redeploys backend-api, data-collection, strategy-service, and mlflow (the 4 services it builds). The just ecs-force-deploy-all command targets only strategy-service. Per-strategy tradai-strategy-{slug} trading services are redeployed via their own start/stop lifecycle. To redeploy other services locally, use just ecs-force-deploy <service> for each service individually.

5.3 Infrastructure Deployment (4-Stack Pulumi)

The deploy-infra.yml workflow manages 4 Pulumi stacks deployed in strict order.

infra/ci (the RunsOn self-hosted runner stack) is intentionally outside this workflow — it is applied by hand only (just infra-up-ci), since deploy-infra.yml itself would run on the RunsOn runners that stack provisions once CI migrates to them.

flowchart TB
    subgraph Validate
        V[Lint + Unit Tests<br/>all 4 stacks]
    end

    subgraph PR["PR: Preview"]
        P1["Preview dev"]
        P2["Preview staging"]
        P3["Preview prod"]
        PS["Post PR Summary<br/>(create/update/delete counts)"]
    end

    subgraph Manual["Manual: Deploy"]
        D1["persistent<br/>S3, DynamoDB, ECR, Cognito, CodeArtifact"]
        D2["foundation<br/>VPC, RDS, SQS, SNS"]
        D3["Lambda Bootstrap<br/>(just lambda-bootstrap)"]
        D4["compute<br/>ALB, ECS, Lambda, Step Functions"]
        D5["edge<br/>API Gateway, WAF, CloudWatch"]
    end

    Validate --> PR
    Validate --> Manual

    P1 --> PS
    P2 --> PS
    P3 --> PS

    D1 --> D2 --> D3 --> D4 --> D5

Deployment Order

The compute stack has a pre-flight check that verifies all Lambda images exist in ECR before deploying. If images are missing, it fails fast with instructions to run just lambda-bootstrap first.

Local equivalent:

just infra-bootstrap dev       # Full: persistent -> foundation -> lambda-bootstrap -> service-push -> compute -> edge
just infra-up-foundation dev   # Single stack
just infra-preview dev         # Preview all 4 stacks
just infra-recover foundation dev  # Recovery: cancel + refresh + preview drift

Deploy script: infra/pulumi-ci.sh handles layer iteration, backend login, stack selection, and pulumi preview/pulumi up for any combination of layers and environments.

5.4 Library Publishing

The publish-libs.yml workflow publishes all seven TradAI packages to AWS CodeArtifact for use by the separate tradai-strategies repository.

Publishing is continuous. Every green dev CI run from a push publishes — no release step, and no v* tag needed for Python packages. Two bounds the trigger does not advertise: ci.yml's paths-ignore means a docs-only merge starts no CI run at all, and the job requires workflow_run.event == 'push', so ci.yml's weekly cron and a manual CI dispatch produce a green dev run that publishes nothing.

v* tags are not obsolete — they still drive the Docker and Lambda release paths, and re-run library publishing. They are simply no longer the trigger Python packages depend on.

Three independent mechanisms cover three different failures, and conflating them is the mistake

942 was originally about:

Failure Covered by Limit
A guard-covered artifact input changed without a version bump version-bump-guard (required check) Covers src/**, artifact-affecting pyproject.toml content, and metadata-referenced files. Its docstring names two gaps it cannot see: .gitignore edits and implicitly-included LICENSE* files
The registry serves an asset that differs from the wheel this run built publish-libs registry read-back Runs only during a publish, and compares against this run's build
A version dev declares never reached the registry registry-drift.yml Scheduled daily, so detection is on the next successful scheduled or manual check

CodeArtifact acceptance is asset/checksum based, which is subtler than "versions are immutable": an identical same-filename re-upload is accepted with 2xx, the same filename with different bytes 409s, and a new filename can still be added under an existing version. So without a version bump the changed wheel is rejected while the run keeps going — and it is the read-back, not the upload, that turns that into a failure. The guard is a required check because catching it on the PR is cheaper than catching it after a merge.

Cross-repo contract. On a run that publishes something new, notify-strategies mints a GitHub App installation token scoped to repositories: strategies and sends a repository_dispatch of type tradai-packages-published. nightly.yml in tradai-bot/strategies listens for exactly that type and validates every strategy against the new packages. POST /dispatches returns 204 whether or not anything listens, so a rename on either side is green and permanently inert. A test pins the string on this side only — it does not read the consumer's workflow, so a listener rename in tradai-bot/strategies remains undetectable from here.

This buys latency, not safety: that repo's 03:00 UTC cron already validates against whatever is latest in the registry, so a dead dispatch costs detection speed, not coverage.

Authentication. All three publishing workflows (publish-libs, publish-npm-types, registry-drift) assume an IAM role by OIDC rather than using stored access keys. The trust policy admits exactly one subject, repo:tradai-bot/tradai:environment:publish-dev, which is why the environment: line on those jobs is load-bearing: workflow_dispatch runs the workflow file as it exists at the dispatched ref, so any in-file guard can be deleted — but deleting environment: yields a ref: subject the policy rejects, and no credentials are ever issued. See §4 (Security).

# CI pipeline steps:
uv build <each of the 7 packages> --out-dir dist       # Build wheel
twine upload --repository-url $CODEARTIFACT_URL ... # Publish to CodeArtifact

Local equivalent:

just codeartifact-login dev    # Configure pip/twine auth (12h expiry)
just publish-all-libs dev      # Build + publish all seven packages

5.5 Documentation Deployment

Triggered by pushes to docs/**, mkdocs.yml, lib/service READMEs, and architecture reports. Builds MkDocs with Material theme and deploys to Cloudflare Pages.


6. Emergency Procedures

Manual Dispatch Bypass

The workflow_dispatch trigger on deploy-lambdas.yml and deploy-infra.yml bypasses the CI gate. Use only for emergency hotfixes when CI is broken or blocking a critical deploy.

  • Emergency Lambda deploy: Actions > Deploy Lambdas > Run workflow (select env). Or locally: just lambda-bootstrap.
  • Emergency ECS hotfix: just ecs-force-deploy-all (or single service: just ecs-force-deploy strategy-service).
  • Rollback: Check out previous tag, rebuild and push: git checkout v1.2.2 && just docker-build && just service-push-all && just ecs-force-deploy-all. For Lambdas: just lambda-bootstrap.
  • Infrastructure recovery: just infra-recover foundation dev (cancels pending ops, refreshes state, previews drift). deploy-infra.yml also prints recovery instructions on failure.

7. Changelog

Date Change Author
2026-03-28 Initial document Architecture team

8. Dependencies

This document relates to:

  • 02-ARCHITECTURE-OVERVIEW.md -- System architecture and service topology
  • 04-SECURITY.md -- Security controls and secrets management
  • 05-SERVICES.md -- Service definitions (backend, data-collection, strategy-service, mlflow)
  • .github/workflows/ -- All 16 workflow definitions
  • justfile -- Local development and deployment commands
  • infra/pulumi-ci.sh -- Pulumi deployment script (4-stack orchestration)
  • .github/actions/setup-workspace/ -- Shared composite action for CI setup