# SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

name: NVIDIA Dynamo Github Validation

on:
  push:
    branches:
    - main
    - release/*.*.*
    - "pull-request/[0-9]+"

concurrency:
    # The group name is a ternary operation. If the ref_name is 'main',
    # then the group name uses the run_id to ensure a unique group for
    # 'main' pushes. Otherwise, the group name is the ref_name, so that
    # workflows on the same PR/branch have the same group name for cancelling.
    group: dynamo-build-test-${{ github.ref_name == 'main' && github.run_id || github.ref_name }}
    cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}

env:
  CUDA_VERSION: '12.9'
  BUILDER_NAME: b-${{ github.run_id }}-${{ github.run_attempt }}

jobs:
  changed-files:
    runs-on: ubuntu-latest
    outputs:
      core: ${{ github.ref_name == 'main' || startsWith(github.ref_name, 'release/') || steps.changes.outputs.core == 'true' }}
      planner: ${{ github.ref_name == 'main' || startsWith(github.ref_name, 'release/') || steps.changes.outputs.planner == 'true' }}
      frontend: ${{ github.ref_name == 'main' || startsWith(github.ref_name, 'release/') || steps.changes.outputs.frontend == 'true' }}
      sglang: ${{ github.ref_name == 'main' || startsWith(github.ref_name, 'release/') || steps.changes.outputs.sglang == 'true' }}
      vllm: ${{ github.ref_name == 'main' || startsWith(github.ref_name, 'release/') || steps.changes.outputs.vllm == 'true' }}
      trtllm: ${{ github.ref_name == 'main' || startsWith(github.ref_name, 'release/') || steps.changes.outputs.trtllm == 'true' }}
      builder_name: ${{ steps.export-builder-name.outputs.builder_name }}
    steps:
      - name: Checkout code
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd  # v6.0.2
        # Do not use fetch-depth: 0 — changed-files now works with shallow clone
      - name: Check for changes
        id: changes
        uses: ./.github/actions/changed-files
        with:
          gh_token: ${{ github.token }}
      - name: Export builder name
        id: export-builder-name
        run: |
          echo "builder_name=${{ env.BUILDER_NAME }}" >> $GITHUB_OUTPUT

  dynamo-status-check:
    runs-on: ubuntu-latest
    needs: [changed-files, build, rust-checks, mypy, test-parallel, test-sequential, test-generic-gpu]
    if: always()
    steps:
      - name: "Check all dependent jobs"
        run: |
          echo '${{ toJson(needs) }}' | jq -e 'to_entries | map(.value.result) | all(. as $result | ["success", "skipped"] | any($result == .))'

  build:
    needs: changed-files
    if: >-
      needs.changed-files.outputs.core == 'true' ||
      needs.changed-files.outputs.planner == 'true' ||
      needs.changed-files.outputs.frontend == 'true' ||
      needs.changed-files.outputs.sglang == 'true' ||
      needs.changed-files.outputs.vllm == 'true' ||
      needs.changed-files.outputs.trtllm == 'true'
    runs-on: prod-builder-v3
    name: Build
    timeout-minutes: 60
    outputs:
      # Only pass the non-secret tag suffix between jobs (GitHub blanks outputs containing secrets)
      runtime_tag_suffix: ${{ steps.define_image_tag.outputs.runtime_tag_suffix }}
      test_tag_suffix: ${{ steps.define_image_tag.outputs.test_tag_suffix }}
    steps:
      - name: Checkout repository
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd  # v6.0.2
        with:
          lfs: true
      - name: Initialize Dynamo Builder
        uses: ./.github/actions/init-dynamo-builder
        with:
          builder_name: ${{ needs.changed-files.outputs.builder_name }}
          flavor: general
          arch: linux/amd64
      - name: Docker Login
        if: github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'push'
        uses: ./.github/actions/docker-login
        with:
          ngc_ci_access_token: ${{ secrets.NGC_CI_ACCESS_TOKEN }}
          aws_default_region: ${{ secrets.AWS_DEFAULT_REGION }}
          aws_account_id: ${{ secrets.AWS_ACCOUNT_ID }}
          azure_acr_hostname: ${{ secrets.AZURE_ACR_HOSTNAME }}
          azure_acr_user: ${{ secrets.AZURE_ACR_USER }}
          azure_acr_password: ${{ secrets.AZURE_ACR_PASSWORD }}
      - name: Define Image Tag
        id: define_image_tag
        env:
          ECR_HOSTNAME: ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ secrets.AWS_DEFAULT_REGION }}.amazonaws.com
        run: |
          CUDA_MAJOR=${CUDA_VERSION%%.*}
          RUNTIME_TAG_SUFFIX="${{ github.sha }}-dynamo-runtime-cuda${CUDA_MAJOR}-amd64"
          TEST_TAG_SUFFIX="${{ github.sha }}-dynamo-test-cuda${CUDA_MAJOR}-amd64"
          echo "runtime_tag_suffix=${RUNTIME_TAG_SUFFIX}" >> $GITHUB_OUTPUT
          echo "test_tag_suffix=${TEST_TAG_SUFFIX}" >> $GITHUB_OUTPUT
          echo "runtime_remote_tag=${ECR_HOSTNAME}/ai-dynamo/dynamo:${RUNTIME_TAG_SUFFIX}" >> $GITHUB_OUTPUT
          echo "test_remote_tag=${ECR_HOSTNAME}/ai-dynamo/dynamo:${TEST_TAG_SUFFIX}" >> $GITHUB_OUTPUT
      - name: Generate Dockerfile
        shell: bash
        run: |
          echo "Generating Dockerfile for target: runtime and framework: dynamo"
          python ./container/render.py \
              --target=runtime \
              --framework=dynamo \
              --platform=amd64 \
              --cuda-version=${{ env.CUDA_VERSION }} \
              --show-result \
              --output-short-filename
      - name: Build and Push Runtime Image
        uses: ./.github/actions/docker-remote-build
        with:
          image_tag: ${{ steps.define_image_tag.outputs.runtime_remote_tag }}
          framework: dynamo
          target: runtime
          platform: amd64
          cuda_version: ${{ env.CUDA_VERSION }}
          ci_token: ${{ secrets.CI_TOKEN }}
          aws_default_region: ${{ secrets.AWS_DEFAULT_REGION }}
          aws_account_id: ${{ secrets.AWS_ACCOUNT_ID }}
          sccache_s3_bucket: ${{ secrets.SCCACHE_S3_BUCKET }}
          aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          push_image: 'true'
      - name: Build and Push Test Image
        env:
          ECR_HOSTNAME: ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ secrets.AWS_DEFAULT_REGION }}.amazonaws.com
        shell: bash
        run: |
          CACHE_TAG="test-dynamo-cuda${CUDA_VERSION%%.*}-amd64-cache"
          CACHE_ARGS="--cache-from type=registry,ref=${ECR_HOSTNAME}/ai-dynamo/dynamo:main-${CACHE_TAG}"
          CACHE_ARGS+=" --cache-from type=registry,ref=${ECR_HOSTNAME}/ai-dynamo/dynamo:release-${CACHE_TAG}"
          if [[ "$GITHUB_REF_NAME" == "main" ]]; then
            CACHE_ARGS+=" --cache-to type=registry,ref=${ECR_HOSTNAME}/ai-dynamo/dynamo:main-${CACHE_TAG},mode=max"
          elif [[ "$GITHUB_REF_NAME" =~ ^release ]]; then
            CACHE_ARGS+=" --cache-to type=registry,ref=${ECR_HOSTNAME}/ai-dynamo/dynamo:release-${CACHE_TAG},mode=max"
          fi

          docker buildx build \
            --progress=plain \
            --push \
            --platform linux/amd64 \
            -f container/Dockerfile.test \
            --build-arg BASE_IMAGE=${{ steps.define_image_tag.outputs.runtime_remote_tag }} \
            ${CACHE_ARGS} \
            -t ${{ steps.define_image_tag.outputs.test_remote_tag }} .

  rust-checks:
    needs: [changed-files, build]
    if: needs.changed-files.outputs.core == 'true'
    runs-on: prod-tester-amd-gpu-v1
    name: Rust Checks
    timeout-minutes: 30
    env:
      CONTAINER_ID: test_${{ github.run_id }}_${{ github.run_attempt }}_rust_dynamo
      IMAGE_TAG: ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ secrets.AWS_DEFAULT_REGION }}.amazonaws.com/ai-dynamo/dynamo:${{ needs.build.outputs.runtime_tag_suffix }}
    steps:
      - name: Checkout repository
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd  # v6.0.2
      - name: Docker Login
        uses: ./.github/actions/docker-login
        with:
          aws_default_region: ${{ secrets.AWS_DEFAULT_REGION }}
          aws_account_id: ${{ secrets.AWS_ACCOUNT_ID }}
      - name: Pull runtime image
        run: |
          source ./.github/scripts/retry_docker.sh
          retry_pull ${{ env.IMAGE_TAG }}
      - name: Run Rust checks (block-manager + media-ffmpeg + integration tests)
        run: |
          docker run --rm --runtime=nvidia --gpus all --user root -w /workspace/lib/llm \
            --name ${{ env.CONTAINER_ID }}_rust_checks \
            -e SCCACHE_BUCKET=${{ secrets.SCCACHE_S3_BUCKET }} \
            -e SCCACHE_REGION=${{ secrets.AWS_DEFAULT_REGION }} \
            -e AWS_ACCESS_KEY_ID=${{ secrets.AWS_ACCESS_KEY_ID }} \
            -e AWS_SECRET_ACCESS_KEY=${{ secrets.AWS_SECRET_ACCESS_KEY }} \
            ${{ env.IMAGE_TAG }} \
            bash -ec 'ARCH_ALT=x86_64 /workspace/container/use-sccache.sh install && \
                      eval $(/workspace/container/use-sccache.sh setup-env) && \
                      rustup component add rustfmt clippy && \
                      cargo fmt -- --check && \
                      cargo clippy --features block-manager,media-ffmpeg,testing-nixl,integration --no-deps --all-targets -- -D warnings && \
                      cargo test --locked --all-targets --features=block-manager,media-ffmpeg,testing-nixl,integration -- --nocapture && \
                      cargo clippy -p kvbm-physical --no-deps --all-targets -- -D warnings && \
                      cargo test --locked -p kvbm-physical --features testing-kvbm -- --nocapture --test-threads=4 && \
                      /workspace/container/use-sccache.sh show-stats "Rust Checks"'

  mypy:
    needs: [changed-files, build]
    if: >-
      needs.changed-files.outputs.core == 'true' ||
      needs.changed-files.outputs.planner == 'true' ||
      needs.changed-files.outputs.frontend == 'true' ||
      needs.changed-files.outputs.sglang == 'true' ||
      needs.changed-files.outputs.vllm == 'true' ||
      needs.changed-files.outputs.trtllm == 'true'
    runs-on: prod-builder-amd-v1
    name: Mypy
    timeout-minutes: 15
    env:
      IMAGE_TAG: ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ secrets.AWS_DEFAULT_REGION }}.amazonaws.com/ai-dynamo/dynamo:${{ needs.build.outputs.test_tag_suffix }}
    steps:
      - name: Checkout repository
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd  # v6.0.2
      - name: Docker Login
        uses: ./.github/actions/docker-login
        with:
          aws_default_region: ${{ secrets.AWS_DEFAULT_REGION }}
          aws_account_id: ${{ secrets.AWS_ACCOUNT_ID }}
      - name: Pull test image
        run: |
          source ./.github/scripts/retry_docker.sh
          retry_pull ${{ env.IMAGE_TAG }}
      - name: Run mypy
        run: |
          docker run --rm -w /workspace \
            --name mypy_${{ github.run_id }}_${{ github.run_attempt }} \
            ${{ env.IMAGE_TAG }} \
            bash -c '
              MYPYPATH=components/src:lib/bindings/python/src

              # Always check shared (non-backend) code
              TARGETS=$(find components/src/dynamo -maxdepth 1 -mindepth 1 -type d ! -name sglang ! -name vllm ! -name trtllm | sort | tr "\n" " ")

              # Add only the backends that changed
              ${{ needs.changed-files.outputs.sglang == 'true' && 'TARGETS="$TARGETS components/src/dynamo/sglang"' || ':' }}
              ${{ needs.changed-files.outputs.vllm == 'true' && 'TARGETS="$TARGETS components/src/dynamo/vllm"' || ':' }}
              ${{ needs.changed-files.outputs.trtllm == 'true' && 'TARGETS="$TARGETS components/src/dynamo/trtllm"' || ':' }}

              MYPYPATH=$MYPYPATH mypy --explicit-package-bases $TARGETS
            '
          docker run --rm -w /workspace \
            --name mypy_bindings_${{ github.run_id }}_${{ github.run_attempt }} \
            ${{ env.IMAGE_TAG }} \
            bash -c 'MYPYPATH=lib/bindings/python/src mypy -p dynamo'

  test-parallel:
    needs: [changed-files, build, mypy]
    if: needs.changed-files.outputs.core == 'true' || needs.changed-files.outputs.planner == 'true' || needs.changed-files.outputs.frontend == 'true'
    runs-on: prod-builder-amd-v1
    name: Pytest (parallel)
    timeout-minutes: 30
    env:
      IMAGE_TAG: ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ secrets.AWS_DEFAULT_REGION }}.amazonaws.com/ai-dynamo/dynamo:${{ needs.build.outputs.test_tag_suffix }}
    steps:
      - name: Checkout repository
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd  # v6.0.2
      - name: Docker Login
        uses: ./.github/actions/docker-login
        with:
          aws_default_region: ${{ secrets.AWS_DEFAULT_REGION }}
          aws_account_id: ${{ secrets.AWS_ACCOUNT_ID }}
      - name: Pull test image
        run: |
          source ./.github/scripts/retry_docker.sh
          retry_pull ${{ env.IMAGE_TAG }}
      - name: Run pytest (parallel tests with xdist)
        uses: ./.github/actions/pytest
        with:
          image_tag: ${{ env.IMAGE_TAG }}
          pytest_marks: "pre_merge and parallel and not (vllm or sglang or trtllm) and (gpu_0)"
          test_suite_name: dynamo
          test_type: "pre_merge_parallel"
          platform_arch: amd64
          hf_token: ${{ secrets.HF_TOKEN }}
          parallel_mode: '4'
          dind_as_sidecar: 'false'

  test-sequential:
    needs: [changed-files, build, mypy]
    if: needs.changed-files.outputs.core == 'true' || needs.changed-files.outputs.planner == 'true' || needs.changed-files.outputs.frontend == 'true'
    runs-on: prod-builder-amd-v1
    name: Pytest (sequential)
    timeout-minutes: 30
    env:
      IMAGE_TAG: ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ secrets.AWS_DEFAULT_REGION }}.amazonaws.com/ai-dynamo/dynamo:${{ needs.build.outputs.test_tag_suffix }}
    steps:
      - name: Checkout repository
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd  # v6.0.2
      - name: Docker Login
        uses: ./.github/actions/docker-login
        with:
          aws_default_region: ${{ secrets.AWS_DEFAULT_REGION }}
          aws_account_id: ${{ secrets.AWS_ACCOUNT_ID }}
      - name: Pull test image
        run: |
          source ./.github/scripts/retry_docker.sh
          retry_pull ${{ env.IMAGE_TAG }}
      - name: Run pytest (sequential tests)
        uses: ./.github/actions/pytest
        with:
          image_tag: ${{ env.IMAGE_TAG }}
          pytest_marks: "pre_merge and not parallel and not (vllm or sglang or trtllm) and (gpu_0)"
          test_suite_name: dynamo
          test_type: "pre_merge_sequential"
          platform_arch: amd64
          hf_token: ${{ secrets.HF_TOKEN }}
          parallel_mode: 'none'
          dind_as_sidecar: 'false'

  test-generic-gpu:
    needs: [changed-files, build, mypy]
    if: needs.changed-files.outputs.core == 'true'
    runs-on: prod-tester-amd-gpu-v1
    name: Pytest (GPU)
    timeout-minutes: 30
    env:
      IMAGE_TAG: ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ secrets.AWS_DEFAULT_REGION }}.amazonaws.com/ai-dynamo/dynamo:${{ needs.build.outputs.test_tag_suffix }}
    steps:
      - name: Checkout repository
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd  # v6.0.2
      - name: Docker Login
        uses: ./.github/actions/docker-login
        with:
          aws_default_region: ${{ secrets.AWS_DEFAULT_REGION }}
          aws_account_id: ${{ secrets.AWS_ACCOUNT_ID }}
      - name: Pull test image
        run: |
          source ./.github/scripts/retry_docker.sh
          retry_pull ${{ env.IMAGE_TAG }}
      - name: Run pytest (gpu)
        uses: ./.github/actions/pytest
        with:
          image_tag: ${{ env.IMAGE_TAG }}
          pytest_marks: "pre_merge and none and gpu_1"
          framework: dynamo
          test_type: "pre_merge_gpu"
          platform_arch: amd64
          hf_token: ${{ secrets.HF_TOKEN }}
          parallel_mode: 'none'
          dind_as_sidecar: 'true'
