diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 00ee997b1e..e22f2b4a07 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -23,6 +23,7 @@ on: - release/* - "pull-request/[0-9]+" tags: + - "v[0-9]*.[0-9]*.[0-9]*" - "v[0-9][0-9][0-9][0-9].[0-9][0-9].[0-9][0-9]*" - "v[0-9].[0-9].[0-9]-rc[0-9]*" @@ -35,11 +36,58 @@ env: jobs: + changes: + runs-on: ubuntu-latest + outputs: + run_core_ci: ${{ steps.gate.outputs.run_core_ci }} + non_rest_changed: ${{ steps.non-rest-changes.outputs.non_rest }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Detect non-rest changes + id: non-rest-changes + if: startsWith(github.ref, 'refs/heads/pull-request/') + uses: dorny/paths-filter@v3 + with: + base: main + predicate-quantifier: every + filters: | + non_rest: + - '**' + - '!rest-api/**' + - '!.github/workflows/rest-*.yml' + + - name: Decide whether Core CI should run + id: gate + env: + REF: ${{ github.ref }} + COMMIT_MESSAGE: ${{ github.event.head_commit.message || '' }} + NON_REST_CHANGED: ${{ steps.non-rest-changes.outputs.non_rest }} + run: | + run_core_ci=true + + if [[ "${REF}" =~ ^refs/heads/pull-request/[0-9]+$ ]]; then + run_core_ci="${NON_REST_CHANGED}" + fi + + if [[ "${COMMIT_MESSAGE}" =~ ci-run-complete-pipeline ]]; then + run_core_ci=true + fi + + echo "run_core_ci=${run_core_ci}" >> "$GITHUB_OUTPUT" + echo "Core CI gate: ${run_core_ci}" + # ============================================================================ # PREPARE STAGE # ============================================================================ prepare: + needs: + - changes + if: ${{ needs.changes.outputs.run_core_ci == 'true' }} runs-on: linux-amd64-cpu4 outputs: version: ${{ steps.version.outputs.version }} @@ -106,14 +154,11 @@ jobs: set -euo pipefail - # Fetch tags for accurate git describe git fetch --tags --force - # Get short SHA SHORT_SHA=$(git rev-parse --short=7 HEAD) echo "short_sha=${SHORT_SHA}" >> $GITHUB_OUTPUT echo "Using Git describe to extract version as VERSION and HELM_VERSION" VERSION=$(git describe --tags --first-parent --always --long) - # HELM_VERSION strips leading 'v' for strict SemVer and replaces the last '-' with '.' HELM_VERSION_BASE="${VERSION#v}" HELM_VERSION=$(echo "$HELM_VERSION_BASE" | sed 's/\(.*\)-/\1./') @@ -440,7 +485,7 @@ jobs: # BUILD STAGE - Release Container # ============================================================================ build-release-container-x86_64: - if: ${{ always() && github.event_name != 'schedule' }} + if: ${{ always() && github.event_name != 'schedule' && needs.prepare.result == 'success' }} needs: - prepare - build-container-x86_64 @@ -461,7 +506,7 @@ jobs: secrets: inherit build-release-container-aarch64: - if: ${{ always() && github.event_name != 'schedule' }} + if: ${{ always() && github.event_name != 'schedule' && needs.prepare.result == 'success' }} needs: - prepare - build-container-aarch64 @@ -488,7 +533,7 @@ jobs: needs: - prepare - build-artifacts-container-x86_64 - if: ${{ always() && github.event_name != 'schedule' }} + if: ${{ always() && github.event_name != 'schedule' && needs.prepare.result == 'success' }} uses: ./.github/workflows/docker-build.yml with: dockerfile_path: dev/docker/Dockerfile.release-forge-cli @@ -505,7 +550,7 @@ jobs: needs: - prepare - build-artifacts-container-aarch64 - if: ${{ always() && github.event_name != 'schedule' }} + if: ${{ always() && github.event_name != 'schedule' && needs.prepare.result == 'success' }} uses: ./.github/workflows/docker-build.yml with: dockerfile_path: dev/docker/Dockerfile.release-forge-cli @@ -1230,8 +1275,9 @@ jobs: build-summary: runs-on: linux-amd64-cpu4 - if: ${{ always() && github.event_name != 'schedule' }} + if: ${{ always() && github.event_name != 'schedule' && needs.prepare.result == 'success' }} needs: + - prepare - build-container-x86_64 - build-container-aarch64 - build-runtime-container-x86_64 @@ -1383,3 +1429,39 @@ jobs: notify-on-failure: true secrets: slack-bot-token: ${{ secrets.CDS_SLACK_BOT_OAUTH_TOKEN }} + + # ============================================================================ + # AGGREGATOR — single required check for branch protection + # ============================================================================ + # Fails iff any leaf job's result is `failure` or `cancelled`. + # `skipped` counts as pass — that's how rest-only PRs unblock when the + # `changes` gate intentionally skips the core pipeline. + # Scope: 1:1 with the existing ruleset's required checks. Wrapping them in + # a single context lets us require ONE name in branch protection and avoid + # the "Expected — Waiting for status" failure mode if any of those jobs is + # ever renamed or workflow-level-filtered away. + carbide-ci-pass: + name: carbide-ci-pass + runs-on: ubuntu-latest + if: always() + needs: + - build-release-container-x86_64 + - build-release-container-aarch64 + - security-secret-scan + - lint-police + steps: + - name: Decide pass/fail + env: + NEEDS_JSON: ${{ toJson(needs) }} + run: | + set -euo pipefail + echo "$NEEDS_JSON" | jq -r 'to_entries[] | "\(.key): \(.value.result)"' + if echo "$NEEDS_JSON" | jq -e ' + to_entries + | map(select(.value.result == "failure" or .value.result == "cancelled")) + | length > 0 + ' >/dev/null; then + echo "::error::One or more required jobs failed or were cancelled" + exit 1 + fi + echo "All required jobs OK (success or skipped)" diff --git a/.github/workflows/promotion.yaml b/.github/workflows/promotion.yaml index 7b22f169cc..8e10389839 100644 --- a/.github/workflows/promotion.yaml +++ b/.github/workflows/promotion.yaml @@ -20,7 +20,7 @@ on: workflow_dispatch: inputs: version: - description: 'Version to promote (e.g., v0.1.0-rc2-0-g85ed21555)' + description: 'Version to promote (e.g., 1.5.0-85ed215)' required: true type: string diff --git a/.github/workflows/rest-build-binaries.yml b/.github/workflows/rest-build-binaries.yml new file mode 100644 index 0000000000..ca79dcfa51 --- /dev/null +++ b/.github/workflows/rest-build-binaries.yml @@ -0,0 +1,92 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +name: Build Go Binaries + +on: + workflow_dispatch: + workflow_call: + inputs: + runner: + description: 'Runner type for the build job' + required: false + default: 'ubuntu-latest' + type: string + upload_artifact: + description: 'Whether to upload artifacts' + required: false + default: false + type: boolean + +defaults: + run: + working-directory: rest-api + +env: + GO_VERSION: "1.25.4" + +jobs: + build-binaries: + name: Build Binaries (${{ matrix.name }}, ${{ matrix.path }}) + runs-on: ${{ inputs.runner }} + strategy: + fail-fast: false + matrix: + include: + - name: api + path: ./api/cmd/api + - name: migrations + path: ./db/cmd/migrations + - name: sitemgr + path: ./site-manager/cmd/sitemgr + - name: workflow + path: ./workflow/cmd/workflow + - name: site-agent + path: ./site-agent/cmd/site-agent + - name: mock-core + path: ./site-agent/cmd/mock-core + - name: mock-flow + path: ./site-agent/cmd/mock-flow + - name: credsmgr + path: ./cert-manager/cmd/credsmgr + - name: flow + path: ./flow + - name: psm + path: ./powershelf-manager + - name: nsm + path: ./nvswitch-manager + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + cache: true + cache-dependency-path: rest-api/go.sum + + - name: Download dependencies + run: go mod download + + - name: Build ${{ matrix.name }} (linux/amd64) + run: | + mkdir -p dist + GOOS=linux GOARCH=amd64 go build -o dist/${{ matrix.name }}-linux-amd64 ${{ matrix.path }} + + - name: Build ${{ matrix.name }} (linux/arm64) + run: | + GOOS=linux GOARCH=arm64 go build -o dist/${{ matrix.name }}-linux-arm64 ${{ matrix.path }} + + - name: Build ${{ matrix.name }} (darwin/arm64) + run: | + GOOS=darwin GOARCH=arm64 go build -o dist/${{ matrix.name }}-darwin-arm64 ${{ matrix.path }} + + - name: Upload ${{ matrix.name }} binaries + if: inputs.upload_artifact == true + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.name }}-binaries + path: rest-api/dist/${{ matrix.name }}-* + retention-days: 7 diff --git a/.github/workflows/rest-build-push-docker.yml b/.github/workflows/rest-build-push-docker.yml new file mode 100644 index 0000000000..414f454095 --- /dev/null +++ b/.github/workflows/rest-build-push-docker.yml @@ -0,0 +1,383 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +name: Build and Push Docker Images + +on: + workflow_call: + inputs: + runner: + description: "Runner type for the build jobs" + required: false + default: "ubuntu-latest" + type: string + semantic_version: + description: "Semantic version from VERSION file" + required: true + type: string + short_sha: + description: "Short SHA for tagging" + required: true + type: string + branch_sha_tag: + description: "Combined branch name and short SHA tag (branchname-sha)" + required: true + type: string + target_registry: + description: "Target NVCR registry path" + required: true + type: string + push_enabled: + description: "Whether to push images to registry" + required: false + default: true + type: boolean + branch_name: + description: "Sanitized branch name for image tags" + required: true + type: string + is_main_branch: + description: "Whether to push 'latest' and version-short-sha tags (only on main) - pass 'true' or 'false' as string" + required: false + default: "false" + type: string + release_tag: + description: "Release tag for the build" + required: false + default: "" + type: string + secrets: + NVCR_USERNAME: + description: "NVIDIA Container Registry username (typically '$oauthtoken')" + required: false + NVCR_TOKEN: + description: "NVIDIA Container Registry API token" + required: false + +jobs: + build-nico-rest-api: + name: Build nico-rest-api + uses: ./.github/workflows/rest-build-push-service.yml + with: + runner: ${{ inputs.runner }} + service_name: nico-rest-api + binary_name: api + binary_path: /app/api + dockerfile: ./rest-api/docker/production/Dockerfile.nico-rest-api + semantic_version: ${{ inputs.semantic_version }} + short_sha: ${{ inputs.short_sha }} + branch_sha_tag: ${{ inputs.branch_sha_tag }} + target_registry: ${{ inputs.target_registry }} + push_enabled: ${{ inputs.push_enabled }} + is_main_branch: ${{ inputs.is_main_branch }} + release_tag: ${{ inputs.release_tag }} + secrets: inherit + + build-nico-rest-db: + name: Build nico-rest-db + uses: ./.github/workflows/rest-build-push-service.yml + with: + runner: ${{ inputs.runner }} + service_name: nico-rest-db + binary_name: migrations + binary_path: /app/migrations + dockerfile: ./rest-api/docker/production/Dockerfile.nico-rest-db + semantic_version: ${{ inputs.semantic_version }} + short_sha: ${{ inputs.short_sha }} + branch_sha_tag: ${{ inputs.branch_sha_tag }} + target_registry: ${{ inputs.target_registry }} + push_enabled: ${{ inputs.push_enabled }} + is_main_branch: ${{ inputs.is_main_branch }} + release_tag: ${{ inputs.release_tag }} + secrets: inherit + + build-nico-rest-site-manager: + name: Build nico-rest-site-manager + uses: ./.github/workflows/rest-build-push-service.yml + with: + runner: ${{ inputs.runner }} + service_name: nico-rest-site-manager + binary_name: sitemgr + binary_path: /app/sitemgr + dockerfile: ./rest-api/docker/production/Dockerfile.nico-rest-site-manager + semantic_version: ${{ inputs.semantic_version }} + short_sha: ${{ inputs.short_sha }} + branch_sha_tag: ${{ inputs.branch_sha_tag }} + target_registry: ${{ inputs.target_registry }} + push_enabled: ${{ inputs.push_enabled }} + is_main_branch: ${{ inputs.is_main_branch }} + release_tag: ${{ inputs.release_tag }} + secrets: inherit + + build-nico-rest-workflow: + name: Build nico-rest-workflow + uses: ./.github/workflows/rest-build-push-service.yml + with: + runner: ${{ inputs.runner }} + service_name: nico-rest-workflow + binary_name: workflow + binary_path: /app/workflow + dockerfile: ./rest-api/docker/production/Dockerfile.nico-rest-workflow + semantic_version: ${{ inputs.semantic_version }} + short_sha: ${{ inputs.short_sha }} + branch_sha_tag: ${{ inputs.branch_sha_tag }} + target_registry: ${{ inputs.target_registry }} + push_enabled: ${{ inputs.push_enabled }} + is_main_branch: ${{ inputs.is_main_branch }} + release_tag: ${{ inputs.release_tag }} + secrets: inherit + + build-nico-rest-site-agent: + name: Build nico-rest-site-agent + uses: ./.github/workflows/rest-build-push-service.yml + with: + runner: ${{ inputs.runner }} + service_name: nico-rest-site-agent + binary_name: site-agent + binary_path: /app/site-agent + dockerfile: ./rest-api/docker/production/Dockerfile.nico-rest-site-agent + semantic_version: ${{ inputs.semantic_version }} + short_sha: ${{ inputs.short_sha }} + branch_sha_tag: ${{ inputs.branch_sha_tag }} + target_registry: ${{ inputs.target_registry }} + push_enabled: ${{ inputs.push_enabled }} + is_main_branch: ${{ inputs.is_main_branch }} + release_tag: ${{ inputs.release_tag }} + secrets: inherit + + build-nico-rest-cert-manager: + name: Build nico-rest-cert-manager + uses: ./.github/workflows/rest-build-push-service.yml + with: + runner: ${{ inputs.runner }} + service_name: nico-rest-cert-manager + binary_name: credsmgr + binary_path: /app/credsmgr + dockerfile: ./rest-api/docker/production/Dockerfile.nico-rest-cert-manager + semantic_version: ${{ inputs.semantic_version }} + short_sha: ${{ inputs.short_sha }} + branch_sha_tag: ${{ inputs.branch_sha_tag }} + target_registry: ${{ inputs.target_registry }} + push_enabled: ${{ inputs.push_enabled }} + is_main_branch: ${{ inputs.is_main_branch }} + release_tag: ${{ inputs.release_tag }} + secrets: inherit + + build-nico-flow: + name: Build nico-flow + uses: ./.github/workflows/rest-build-push-service.yml + with: + runner: ${{ inputs.runner }} + service_name: nico-flow + binary_name: flow + binary_path: /app/flow + dockerfile: ./rest-api/docker/production/Dockerfile.nico-flow + semantic_version: ${{ inputs.semantic_version }} + short_sha: ${{ inputs.short_sha }} + branch_sha_tag: ${{ inputs.branch_sha_tag }} + target_registry: ${{ inputs.target_registry }} + push_enabled: ${{ inputs.push_enabled }} + is_main_branch: ${{ inputs.is_main_branch }} + release_tag: ${{ inputs.release_tag }} + secrets: inherit + + build-nico-psm: + name: Build nico-psm + uses: ./.github/workflows/rest-build-push-service.yml + with: + runner: ${{ inputs.runner }} + service_name: nico-psm + binary_name: psm + binary_path: /app/psm + dockerfile: ./rest-api/docker/production/Dockerfile.nico-psm + semantic_version: ${{ inputs.semantic_version }} + short_sha: ${{ inputs.short_sha }} + branch_sha_tag: ${{ inputs.branch_sha_tag }} + target_registry: ${{ inputs.target_registry }} + push_enabled: ${{ inputs.push_enabled }} + is_main_branch: ${{ inputs.is_main_branch }} + release_tag: ${{ inputs.release_tag }} + secrets: inherit + + build-nico-nsm: + name: Build nico-nsm + uses: ./.github/workflows/rest-build-push-service.yml + with: + runner: ${{ inputs.runner }} + service_name: nico-nsm + binary_name: nsm + binary_path: /app/nsm + dockerfile: ./rest-api/docker/production/Dockerfile.nico-nsm + semantic_version: ${{ inputs.semantic_version }} + short_sha: ${{ inputs.short_sha }} + branch_sha_tag: ${{ inputs.branch_sha_tag }} + target_registry: ${{ inputs.target_registry }} + push_enabled: ${{ inputs.push_enabled }} + is_main_branch: ${{ inputs.is_main_branch }} + release_tag: ${{ inputs.release_tag }} + secrets: inherit + + build-summary: + name: Build Summary + runs-on: ${{ inputs.runner }} + needs: + - build-nico-rest-api + - build-nico-rest-db + - build-nico-rest-site-manager + - build-nico-rest-workflow + - build-nico-rest-site-agent + - build-nico-rest-cert-manager + - build-nico-flow + - build-nico-psm + - build-nico-nsm + if: always() + outputs: + build_artifacts: ${{ steps.aggregate.outputs.artifacts }} + + steps: + - name: Generate build summary + run: | + echo "# Docker Build Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "## Build Information" >> $GITHUB_STEP_SUMMARY + echo "- **Semantic Version**: \`${{ inputs.semantic_version }}\`" >> $GITHUB_STEP_SUMMARY + echo "- **Git SHA**: \`${{ inputs.short_sha }}\`" >> $GITHUB_STEP_SUMMARY + echo "- **Branch**: \`${{ inputs.branch_name }}\`" >> $GITHUB_STEP_SUMMARY + echo "- **Branch-SHA Tag**: \`${{ inputs.branch_sha_tag }}\`" >> $GITHUB_STEP_SUMMARY + echo "- **Registry**: \`${{ inputs.target_registry }}\`" >> $GITHUB_STEP_SUMMARY + echo "- **Push Enabled**: \`${{ inputs.push_enabled }}\`" >> $GITHUB_STEP_SUMMARY + echo "- **Is Main Branch**: \`${{ inputs.is_main_branch }}\`" >> $GITHUB_STEP_SUMMARY + echo "- **Ref Name**: \`${{ github.ref_name }}\`" >> $GITHUB_STEP_SUMMARY + echo "- **Platforms**: \`linux/amd64, linux/arm64\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${{ inputs.push_enabled }}" = "false" ]; then + echo "> **Note**: Images were built but NOT pushed to registry (build-only mode)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + fi + echo "## Docker Image Tags" >> $GITHUB_STEP_SUMMARY + echo "All images tagged with: \`${{ inputs.branch_sha_tag }}\`" >> $GITHUB_STEP_SUMMARY + if [ "${{ inputs.is_main_branch }}" = "true" ] || [ "${{ github.ref_name }}" = "main" ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Main branch additional tags:**" >> $GITHUB_STEP_SUMMARY + echo "- \`${{ inputs.semantic_version }}\`" >> $GITHUB_STEP_SUMMARY + echo "- \`latest\`" >> $GITHUB_STEP_SUMMARY + fi + if [ "${{ inputs.release_tag }}" != "" ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Release tag additional tags:**" >> $GITHUB_STEP_SUMMARY + echo "- \`${{ inputs.release_tag }}\`" >> $GITHUB_STEP_SUMMARY + fi + echo "" >> $GITHUB_STEP_SUMMARY + echo "## Images Built" >> $GITHUB_STEP_SUMMARY + echo "1. \`nico-rest-api:${{ inputs.branch_sha_tag }}\` (amd64 + arm64)" >> $GITHUB_STEP_SUMMARY + echo "2. \`nico-rest-db:${{ inputs.branch_sha_tag }}\` (amd64 + arm64)" >> $GITHUB_STEP_SUMMARY + echo "3. \`nico-rest-site-manager:${{ inputs.branch_sha_tag }}\` (amd64 + arm64)" >> $GITHUB_STEP_SUMMARY + echo "4. \`nico-rest-workflow:${{ inputs.branch_sha_tag }}\` (amd64 + arm64)" >> $GITHUB_STEP_SUMMARY + echo "5. \`nico-rest-site-agent:${{ inputs.branch_sha_tag }}\` (amd64 + arm64)" >> $GITHUB_STEP_SUMMARY + echo "6. \`nico-rest-cert-manager:${{ inputs.branch_sha_tag }}\` (amd64 + arm64)" >> $GITHUB_STEP_SUMMARY + echo "7. \`nico-flow:${{ inputs.branch_sha_tag }}\` (amd64 + arm64)" >> $GITHUB_STEP_SUMMARY + echo "8. \`nico-psm:${{ inputs.branch_sha_tag }}\` (amd64 + arm64)" >> $GITHUB_STEP_SUMMARY + echo "9. \`nico-nsm:${{ inputs.branch_sha_tag }}\` (amd64 + arm64)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${{ inputs.push_enabled }}" = "true" ]; then + echo "## Binary Artifacts Uploaded" >> $GITHUB_STEP_SUMMARY + echo "Each binary uploaded for both amd64 and arm64 with the publish build version." >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + fi + echo "## Job Status" >> $GITHUB_STEP_SUMMARY + echo "- nico-rest-api: \`${{ needs.build-nico-rest-api.result }}\`" >> $GITHUB_STEP_SUMMARY + echo "- nico-rest-db: \`${{ needs.build-nico-rest-db.result }}\`" >> $GITHUB_STEP_SUMMARY + echo "- nico-rest-site-manager: \`${{ needs.build-nico-rest-site-manager.result }}\`" >> $GITHUB_STEP_SUMMARY + echo "- nico-rest-workflow: \`${{ needs.build-nico-rest-workflow.result }}\`" >> $GITHUB_STEP_SUMMARY + echo "- nico-rest-site-agent: \`${{ needs.build-nico-rest-site-agent.result }}\`" >> $GITHUB_STEP_SUMMARY + echo "- nico-rest-cert-manager: \`${{ needs.build-nico-rest-cert-manager.result }}\`" >> $GITHUB_STEP_SUMMARY + echo "- nico-flow: \`${{ needs.build-nico-flow.result }}\`" >> $GITHUB_STEP_SUMMARY + echo "- nico-psm: \`${{ needs.build-nico-psm.result }}\`" >> $GITHUB_STEP_SUMMARY + echo "- nico-nsm: \`${{ needs.build-nico-nsm.result }}\`" >> $GITHUB_STEP_SUMMARY + + - name: Aggregate outputs + id: aggregate + run: | + cat < artifacts.json + [ + { + "service": "nico-rest-api", + "image_ref": "${{ needs.build-nico-rest-api.outputs.image_ref }}", + "binary_resource": "${{ needs.build-nico-rest-api.outputs.ngc_binary_resource }}", + "image_resource": "${{ needs.build-nico-rest-api.outputs.ngc_image_resource }}" + }, + { + "service": "nico-rest-db", + "image_ref": "${{ needs.build-nico-rest-db.outputs.image_ref }}", + "binary_resource": "${{ needs.build-nico-rest-db.outputs.ngc_binary_resource }}", + "image_resource": "${{ needs.build-nico-rest-db.outputs.ngc_image_resource }}" + }, + { + "service": "nico-rest-site-manager", + "image_ref": "${{ needs.build-nico-rest-site-manager.outputs.image_ref }}", + "binary_resource": "${{ needs.build-nico-rest-site-manager.outputs.ngc_binary_resource }}", + "image_resource": "${{ needs.build-nico-rest-site-manager.outputs.ngc_image_resource }}" + }, + { + "service": "nico-rest-workflow", + "image_ref": "${{ needs.build-nico-rest-workflow.outputs.image_ref }}", + "binary_resource": "${{ needs.build-nico-rest-workflow.outputs.ngc_binary_resource }}", + "image_resource": "${{ needs.build-nico-rest-workflow.outputs.ngc_image_resource }}" + }, + { + "service": "nico-rest-site-agent", + "image_ref": "${{ needs.build-nico-rest-site-agent.outputs.image_ref }}", + "binary_resource": "${{ needs.build-nico-rest-site-agent.outputs.ngc_binary_resource }}", + "image_resource": "${{ needs.build-nico-rest-site-agent.outputs.ngc_image_resource }}" + }, + { + "service": "nico-rest-cert-manager", + "image_ref": "${{ needs.build-nico-rest-cert-manager.outputs.image_ref }}", + "binary_resource": "${{ needs.build-nico-rest-cert-manager.outputs.ngc_binary_resource }}", + "image_resource": "${{ needs.build-nico-rest-cert-manager.outputs.ngc_image_resource }}" + }, + { + "service": "nico-flow", + "image_ref": "${{ needs.build-nico-flow.outputs.image_ref }}", + "binary_resource": "${{ needs.build-nico-flow.outputs.ngc_binary_resource }}", + "image_resource": "${{ needs.build-nico-flow.outputs.ngc_image_resource }}" + }, + { + "service": "nico-psm", + "image_ref": "${{ needs.build-nico-psm.outputs.image_ref }}", + "binary_resource": "${{ needs.build-nico-psm.outputs.ngc_binary_resource }}", + "image_resource": "${{ needs.build-nico-psm.outputs.ngc_image_resource }}" + }, + { + "service": "nico-nsm", + "image_ref": "${{ needs.build-nico-nsm.outputs.image_ref }}", + "binary_resource": "${{ needs.build-nico-nsm.outputs.ngc_binary_resource }}", + "image_resource": "${{ needs.build-nico-nsm.outputs.ngc_image_resource }}" + } + ] + EOF + + ARTIFACTS=$(jq -c . artifacts.json) + echo "artifacts=$ARTIFACTS" >> $GITHUB_OUTPUT + + security-container-scan-summary: + name: Container Scan Summary + runs-on: ${{ inputs.runner }} + needs: + - build-nico-rest-api + - build-nico-rest-db + - build-nico-rest-site-manager + - build-nico-rest-workflow + - build-nico-rest-site-agent + - build-nico-rest-cert-manager + - build-nico-flow + - build-nico-psm + - build-nico-nsm + if: always() + permissions: + contents: read + pull-requests: write + steps: + - uses: NVIDIA/dsx-github-actions/.github/actions/security-container-scan-aggregate@739847ddf00fda38916504ef84e1f504eac3158f + with: + post-pr-comment: 'true' diff --git a/.github/workflows/rest-build-push-service.yml b/.github/workflows/rest-build-push-service.yml new file mode 100644 index 0000000000..e6c4ebb3bf --- /dev/null +++ b/.github/workflows/rest-build-push-service.yml @@ -0,0 +1,264 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +name: Build and Push Service + +on: + workflow_call: + inputs: + runner: + description: 'Runner type for the build job' + required: false + default: 'ubuntu-latest' + type: string + service_name: + description: 'Service name (e.g., nico-rest-api)' + required: true + type: string + binary_name: + description: 'Binary name in artifacts (e.g., api, migrations, sitemgr)' + required: true + type: string + binary_path: + description: 'Path to binary in container (e.g., /app/api)' + required: true + type: string + dockerfile: + description: 'Path to Dockerfile' + required: true + type: string + semantic_version: + description: 'Semantic version from VERSION file' + required: true + type: string + short_sha: + description: 'Short SHA for tagging' + required: true + type: string + branch_sha_tag: + description: 'Combined branch name and short SHA tag' + required: true + type: string + target_registry: + description: 'Target NVCR registry path' + required: true + type: string + push_enabled: + description: 'Whether to push images to registry' + required: true + type: boolean + is_main_branch: + description: 'Whether this is the main branch' + required: true + type: string + release_tag: + description: 'Release tag for the build' + required: false + default: "" + type: string + ngc_path: + description: 'NGC path for resource uploads' + required: false + default: '0837451325059433/carbide-dev' + type: string + secrets: + NVCR_USERNAME: + description: 'NVCR username' + required: false + NVCR_TOKEN: + description: 'NVCR token' + required: false + +jobs: + build: + name: Build ${{ inputs.service_name }} + runs-on: ${{ inputs.runner }} + permissions: + contents: read + packages: write + security-events: write + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up QEMU for multi-architecture builds + uses: docker/setup-qemu-action@v3 + with: + platforms: linux/amd64,linux/arm64 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to NVCR + uses: docker/login-action@v3 + with: + registry: nvcr.io + username: ${{ secrets.NVCR_USERNAME }} + password: ${{ secrets.NVCR_TOKEN }} + + - name: Build amd64 image for Grype scan (load to daemon) + id: scan-build + continue-on-error: true + uses: docker/build-push-action@v5 + with: + context: ./rest-api + file: ${{ inputs.dockerfile }} + platforms: linux/amd64 + push: false + load: true + tags: localbuild/scan-${{ inputs.service_name }}:${{ github.run_id }}-${{ github.run_attempt }} + cache-from: type=gha,scope=${{ inputs.service_name }} + cache-to: type=gha,mode=max,scope=${{ inputs.service_name }} + + - name: Grype vulnerability scan + if: steps.scan-build.outcome == 'success' + continue-on-error: true + uses: NVIDIA/dsx-github-actions/.github/actions/security-container-scan@739847ddf00fda38916504ef84e1f504eac3158f + with: + image: localbuild/scan-${{ inputs.service_name }}:${{ github.run_id }}-${{ github.run_attempt }} + fail-on: critical + fail-build: 'false' + write-summary: 'false' + upload-sarif: ${{ github.ref == 'refs/heads/main' && 'true' || 'false' }} + sarif-category: grype-${{ inputs.service_name }} + artifact-name: grype-${{ inputs.service_name }}-${{ github.run_id }}-${{ github.run_attempt }} + sbom-artifact-name: sbom-${{ inputs.service_name }}-${{ github.run_id }}-${{ github.run_attempt }} + + - name: Determine Docker tags + id: docker-tags + run: | + TAGS="" + PRIMARY_TAG="" + ARTIFACT_VERSION="${{ inputs.semantic_version }}-${{ inputs.short_sha }}" + + if [ "${{ inputs.is_main_branch }}" == "true" ]; then + PRIMARY_TAG="${{ inputs.semantic_version }}-${{ inputs.short_sha }}" + TAGS="${TAGS},${{ inputs.target_registry }}/${{ inputs.service_name }}:${PRIMARY_TAG}" + TAGS="${TAGS},${{ inputs.target_registry }}/${{ inputs.service_name }}:latest" + elif [ "${{ inputs.release_tag }}" != "" ]; then + PRIMARY_TAG="${{ inputs.release_tag }}" + ARTIFACT_VERSION="${{ inputs.release_tag }}" + TAGS="${TAGS},${{ inputs.target_registry }}/${{ inputs.service_name }}:${PRIMARY_TAG}" + else + PRIMARY_TAG="${{ inputs.branch_sha_tag }}" + TAGS="${TAGS},${{ inputs.target_registry }}/${{ inputs.service_name }}:${PRIMARY_TAG}" + fi + + TAGS="${TAGS#,}" + echo "tags=$TAGS" >> $GITHUB_OUTPUT + echo "primary_tag=$PRIMARY_TAG" >> $GITHUB_OUTPUT + echo "artifact_version=$ARTIFACT_VERSION" >> $GITHUB_OUTPUT + + - name: Build and push ${{ inputs.service_name }} (multi-arch) + uses: docker/build-push-action@v5 + with: + context: ./rest-api + file: ${{ inputs.dockerfile }} + platforms: linux/amd64,linux/arm64 + push: ${{ inputs.push_enabled }} + tags: ${{ steps.docker-tags.outputs.tags }} + cache-from: type=gha,scope=${{ inputs.service_name }} + cache-to: type=gha,mode=max,scope=${{ inputs.service_name }} + labels: | + org.opencontainers.image.title=${{ inputs.service_name }} + org.opencontainers.image.version=${{ inputs.semantic_version }} + org.opencontainers.image.revision=${{ inputs.short_sha }} + org.opencontainers.image.created=${{ github.event.head_commit.timestamp }} + org.opencontainers.image.source=${{ github.repositoryUrl }} + org.opencontainers.image.url=${{ github.repositoryUrl }} + + - name: Extract amd64 binary from image + if: inputs.push_enabled == true + run: | + mkdir -p artifacts + docker pull --platform linux/amd64 ${{ inputs.target_registry }}/${{ inputs.service_name }}:${{ steps.docker-tags.outputs.primary_tag }} + docker create --name temp-container --platform linux/amd64 ${{ inputs.target_registry }}/${{ inputs.service_name }}:${{ steps.docker-tags.outputs.primary_tag }} + docker cp temp-container:${{ inputs.binary_path }} artifacts/${{ inputs.binary_name }}-amd64 + docker rm temp-container + chmod +x artifacts/${{ inputs.binary_name }}-amd64 + + - name: Extract arm64 binary from image + if: inputs.push_enabled == true + run: | + docker pull --platform linux/arm64 ${{ inputs.target_registry }}/${{ inputs.service_name }}:${{ steps.docker-tags.outputs.primary_tag }} + docker create --name temp-container-arm --platform linux/arm64 ${{ inputs.target_registry }}/${{ inputs.service_name }}:${{ steps.docker-tags.outputs.primary_tag }} + docker cp temp-container-arm:${{ inputs.binary_path }} artifacts/${{ inputs.binary_name }}-arm64 + docker rm temp-container-arm + chmod +x artifacts/${{ inputs.binary_name }}-arm64 + + - name: Export Docker image + if: inputs.push_enabled == true + run: | + docker save ${{ inputs.target_registry }}/${{ inputs.service_name }}:${{ steps.docker-tags.outputs.primary_tag }} | gzip > artifacts/${{ inputs.service_name }}-${{ steps.docker-tags.outputs.artifact_version }}.tar.gz + + - name: Upload binary artifact with semantic version + if: inputs.push_enabled == true + uses: NVIDIA/dsx-github-actions/.github/actions/resource-push-ngc@47c68bf27edde19d1acece9b7721b4a1a0044dfa + with: + name: ${{ inputs.service_name }}-binary-amd64 + display-name: ${{ inputs.service_name }}-binary-amd64 + description: ${{ inputs.service_name }} binary (amd64) + version: ${{ steps.docker-tags.outputs.artifact_version }} + path: artifacts/${{ inputs.binary_name }}-amd64 + ngc-path: ${{ inputs.ngc_path }} + ngc-key: ${{ secrets.NVCR_TOKEN }} + + - name: Upload binary artifact with latest tag + if: inputs.is_main_branch == 'true' && inputs.push_enabled == true + uses: NVIDIA/dsx-github-actions/.github/actions/resource-push-ngc@47c68bf27edde19d1acece9b7721b4a1a0044dfa + with: + name: ${{ inputs.service_name }}-binary-amd64 + display-name: ${{ inputs.service_name }}-binary-amd64 + description: ${{ inputs.service_name }} binary (amd64) + version: latest + path: artifacts/${{ inputs.binary_name }}-amd64 + ngc-path: ${{ inputs.ngc_path }} + ngc-key: ${{ secrets.NVCR_TOKEN }} + + - name: Upload arm64 binary artifact with semantic version + if: inputs.push_enabled == true + uses: NVIDIA/dsx-github-actions/.github/actions/resource-push-ngc@47c68bf27edde19d1acece9b7721b4a1a0044dfa + with: + name: ${{ inputs.service_name }}-binary-arm64 + display-name: ${{ inputs.service_name }}-binary-arm64 + description: ${{ inputs.service_name }} binary (arm64) + version: ${{ steps.docker-tags.outputs.artifact_version }} + path: artifacts/${{ inputs.binary_name }}-arm64 + ngc-path: ${{ inputs.ngc_path }} + ngc-key: ${{ secrets.NVCR_TOKEN }} + + - name: Upload arm64 binary artifact with latest tag + if: inputs.is_main_branch == 'true' && inputs.push_enabled == true + uses: NVIDIA/dsx-github-actions/.github/actions/resource-push-ngc@47c68bf27edde19d1acece9b7721b4a1a0044dfa + with: + name: ${{ inputs.service_name }}-binary-arm64 + display-name: ${{ inputs.service_name }}-binary-arm64 + description: ${{ inputs.service_name }} binary (arm64) + version: latest + path: artifacts/${{ inputs.binary_name }}-arm64 + ngc-path: ${{ inputs.ngc_path }} + ngc-key: ${{ secrets.NVCR_TOKEN }} + + - name: Upload Docker image artifact with semantic version + if: inputs.push_enabled == true + uses: NVIDIA/dsx-github-actions/.github/actions/resource-push-ngc@47c68bf27edde19d1acece9b7721b4a1a0044dfa + with: + name: ${{ inputs.service_name }}-image + display-name: ${{ inputs.service_name }}-image + description: ${{ inputs.service_name }} image + version: ${{ steps.docker-tags.outputs.artifact_version }} + path: artifacts/${{ inputs.service_name }}-${{ steps.docker-tags.outputs.artifact_version }}.tar.gz + ngc-path: ${{ inputs.ngc_path }} + ngc-key: ${{ secrets.NVCR_TOKEN }} + + - name: Upload Docker image artifact with latest tag + if: inputs.is_main_branch == 'true' && inputs.push_enabled == true + uses: NVIDIA/dsx-github-actions/.github/actions/resource-push-ngc@47c68bf27edde19d1acece9b7721b4a1a0044dfa + with: + name: ${{ inputs.service_name }}-image + display-name: ${{ inputs.service_name }}-image + description: ${{ inputs.service_name }} image + version: latest + path: artifacts/${{ inputs.service_name }}-${{ inputs.semantic_version }}-${{ inputs.short_sha }}.tar.gz + ngc-path: ${{ inputs.ngc_path }} + ngc-key: ${{ secrets.NVCR_TOKEN }} diff --git a/.github/workflows/rest-ci.yml b/.github/workflows/rest-ci.yml new file mode 100644 index 0000000000..4e8f5af51c --- /dev/null +++ b/.github/workflows/rest-ci.yml @@ -0,0 +1,179 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +name: NICo REST CI + +on: + workflow_dispatch: + push: + branches: + - main + - release/* + - 'pull-request/[0-9]+' + tags: + - "v[0-9]*.[0-9]*.[0-9]*" + - "v[0-9][0-9][0-9][0-9].[0-9][0-9].[0-9][0-9]*" + - "v[0-9].[0-9].[0-9]-rc[0-9]*" + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + changes: + name: Detect REST CI Gate + runs-on: ubuntu-latest + outputs: + run_rest_ci: ${{ steps.gate.outputs.run_rest_ci }} + rest_api_changed: ${{ steps.filter.outputs.rest_api }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Detect rest-api changes + id: filter + if: startsWith(github.ref, 'refs/heads/pull-request/') + uses: dorny/paths-filter@v3 + with: + base: main + filters: | + rest_api: + - 'rest-api/**' + - '.github/workflows/rest-*.yml' + + - name: Decide whether REST CI should run + id: gate + env: + REF: ${{ github.ref }} + COMMIT_MESSAGE: ${{ github.event.head_commit.message || '' }} + REST_API_CHANGED: ${{ steps.filter.outputs.rest_api }} + run: | + run_rest_ci=true + + if [[ "${REF}" =~ ^refs/heads/pull-request/[0-9]+$ ]]; then + run_rest_ci="${REST_API_CHANGED:-false}" + fi + + if [[ "${COMMIT_MESSAGE}" =~ ci-run-complete-pipeline ]]; then + run_rest_ci=true + fi + + echo "run_rest_ci=${run_rest_ci}" >> "$GITHUB_OUTPUT" + echo "REST CI gate: ${run_rest_ci}" + + prepare: + name: Prepare Build Info + needs: + - changes + if: ${{ needs.changes.outputs.run_rest_ci == 'true' }} + uses: ./.github/workflows/rest-prepare-build-info.yml + with: + runner: ubuntu-latest + + lint-and-test: + name: Lint and Test + needs: prepare + uses: ./.github/workflows/rest-lint-and-test.yml + + build-binaries: + name: Build Go Binaries + needs: + - prepare + - lint-and-test + with: + upload_artifact: true + uses: ./.github/workflows/rest-build-binaries.yml + + security-secret-scan: + name: REST Secret Scan with TruffleHog + needs: prepare + runs-on: linux-amd64-cpu4 + timeout-minutes: 30 + permissions: + actions: read + contents: read + pull-requests: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Run TruffleHog Scan + uses: NVIDIA/dsx-github-actions/.github/actions/trufflehog-scan@f435aa6bf125fe6f9e5ac438f8cef75f90e29a2b + with: + extra-args: '--results=verified,unknown --only-verified' + post-pr-comment: 'true' + fail-on-findings: 'true' + + build-and-push: + name: Build and Push Docker Images + needs: + - prepare + - lint-and-test + permissions: + contents: read + packages: write + pull-requests: write + security-events: write + uses: ./.github/workflows/rest-build-push-docker.yml + with: + runner: ubuntu-latest + semantic_version: ${{ needs.prepare.outputs.semantic_version }} + short_sha: ${{ needs.prepare.outputs.short_sha }} + branch_sha_tag: ${{ needs.prepare.outputs.branch_sha_tag }} + target_registry: ${{ needs.prepare.outputs.target_registry }} + branch_name: ${{ needs.prepare.outputs.branch_name }} + is_main_branch: ${{ needs.prepare.outputs.is_main_branch }} + push_enabled: ${{ github.event_name != 'workflow_dispatch' && !contains(github.ref, 'pull-request/') }} + release_tag: ${{ needs.prepare.outputs.release_tag }} + secrets: + NVCR_USERNAME: ${{ secrets.NVCR_USERNAME }} + NVCR_TOKEN: ${{ secrets.NVCR_TOKEN }} + + helm: + name: Helm Charts + needs: + - prepare + if: ${{ !cancelled() && needs.prepare.result == 'success' }} + uses: ./.github/workflows/rest-helm-workflows.yml + with: + app_version: ${{ needs.prepare.outputs.semantic_version }} + secrets: + NVCR_STG_TOKEN: ${{ secrets.NVCR_TOKEN }} + + # ============================================================================ + # AGGREGATOR — single required check for branch protection + # ============================================================================ + # Fails iff any leaf job's result is `failure` or `cancelled`. + # `skipped` counts as pass — that's how core-only PRs unblock when the + # `changes` gate intentionally skips the REST pipeline. + # Scope: mirrors core's 4-check coverage — lint, security, container build, + # and helm packaging. Tighten later by adding prepare / build-binaries / + # changes if regressions appear. + rest-ci-pass: + name: rest-ci-pass + runs-on: ubuntu-latest + if: always() + needs: + - lint-and-test + - security-secret-scan + - build-and-push + - helm + steps: + - name: Decide pass/fail + env: + NEEDS_JSON: ${{ toJson(needs) }} + run: | + set -euo pipefail + echo "$NEEDS_JSON" | jq -r 'to_entries[] | "\(.key): \(.value.result)"' + if echo "$NEEDS_JSON" | jq -e ' + to_entries + | map(select(.value.result == "failure" or .value.result == "cancelled")) + | length > 0 + ' >/dev/null; then + echo "::error::One or more required jobs failed or were cancelled" + exit 1 + fi + echo "All required jobs OK (success or skipped)" diff --git a/.github/workflows/rest-helm-workflows.yml b/.github/workflows/rest-helm-workflows.yml new file mode 100644 index 0000000000..aa60ccb603 --- /dev/null +++ b/.github/workflows/rest-helm-workflows.yml @@ -0,0 +1,135 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +name: Helm Charts + +on: + workflow_call: + inputs: + app_version: + description: "Application version for Helm chart appVersion" + required: true + type: string + secrets: + NVCR_STG_TOKEN: + required: true + +jobs: + detect-changes: + name: Detect Helm Chart Changes + runs-on: ubuntu-latest + outputs: + any_changed: ${{ steps.changes.outputs.any_changed }} + nico_rest_changed: ${{ steps.changes.outputs.nico_rest }} + site_agent_changed: ${{ steps.changes.outputs.site_agent }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Check for helm/ changes + uses: dorny/paths-filter@v3 + id: changes + with: + filters: | + any_changed: + - 'rest-api/helm/**' + nico_rest: + - 'rest-api/helm/charts/nico-rest/**' + site_agent: + - 'rest-api/helm/charts/nico-rest-site-agent/**' + + validate-versions: + name: Validate Helm Chart Versions + needs: + - detect-changes + if: ${{ needs.detect-changes.outputs.any_changed == 'true' && contains(github.ref, 'pull-request/') }} + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Verify Chart.yaml versions are updated + run: | + FAILED=false + check_version() { + local chart_dir="$1" changed="$2" + if [ "$changed" != "true" ]; then + echo "⊘ ${chart_dir}: no changes, skipping version check" + return + fi + local chart_yaml="${chart_dir}/Chart.yaml" + local current_version=$(grep '^version:' "$chart_yaml" | awk '{print $2}') + local base_version=$(git show origin/main:"$chart_yaml" 2>/dev/null | grep '^version:' | awk '{print $2}' || echo "") + if [ -n "$base_version" ] && [ "$current_version" = "$base_version" ]; then + echo "::error file=${chart_yaml}::Chart version ($current_version) was not updated. Please bump the version in $chart_yaml" + FAILED=true + else + echo "✓ ${chart_dir}: version updated to $current_version" + fi + } + check_version "rest-api/helm/charts/nico-rest" "${{ needs.detect-changes.outputs.nico_rest_changed }}" + check_version "rest-api/helm/charts/nico-rest-site-agent" "${{ needs.detect-changes.outputs.site_agent_changed }}" + if [ "$FAILED" = "true" ]; then + exit 1 + fi + + validate-charts: + name: Validate Helm Charts + needs: + - detect-changes + - validate-versions + if: ${{ always() && needs.detect-changes.outputs.any_changed == 'true' && (needs.validate-versions.result == 'success' || needs.validate-versions.result == 'skipped') }} + runs-on: ubuntu-latest + strategy: + matrix: + include: + - chart: rest-api/helm/charts/nico-rest + valueOverrides: '["nico-rest-api.config.keycloak.enabled=true","nico-rest-api.config.keycloak.baseURL=http://keycloak:8082","nico-rest-api.config.keycloak.realm=test","nico-rest-api.config.keycloak.clientID=test"]' + - chart: rest-api/helm/charts/nico-rest-site-agent + valueOverrides: '[]' + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Validate Helm chart + uses: NVIDIA/dsx-github-actions/.github/actions/helm-validate@94bde998f5d7965576b0c663db7d5d709c918167 + with: + chart-path: ${{ matrix.chart }} + lint: 'true' + template: 'true' + valueOverrides: ${{ matrix.valueOverrides }} + + push-charts: + name: Push Helm Charts + needs: + - detect-changes + - validate-charts + if: false + runs-on: ubuntu-latest + strategy: + matrix: + chart: + - rest-api/helm/charts/nico-rest + - rest-api/helm/charts/nico-rest-site-agent + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Read chart version from Chart.yaml + id: chart-version + run: | + VERSION=$(grep '^version:' ${{ matrix.chart }}/Chart.yaml | awk '{print $2}') + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Package and push Helm chart to NGC + uses: NVIDIA/dsx-github-actions/.github/actions/helm-package-push@94bde998f5d7965576b0c663db7d5d709c918167 + with: + chart-path: ${{ matrix.chart }} + chart-version: ${{ steps.chart-version.outputs.version }} + app-version: ${{ inputs.app_version }} + lint: 'false' + ngc-key: ${{ secrets.NVCR_STG_TOKEN }} + ngc-path: 0837451325059433/carbide-dev + ngc-duplicate: fail diff --git a/.github/workflows/rest-lint-and-test.yml b/.github/workflows/rest-lint-and-test.yml new file mode 100644 index 0000000000..58685fba33 --- /dev/null +++ b/.github/workflows/rest-lint-and-test.yml @@ -0,0 +1,239 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +name: Lint and Test + +on: + workflow_dispatch: + workflow_call: + +defaults: + run: + working-directory: rest-api + +env: + GO_VERSION: "1.25.4" + +jobs: + style: + name: Style Check + runs-on: ubuntu-latest + continue-on-error: false + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + cache: true + cache-dependency-path: rest-api/go.sum + + - name: Download dependencies + run: go mod download + + - name: Install and run revive + run: | + go install github.com/mgechev/revive@v1.3.9 + pkgs=$(go list ./... | grep -v $(go env GOMODCACHE)) + go list $pkgs | grep -v $(go env GOMODCACHE) | xargs -L1 revive -config .revive.toml -set_exit_status + + - name: Run go fmt + run: | + pkgs=$(go list ./... | grep -v $(go env GOMODCACHE)) + go fmt $pkgs > /dev/null 2>&1 + if git diff --quiet; then + echo "go fmt was clean" + else + echo "::error::go fmt was unclean on the following files:" + git -P diff --name-only + exit 1 + fi + + lint-go: + name: Lint Go + runs-on: linux-amd64-cpu4 + continue-on-error: false + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + cache: true + cache-dependency-path: rest-api/go.sum + + - name: Download dependencies + run: go mod download + + - name: Run go vet + run: go vet ./... + + - name: Run golangci-lint + uses: golangci/golangci-lint-action@v9 + with: + version: v2.7.2 + args: --timeout=10m --issues-exit-code=0 ./... + working-directory: rest-api + + lint-openapi: + name: Lint OpenAPI + runs-on: linux-amd64-cpu4 + continue-on-error: false + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up node.js + uses: actions/setup-node@v6 + with: + node-version: '24' + + - name: Install Redocly CLI + run: npm install -g @redocly/cli@latest + + - name: Run OpenAPI lint + run: redocly lint ./openapi/spec.yaml --format=github-actions + + check-generated-files: + name: Check Generated Files + runs-on: ubuntu-latest + continue-on-error: false + defaults: + run: + working-directory: . + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Check OpenAPI generated files are up to date + run: | + BASE="origin/main" + if ! git rev-parse --verify "$BASE" >/dev/null 2>&1; then + echo "Base branch $BASE not found, skipping check." + exit 0 + fi + + CHANGED=$(git diff --name-only "$BASE"...HEAD) + + if echo "$CHANGED" | grep -q '^rest-api/openapi/spec.yaml$'; then + SDK_UPDATED=false + DOCS_UPDATED=false + echo "$CHANGED" | grep -q '^rest-api/sdk/standard/' && SDK_UPDATED=true + echo "$CHANGED" | grep -q '^rest-api/docs/index.html$' && DOCS_UPDATED=true + + if [ "$SDK_UPDATED" = false ]; then + echo "::warning::rest-api/sdk/standard/ was not updated. Please ensure you have run: make generate-sdk" + fi + if [ "$DOCS_UPDATED" = false ]; then + echo "::warning::rest-api/docs/index.html was not updated. Please ensure you have run: make publish-openapi" + fi + + if [ "$SDK_UPDATED" = false ] && [ "$DOCS_UPDATED" = false ]; then + echo "::error::rest-api/openapi/spec.yaml was modified but neither rest-api/sdk/standard/ nor rest-api/docs/index.html was updated." + exit 1 + fi + echo "✓ rest-api/openapi/spec.yaml changed and generated files were also updated (sdk=$SDK_UPDATED, docs=$DOCS_UPDATED)." + else + echo "✓ rest-api/openapi/spec.yaml not changed, skipping generated files check." + fi + + check-protobuf-generated: + name: Check Protobuf Generated Code + runs-on: ubuntu-latest + continue-on-error: false + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + cache: true + cache-dependency-path: rest-api/go.sum + + - name: Install buf and protoc plugins + run: | + go install github.com/bufbuild/buf/cmd/buf@latest + go install google.golang.org/protobuf/cmd/protoc-gen-go@latest + go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest + + - name: Regenerate protobuf code + run: | + cd workflow-schema && buf generate + cd flow && buf generate + + - name: Check for uncommitted changes + run: | + UNTRACKED=$(git ls-files --others --exclude-standard) + + MEANINGFUL_DIFF=$(git diff --unified=0 | grep '^[+-]' | grep -v '^[+-][+-][+-]' | grep -v '^[+-]//.*protoc-gen-go' | grep -v '^[+-]//.*protoc v' || true) + + if [ -z "$MEANINGFUL_DIFF" ] && [ -z "$UNTRACKED" ]; then + echo "✓ Protobuf generated code is up to date." + else + echo "::error::Protobuf generated code is out of date. Please run 'make core-protogen' and 'make flow-protogen' and commit the results." + echo "" + echo "Changed files:" + git status --porcelain + echo "" + echo "Diff:" + git -P diff + if [ -n "$UNTRACKED" ]; then + echo "" + echo "Untracked files:" + echo "$UNTRACKED" + fi + exit 1 + fi + + test: + name: Test (${{ matrix.module }}) + runs-on: ubuntu-latest + continue-on-error: false + strategy: + fail-fast: false + matrix: + # TODO: add flow / powershelf-manager / nvswitch-manager when rest-api/Makefile gets test- targets + module: + - api + - auth + - cert-manager + - common + - db + - ipam + - site-agent + - site-manager + - site-workflow + - workflow + + env: + # db/pkg/util/testing.go uses 'postgres' hostname when CI=true (GitLab convention); force localhost. + CI: "false" + DB_NAME: nicotest + DB_USER: postgres + DB_PASSWORD: postgres + DB_HOST: localhost + DB_PORT: 30432 + CGO_ENABLED: 1 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + cache: true + cache-dependency-path: rest-api/go.sum + + - name: Run unit tests for ${{ matrix.module }} + working-directory: . + run: make rest-api/test-${{ matrix.module }} diff --git a/.github/workflows/rest-prepare-build-info.yml b/.github/workflows/rest-prepare-build-info.yml new file mode 100644 index 0000000000..11930d80a2 --- /dev/null +++ b/.github/workflows/rest-prepare-build-info.yml @@ -0,0 +1,145 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +name: Prepare Build Information + +on: + workflow_call: + inputs: + runner: + description: "Runner type for the job" + required: false + default: "ubuntu-latest" + type: string + outputs: + semantic_version: + description: "Semantic version from VERSION file" + value: ${{ jobs.prepare.outputs.semantic_version }} + short_sha: + description: "Short SHA (7 characters) of the current commit" + value: ${{ jobs.prepare.outputs.short_sha }} + full_sha: + description: "Full SHA of the current commit" + value: ${{ jobs.prepare.outputs.full_sha }} + target_registry: + description: "Target NVCR registry path" + value: ${{ jobs.prepare.outputs.target_registry }} + branch_name: + description: "Sanitized branch name for use in image tags" + value: ${{ jobs.prepare.outputs.branch_name }} + branch_sha_tag: + description: "Combined branch name and short SHA tag (branchname-sha)" + value: ${{ jobs.prepare.outputs.branch_sha_tag }} + is_main_branch: + description: "Whether the current branch is main" + value: ${{ jobs.prepare.outputs.is_main_branch }} + release_tag: + description: "Release tag for the build" + value: ${{ jobs.prepare.outputs.release_tag }} + push_requested: + description: "Whether a push request was made using commit message" + value: ${{ jobs.prepare.outputs.push_requested }} + helm_version: + description: "Helm chart version (from Chart.yaml)" + value: ${{ jobs.prepare.outputs.helm_version }} + +jobs: + prepare: + name: Prepare Build Environment + runs-on: ${{ inputs.runner }} + + outputs: + semantic_version: ${{ steps.generate-version.outputs.semantic_version }} + short_sha: ${{ steps.generate-version.outputs.short_sha }} + full_sha: ${{ steps.generate-version.outputs.full_sha }} + target_registry: ${{ steps.generate-version.outputs.target_registry }} + branch_name: ${{ steps.generate-version.outputs.branch_name }} + branch_sha_tag: ${{ steps.generate-version.outputs.branch_sha_tag }} + is_main_branch: ${{ steps.generate-version.outputs.is_main_branch }} + release_tag: ${{ steps.generate-version.outputs.release_tag }} + push_requested: ${{ steps.generate-version.outputs.push_requested }} + helm_version: ${{ steps.generate-version.outputs.helm_version }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: recursive + + - name: Generate version and build information + id: generate-version + run: | + git fetch --tags --force + RAW_VERSION=$(git describe --tags --first-parent --always --long) + SEMANTIC_VERSION="${RAW_VERSION#v}" + + SHORT_SHA=$(git rev-parse --short HEAD) + FULL_SHA=$(git rev-parse HEAD) + + target_registry="nvcr.io/0837451325059433/carbide-dev" + + BRANCH_NAME="${{ github.ref_name }}" + BRANCH_NAME=$(echo "$BRANCH_NAME" | sed 's/[^a-zA-Z0-9._-]/-/g' | sed 's/--*/-/g' | sed 's/^-//' | sed 's/-$//') + BRANCH_SHA_TAG="${BRANCH_NAME}-${SHORT_SHA}" + + if [ "${{ github.ref }}" = "refs/heads/main" ]; then + IS_MAIN_BRANCH="true" + else + IS_MAIN_BRANCH="false" + fi + + REF_NAME="${{ github.ref_name }}" + if [[ "$REF_NAME" =~ ^v[0-9]+\.[0-9]+\.[0-9]+.*$ ]]; then + RELEASE_TAG="${REF_NAME}" + else + RELEASE_TAG="" + fi + + if echo "${{ github.event.head_commit.message }}" | grep -q "push-container"; then + PUSH_REQUESTED="true" + else + PUSH_REQUESTED="false" + fi + + echo "semantic_version=$SEMANTIC_VERSION" >> $GITHUB_OUTPUT + echo "short_sha=$SHORT_SHA" >> $GITHUB_OUTPUT + echo "full_sha=$FULL_SHA" >> $GITHUB_OUTPUT + echo "target_registry=$target_registry" >> $GITHUB_OUTPUT + echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT + echo "branch_sha_tag=$BRANCH_SHA_TAG" >> $GITHUB_OUTPUT + echo "is_main_branch=$IS_MAIN_BRANCH" >> $GITHUB_OUTPUT + echo "release_tag=$RELEASE_TAG" >> $GITHUB_OUTPUT + echo "push_requested=$PUSH_REQUESTED" >> $GITHUB_OUTPUT + + HELM_VERSION=$(grep '^version:' rest-api/helm/charts/nico-rest/Chart.yaml | awk '{print $2}') + echo "helm_version=$HELM_VERSION" >> $GITHUB_OUTPUT + + echo "Generated build information:" + echo " SEMANTIC_VERSION: $SEMANTIC_VERSION" + echo " SHORT_SHA: $SHORT_SHA" + echo " FULL_SHA: $FULL_SHA" + echo " TARGET_REGISTRY: $target_registry" + echo " BRANCH_NAME: $BRANCH_NAME" + echo " BRANCH_SHA_TAG: $BRANCH_SHA_TAG" + echo " IS_MAIN_BRANCH: $IS_MAIN_BRANCH" + echo " RELEASE_TAG: $RELEASE_TAG" + echo " PUSH_REQUESTED: $PUSH_REQUESTED" + echo " HELM_VERSION: $HELM_VERSION" + + - name: Generate build summary + run: | + echo "# Build Information Generated" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "## Version Details" >> $GITHUB_STEP_SUMMARY + echo "- **Semantic Version**: \`${{ steps.generate-version.outputs.semantic_version }}\`" >> $GITHUB_STEP_SUMMARY + echo "- **Short SHA**: \`${{ steps.generate-version.outputs.short_sha }}\`" >> $GITHUB_STEP_SUMMARY + echo "- **Full SHA**: \`${{ steps.generate-version.outputs.full_sha }}\`" >> $GITHUB_STEP_SUMMARY + echo "- **Branch**: \`${{ github.ref_name }}\`" >> $GITHUB_STEP_SUMMARY + echo "- **Branch Name (sanitized)**: \`${{ steps.generate-version.outputs.branch_name }}\`" >> $GITHUB_STEP_SUMMARY + echo "- **Branch-SHA Tag**: \`${{ steps.generate-version.outputs.branch_sha_tag }}\`" >> $GITHUB_STEP_SUMMARY + echo "- **Is Main Branch**: \`${{ steps.generate-version.outputs.is_main_branch }}\`" >> $GITHUB_STEP_SUMMARY + echo "- **Release Tag**: \`${{ steps.generate-version.outputs.release_tag }}\`" >> $GITHUB_STEP_SUMMARY + echo "- **Push Requested**: \`${{ steps.generate-version.outputs.push_requested }}\`" >> $GITHUB_STEP_SUMMARY + echo "- **Helm Version**: \`${{ steps.generate-version.outputs.helm_version }}\`" >> $GITHUB_STEP_SUMMARY + echo "- **Target Registry**: \`${{ steps.generate-version.outputs.target_registry }}\`" >> $GITHUB_STEP_SUMMARY diff --git a/rest-api/PHASE2_TEST.md b/rest-api/PHASE2_TEST.md new file mode 100644 index 0000000000..e8817b83ab --- /dev/null +++ b/rest-api/PHASE2_TEST.md @@ -0,0 +1,3 @@ +# Phase 2 path-trigger smoke marker — rest-only + +Throwaway file for #1756 Phase 2.1 validation. Touching only `rest-api/**` so the core gate skips and the rest gate runs end-to-end. Delete after validation.