From a0aa0a8c8be227717c9aa1a12d120f7d84141a26 Mon Sep 17 00:00:00 2001 From: Larry Chen Date: Tue, 19 May 2026 15:17:19 +0800 Subject: [PATCH 01/20] ci(rest): add prepare-build-info workflow (#1756) --- .github/workflows/rest-prepare-build-info.yml | 148 ++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 .github/workflows/rest-prepare-build-info.yml diff --git a/.github/workflows/rest-prepare-build-info.yml b/.github/workflows/rest-prepare-build-info.yml new file mode 100644 index 0000000000..1e0382ac64 --- /dev/null +++ b/.github/workflows/rest-prepare-build-info.yml @@ -0,0 +1,148 @@ +# 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: | + if [ -f "rest-api/VERSION" ]; then + SEMANTIC_VERSION=$(grep -v '^#' rest-api/VERSION | tr -d '[:space:]') + else + echo "ERROR: rest-api/VERSION file not found" + exit 1 + fi + + 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 From 82ebaf7aec62b78c46924c59ffa59e76909bdffd Mon Sep 17 00:00:00 2001 From: Larry Chen Date: Tue, 19 May 2026 15:20:35 +0800 Subject: [PATCH 02/20] ci(rest): add lint-and-test workflow (#1756) --- .github/workflows/rest-lint-and-test.yml | 338 +++++++++++++++++++++++ 1 file changed, 338 insertions(+) create mode 100644 .github/workflows/rest-lint-and-test.yml diff --git a/.github/workflows/rest-lint-and-test.yml b/.github/workflows/rest-lint-and-test.yml new file mode 100644 index 0000000000..23a25b44a8 --- /dev/null +++ b/.github/workflows/rest-lint-and-test.yml @@ -0,0 +1,338 @@ +# 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: + module: + - api + - auth + - cert-manager + - common + - db + - ipam + - powershelf-manager + - nvswitch-manager + - flow + - site-agent + - site-manager + - site-workflow + - workflow + - workflow-schema + + services: + postgres: + image: postgres:14.4-alpine + env: + POSTGRES_DB: nicotest + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_HOST_AUTH_METHOD: trust + ports: + - 30432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + env: + 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: Download dependencies + run: go mod download + + - name: Install go-junit-report + run: go install github.com/jstemmer/go-junit-report/v2@latest + + - name: Tune PostgreSQL for faster tests + run: | + docker exec ${{ job.services.postgres.id }} \ + psql -U postgres -d nicotest -v ON_ERROR_STOP=1 \ + -c "ALTER SYSTEM SET fsync = 'off'" \ + -c "ALTER SYSTEM SET synchronous_commit = 'off'" \ + -c "ALTER SYSTEM SET full_page_writes = 'off'" \ + -c "ALTER SYSTEM SET autovacuum = 'off'" \ + -c "SELECT pg_reload_conf()" + + - name: Run unit tests for ${{ matrix.module }} + run: | + set -o pipefail + echo "Testing module: ${{ matrix.module }}" + + if [ "${{ matrix.module }}" = "site-agent" ]; then + make nico-mock-server-start + make flow-mock-server-start + fi + + go test -v -race -p 1 ./${{ matrix.module }}/... -coverprofile=coverage-${{ matrix.module }}.txt -covermode=atomic 2>&1 | tee test-output-${{ matrix.module }}.txt + + if [ "${{ matrix.module }}" = "site-agent" ]; then + make nico-mock-server-stop + make flow-mock-server-stop + fi + + - name: Generate JUnit report + if: always() + run: | + cat test-output-${{ matrix.module }}.txt | go-junit-report -set-exit-code > report-${{ matrix.module }}.xml || true + + - name: Display coverage summary + if: always() + run: | + if [ -f coverage-${{ matrix.module }}.txt ]; then + go tool cover -func coverage-${{ matrix.module }}.txt + fi + + - name: Upload test report artifact + uses: actions/upload-artifact@v4 + if: always() + with: + name: test-report-${{ matrix.module }} + path: rest-api/report-${{ matrix.module }}.xml + retention-days: 7 + + publish-test-results: + name: Publish Test Results + runs-on: ubuntu-latest + needs: [test] + if: always() + defaults: + run: + working-directory: . + permissions: + contents: read + checks: write + pull-requests: write + steps: + - name: Download all test report artifacts + uses: actions/download-artifact@v4 + with: + pattern: test-report-* + path: test-reports + merge-multiple: true + + - name: Publish Test Results + uses: EnricoMi/publish-unit-test-result-action@v2 + if: always() + with: + files: test-reports/**/*.xml + check_name: Test Results + comment_mode: always + compare_to_earlier_commit: true + job_summary: true + report_individual_runs: true + report_suite_logs: info From 3a9542e7e2b403375b84d891c946caad9f93c160 Mon Sep 17 00:00:00 2001 From: Larry Chen Date: Tue, 19 May 2026 15:21:37 +0800 Subject: [PATCH 03/20] ci(rest): add build-binaries workflow (#1756) --- .github/workflows/rest-build-binaries.yml | 92 +++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 .github/workflows/rest-build-binaries.yml 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 From 2d276b5637466372e459f649f522b16170e27567 Mon Sep 17 00:00:00 2001 From: Larry Chen Date: Tue, 19 May 2026 15:25:52 +0800 Subject: [PATCH 04/20] ci(rest): add build-push-service workflow (#1756) --- .github/workflows/rest-build-push-service.yml | 256 ++++++++++++++++++ 1 file changed, 256 insertions(+) create mode 100644 .github/workflows/rest-build-push-service.yml diff --git a/.github/workflows/rest-build-push-service.yml b/.github/workflows/rest-build-push-service.yml new file mode 100644 index 0000000000..b2bf305ad2 --- /dev/null +++ b/.github/workflows/rest-build-push-service.yml @@ -0,0 +1,256 @@ +# 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="" + + if [ "${{ inputs.is_main_branch }}" == "true" ]; then + TAGS="${TAGS},${{ inputs.target_registry }}/${{ inputs.service_name }}:${{ inputs.semantic_version }}-${{ inputs.short_sha }}" + TAGS="${TAGS},${{ inputs.target_registry }}/${{ inputs.service_name }}:latest" + elif [ "${{ inputs.release_tag }}" != "" ]; then + TAGS="${TAGS},${{ inputs.target_registry }}/${{ inputs.service_name }}:${{ inputs.release_tag }}" + else + TAGS="${TAGS},${{ inputs.target_registry }}/${{ inputs.service_name }}:${{ inputs.branch_sha_tag }}" + fi + + TAGS="${TAGS#,}" + echo "tags=$TAGS" >> $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.is_main_branch == 'true' && inputs.push_enabled == true + run: | + mkdir -p artifacts + docker pull --platform linux/amd64 ${{ inputs.target_registry }}/${{ inputs.service_name }}:${{ inputs.semantic_version }}-${{ inputs.short_sha }} + docker create --name temp-container --platform linux/amd64 ${{ inputs.target_registry }}/${{ inputs.service_name }}:${{ inputs.semantic_version }}-${{ inputs.short_sha }} + 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.is_main_branch == 'true' && inputs.push_enabled == true + run: | + docker pull --platform linux/arm64 ${{ inputs.target_registry }}/${{ inputs.service_name }}:${{ inputs.semantic_version }}-${{ inputs.short_sha }} + docker create --name temp-container-arm --platform linux/arm64 ${{ inputs.target_registry }}/${{ inputs.service_name }}:${{ inputs.semantic_version }}-${{ inputs.short_sha }} + 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.is_main_branch == 'true' && inputs.push_enabled == true + run: | + docker save ${{ inputs.target_registry }}/${{ inputs.service_name }}:${{ inputs.semantic_version }}-${{ inputs.short_sha }} | gzip > artifacts/${{ inputs.service_name }}-${{ inputs.semantic_version }}-${{ inputs.short_sha }}.tar.gz + + - name: Upload binary artifact with semantic version + 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: ${{ inputs.semantic_version }}-${{ inputs.short_sha }} + 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.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: ${{ inputs.semantic_version }}-${{ inputs.short_sha }} + 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.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: ${{ inputs.semantic_version }}-${{ inputs.short_sha }} + path: artifacts/${{ inputs.service_name }}-${{ inputs.semantic_version }}-${{ inputs.short_sha }}.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 }} From 7a6cb963f9e3b91c529325c3f512e0534067f7f0 Mon Sep 17 00:00:00 2001 From: Larry Chen Date: Tue, 19 May 2026 15:37:42 +0800 Subject: [PATCH 05/20] ci(rest): add build-push-docker workflow (#1756) --- .github/workflows/rest-build-push-docker.yml | 383 +++++++++++++++++++ 1 file changed, 383 insertions(+) create mode 100644 .github/workflows/rest-build-push-docker.yml diff --git a/.github/workflows/rest-build-push-docker.yml b/.github/workflows/rest-build-push-docker.yml new file mode 100644 index 0000000000..e7d910d9ec --- /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: ./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: ./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: ./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: ./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: ./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: ./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: ./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: ./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: ./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.is_main_branch }}" = "true" ] || [ "${{ github.ref_name }}" = "main" ]; then + echo "## Binary Artifacts Uploaded (Main Branch)" >> $GITHUB_STEP_SUMMARY + echo "Each binary uploaded for both amd64 and arm64 with versions: \`${{ inputs.semantic_version }}\`, \`latest\`, \`${{ inputs.branch_sha_tag }}\`" >> $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.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' From a61cc6485a33a78e1ad99fd226ed63cb9cb92a69 Mon Sep 17 00:00:00 2001 From: Larry Chen Date: Tue, 19 May 2026 15:40:04 +0800 Subject: [PATCH 06/20] ci(rest): add helm-workflows with push disabled (#1756) --- .github/workflows/rest-helm-workflows.yml | 135 ++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 .github/workflows/rest-helm-workflows.yml 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 From 9bd977a9fb546324cba4609cc14d1a0c3be53d2f Mon Sep 17 00:00:00 2001 From: Larry Chen Date: Tue, 19 May 2026 15:41:21 +0800 Subject: [PATCH 07/20] ci(rest): add rest-ci entry workflow with build-only mock (#1756) --- .github/workflows/rest-ci.yml | 93 +++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 .github/workflows/rest-ci.yml diff --git a/.github/workflows/rest-ci.yml b/.github/workflows/rest-ci.yml new file mode 100644 index 0000000000..03fde08086 --- /dev/null +++ b/.github/workflows/rest-ci.yml @@ -0,0 +1,93 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +name: NICo REST CI + +on: + workflow_dispatch: + pull_request: + paths: + - 'rest-api/**' + - '.github/workflows/rest-*.yml' + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + prepare: + name: Prepare Build Info + 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: 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: false + release_tag: ${{ needs.prepare.outputs.release_tag }} + secrets: + NVCR_USERNAME: ${{ secrets.NVCR_STG_USERNAME }} + NVCR_TOKEN: ${{ secrets.NVCR_STG_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_STG_TOKEN }} From 03e662aa5aea13fa19a1ecacda6ea75dafff95ce Mon Sep 17 00:00:00 2001 From: Larry Chen Date: Tue, 19 May 2026 15:50:12 +0800 Subject: [PATCH 08/20] ci(rest): trigger on push to pull-request/* branches (#1756) --- .github/workflows/rest-ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/rest-ci.yml b/.github/workflows/rest-ci.yml index 03fde08086..83610f1069 100644 --- a/.github/workflows/rest-ci.yml +++ b/.github/workflows/rest-ci.yml @@ -5,7 +5,9 @@ name: NICo REST CI on: workflow_dispatch: - pull_request: + push: + branches: + - 'pull-request/[0-9]+' paths: - 'rest-api/**' - '.github/workflows/rest-*.yml' From 8050ab9c1eb547029fb5f77dbcc31da500c4262d Mon Sep 17 00:00:00 2001 From: Larry Chen Date: Tue, 19 May 2026 16:29:04 +0800 Subject: [PATCH 09/20] ci(rest): fix protobuf cd path (workflow-schema/flow not rest-api/flow) (#1756) --- .github/workflows/rest-lint-and-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rest-lint-and-test.yml b/.github/workflows/rest-lint-and-test.yml index 23a25b44a8..81deb42107 100644 --- a/.github/workflows/rest-lint-and-test.yml +++ b/.github/workflows/rest-lint-and-test.yml @@ -167,7 +167,7 @@ jobs: - name: Regenerate protobuf code run: | cd workflow-schema && buf generate - cd ../flow && buf generate + cd flow && buf generate - name: Check for uncommitted changes run: | From 2e80712e9587a982c0b6ffa9f95d9582fdb7e932 Mon Sep 17 00:00:00 2001 From: Larry Chen Date: Tue, 19 May 2026 17:24:04 +0800 Subject: [PATCH 10/20] ci(rest): use core repo's NVCR_USERNAME/NVCR_TOKEN secrets (#1756) --- .github/workflows/rest-ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/rest-ci.yml b/.github/workflows/rest-ci.yml index 83610f1069..1a405412a8 100644 --- a/.github/workflows/rest-ci.yml +++ b/.github/workflows/rest-ci.yml @@ -80,8 +80,8 @@ jobs: push_enabled: false release_tag: ${{ needs.prepare.outputs.release_tag }} secrets: - NVCR_USERNAME: ${{ secrets.NVCR_STG_USERNAME }} - NVCR_TOKEN: ${{ secrets.NVCR_STG_TOKEN }} + NVCR_USERNAME: ${{ secrets.NVCR_USERNAME }} + NVCR_TOKEN: ${{ secrets.NVCR_TOKEN }} helm: name: Helm Charts @@ -92,4 +92,4 @@ jobs: with: app_version: ${{ needs.prepare.outputs.semantic_version }} secrets: - NVCR_STG_TOKEN: ${{ secrets.NVCR_STG_TOKEN }} + NVCR_STG_TOKEN: ${{ secrets.NVCR_TOKEN }} From 9438c7f4d74519255612a95e98529926f9e02636 Mon Sep 17 00:00:00 2001 From: Larry Chen Date: Wed, 20 May 2026 10:58:13 +0800 Subject: [PATCH 11/20] ci(rest): prefix dockerfile paths with rest-api/ for buildx (#1756) --- .github/workflows/rest-build-push-docker.yml | 26 ++++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/rest-build-push-docker.yml b/.github/workflows/rest-build-push-docker.yml index e7d910d9ec..414f454095 100644 --- a/.github/workflows/rest-build-push-docker.yml +++ b/.github/workflows/rest-build-push-docker.yml @@ -63,7 +63,7 @@ jobs: service_name: nico-rest-api binary_name: api binary_path: /app/api - dockerfile: ./docker/production/Dockerfile.nico-rest-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 }} @@ -81,7 +81,7 @@ jobs: service_name: nico-rest-db binary_name: migrations binary_path: /app/migrations - dockerfile: ./docker/production/Dockerfile.nico-rest-db + 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 }} @@ -99,7 +99,7 @@ jobs: service_name: nico-rest-site-manager binary_name: sitemgr binary_path: /app/sitemgr - dockerfile: ./docker/production/Dockerfile.nico-rest-site-manager + 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 }} @@ -117,7 +117,7 @@ jobs: service_name: nico-rest-workflow binary_name: workflow binary_path: /app/workflow - dockerfile: ./docker/production/Dockerfile.nico-rest-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 }} @@ -135,7 +135,7 @@ jobs: service_name: nico-rest-site-agent binary_name: site-agent binary_path: /app/site-agent - dockerfile: ./docker/production/Dockerfile.nico-rest-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 }} @@ -153,7 +153,7 @@ jobs: service_name: nico-rest-cert-manager binary_name: credsmgr binary_path: /app/credsmgr - dockerfile: ./docker/production/Dockerfile.nico-rest-cert-manager + 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 }} @@ -171,7 +171,7 @@ jobs: service_name: nico-flow binary_name: flow binary_path: /app/flow - dockerfile: ./docker/production/Dockerfile.nico-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 }} @@ -189,7 +189,7 @@ jobs: service_name: nico-psm binary_name: psm binary_path: /app/psm - dockerfile: ./docker/production/Dockerfile.nico-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 }} @@ -207,7 +207,7 @@ jobs: service_name: nico-nsm binary_name: nsm binary_path: /app/nsm - dockerfile: ./docker/production/Dockerfile.nico-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 }} @@ -279,9 +279,9 @@ jobs: 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.is_main_branch }}" = "true" ] || [ "${{ github.ref_name }}" = "main" ]; then - echo "## Binary Artifacts Uploaded (Main Branch)" >> $GITHUB_STEP_SUMMARY - echo "Each binary uploaded for both amd64 and arm64 with versions: \`${{ inputs.semantic_version }}\`, \`latest\`, \`${{ inputs.branch_sha_tag }}\`" >> $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 @@ -345,7 +345,7 @@ jobs: { "service": "nico-psm", "image_ref": "${{ needs.build-nico-psm.outputs.image_ref }}", - "binary_resource": "${{ needs.build-nico-psm.ngc_binary_resource }}", + "binary_resource": "${{ needs.build-nico-psm.outputs.ngc_binary_resource }}", "image_resource": "${{ needs.build-nico-psm.outputs.ngc_image_resource }}" }, { From b767a050a4c3cf21ba339a9df2539157bf354822 Mon Sep 17 00:00:00 2001 From: Larry Chen Date: Wed, 20 May 2026 11:07:59 +0800 Subject: [PATCH 12/20] chore: add root VERSION file (1.5.0); prepare-build-info reads root first (#1756) --- .github/workflows/rest-prepare-build-info.yml | 11 ++++++++--- VERSION | 4 ++++ 2 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 VERSION diff --git a/.github/workflows/rest-prepare-build-info.yml b/.github/workflows/rest-prepare-build-info.yml index 1e0382ac64..fbaccc1e5f 100644 --- a/.github/workflows/rest-prepare-build-info.yml +++ b/.github/workflows/rest-prepare-build-info.yml @@ -70,10 +70,15 @@ jobs: - name: Generate version and build information id: generate-version run: | - if [ -f "rest-api/VERSION" ]; then - SEMANTIC_VERSION=$(grep -v '^#' rest-api/VERSION | tr -d '[:space:]') + VERSION_FILE="VERSION" + if [ ! -f "$VERSION_FILE" ]; then + VERSION_FILE="rest-api/VERSION" + fi + + if [ -f "$VERSION_FILE" ]; then + SEMANTIC_VERSION=$(grep -v '^#' "$VERSION_FILE" | tr -d '[:space:]') else - echo "ERROR: rest-api/VERSION file not found" + echo "ERROR: VERSION file not found" exit 1 fi diff --git a/VERSION b/VERSION new file mode 100644 index 0000000000..2280e2b09e --- /dev/null +++ b/VERSION @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +1.5.0 From 294fdad83769612e4e54e16af0dd1ef945dd2146 Mon Sep 17 00:00:00 2001 From: Larry Chen Date: Wed, 20 May 2026 11:08:00 +0800 Subject: [PATCH 13/20] ci(core): add changes gate to skip core CI on rest-only PRs (#1756) --- .github/workflows/ci.yaml | 77 +++++++++++++++++++++++++++++++++------ 1 file changed, 65 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 00ee997b1e..d322795a56 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,54 @@ 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 + + - name: Detect non-rest changes + id: non-rest-changes + uses: dorny/paths-filter@v3 + with: + 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,19 +150,27 @@ 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./') + + VERSION_FILE="VERSION" + if [ ! -f "$VERSION_FILE" ]; then + VERSION_FILE="rest-api/VERSION" + fi + + if [ ! -f "$VERSION_FILE" ]; then + echo "::error::VERSION file not found" + exit 1 + fi + + SEMANTIC_VERSION=$(grep -v '^#' "$VERSION_FILE" | tr -d '[:space:]') + VERSION="${SEMANTIC_VERSION}-${SHORT_SHA}" + HELM_VERSION="${VERSION}" echo "version=${VERSION}" >> $GITHUB_OUTPUT echo "helm_version=${HELM_VERSION}" >> $GITHUB_OUTPUT + echo "Using ${VERSION_FILE} to calculate VERSION and HELM_VERSION" echo "Calculated VERSION: ${VERSION}" echo "Calculated HELM_VERSION: ${HELM_VERSION}" @@ -440,7 +492,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 +513,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 +540,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 +557,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 +1282,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 From 3a74b988debe43d6a89fe2d4ecd09efefeedde64 Mon Sep 17 00:00:00 2001 From: Larry Chen Date: Wed, 20 May 2026 11:08:00 +0800 Subject: [PATCH 14/20] ci(rest): extend triggers to main/release/tags + add changes gate (#1756) --- .github/workflows/rest-ci.yml | 49 ++++++++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/.github/workflows/rest-ci.yml b/.github/workflows/rest-ci.yml index 1a405412a8..487c675b8e 100644 --- a/.github/workflows/rest-ci.yml +++ b/.github/workflows/rest-ci.yml @@ -7,18 +7,59 @@ on: workflow_dispatch: push: branches: + - main + - release/* - 'pull-request/[0-9]+' - paths: - - 'rest-api/**' - - '.github/workflows/rest-*.yml' + 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 + + - name: Detect rest-api changes + id: filter + uses: dorny/paths-filter@v3 + with: + filters: | + rest_api: + - 'VERSION' + - 'rest-api/**' + - '.github/workflows/rest-*.yml' + + - name: Decide whether REST CI should run + id: gate + env: + REF: ${{ github.ref }} + 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}" + 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 @@ -77,7 +118,7 @@ jobs: 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: false + 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 }} From f1eb47d0cfac10ccb73c82a38cc20b4a708f754c Mon Sep 17 00:00:00 2001 From: Larry Chen Date: Wed, 20 May 2026 11:08:00 +0800 Subject: [PATCH 15/20] ci: track primary_tag in build-push-service; simplify promotion helm_version (#1756) --- .github/workflows/promotion.yaml | 5 +-- .github/workflows/rest-build-push-service.yml | 44 +++++++++++-------- 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/.github/workflows/promotion.yaml b/.github/workflows/promotion.yaml index 7b22f169cc..f689a7bad9 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 @@ -44,8 +44,7 @@ jobs: run: | set -euo pipefail - HELM_VERSION_BASE="${VERSION#v}" - HELM_VERSION="$(echo "${HELM_VERSION_BASE}" | sed 's/\(.*\)-/\1./')" + HELM_VERSION="${VERSION#v}" echo "helm_version=${HELM_VERSION}" >> "$GITHUB_OUTPUT" echo "Promoting chart version ${HELM_VERSION} from release ${VERSION}" diff --git a/.github/workflows/rest-build-push-service.yml b/.github/workflows/rest-build-push-service.yml index b2bf305ad2..e6c4ebb3bf 100644 --- a/.github/workflows/rest-build-push-service.yml +++ b/.github/workflows/rest-build-push-service.yml @@ -128,18 +128,26 @@ jobs: id: docker-tags run: | TAGS="" + PRIMARY_TAG="" + ARTIFACT_VERSION="${{ inputs.semantic_version }}-${{ inputs.short_sha }}" if [ "${{ inputs.is_main_branch }}" == "true" ]; then - TAGS="${TAGS},${{ inputs.target_registry }}/${{ inputs.service_name }}:${{ inputs.semantic_version }}-${{ inputs.short_sha }}" + 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 - TAGS="${TAGS},${{ inputs.target_registry }}/${{ inputs.service_name }}:${{ inputs.release_tag }}" + PRIMARY_TAG="${{ inputs.release_tag }}" + ARTIFACT_VERSION="${{ inputs.release_tag }}" + TAGS="${TAGS},${{ inputs.target_registry }}/${{ inputs.service_name }}:${PRIMARY_TAG}" else - TAGS="${TAGS},${{ inputs.target_registry }}/${{ inputs.service_name }}:${{ inputs.branch_sha_tag }}" + 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 @@ -160,37 +168,37 @@ jobs: org.opencontainers.image.url=${{ github.repositoryUrl }} - name: Extract amd64 binary from image - if: inputs.is_main_branch == 'true' && inputs.push_enabled == true + if: inputs.push_enabled == true run: | mkdir -p artifacts - docker pull --platform linux/amd64 ${{ inputs.target_registry }}/${{ inputs.service_name }}:${{ inputs.semantic_version }}-${{ inputs.short_sha }} - docker create --name temp-container --platform linux/amd64 ${{ inputs.target_registry }}/${{ inputs.service_name }}:${{ inputs.semantic_version }}-${{ inputs.short_sha }} + 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.is_main_branch == 'true' && inputs.push_enabled == true + if: inputs.push_enabled == true run: | - docker pull --platform linux/arm64 ${{ inputs.target_registry }}/${{ inputs.service_name }}:${{ inputs.semantic_version }}-${{ inputs.short_sha }} - docker create --name temp-container-arm --platform linux/arm64 ${{ inputs.target_registry }}/${{ inputs.service_name }}:${{ inputs.semantic_version }}-${{ inputs.short_sha }} + 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.is_main_branch == 'true' && inputs.push_enabled == true + if: inputs.push_enabled == true run: | - docker save ${{ inputs.target_registry }}/${{ inputs.service_name }}:${{ inputs.semantic_version }}-${{ inputs.short_sha }} | gzip > artifacts/${{ inputs.service_name }}-${{ inputs.semantic_version }}-${{ inputs.short_sha }}.tar.gz + 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.is_main_branch == 'true' && inputs.push_enabled == true + 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: ${{ inputs.semantic_version }}-${{ inputs.short_sha }} + version: ${{ steps.docker-tags.outputs.artifact_version }} path: artifacts/${{ inputs.binary_name }}-amd64 ngc-path: ${{ inputs.ngc_path }} ngc-key: ${{ secrets.NVCR_TOKEN }} @@ -208,13 +216,13 @@ jobs: ngc-key: ${{ secrets.NVCR_TOKEN }} - name: Upload arm64 binary artifact with semantic version - if: inputs.is_main_branch == 'true' && inputs.push_enabled == true + 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: ${{ inputs.semantic_version }}-${{ inputs.short_sha }} + version: ${{ steps.docker-tags.outputs.artifact_version }} path: artifacts/${{ inputs.binary_name }}-arm64 ngc-path: ${{ inputs.ngc_path }} ngc-key: ${{ secrets.NVCR_TOKEN }} @@ -232,14 +240,14 @@ jobs: ngc-key: ${{ secrets.NVCR_TOKEN }} - name: Upload Docker image artifact with semantic version - if: inputs.is_main_branch == 'true' && inputs.push_enabled == true + 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: ${{ inputs.semantic_version }}-${{ inputs.short_sha }} - path: artifacts/${{ inputs.service_name }}-${{ inputs.semantic_version }}-${{ inputs.short_sha }}.tar.gz + 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 }} From c8470aae82dc8fe00c3484475073d72be10f3c74 Mon Sep 17 00:00:00 2001 From: Larry Chen Date: Wed, 20 May 2026 12:38:47 +0800 Subject: [PATCH 16/20] revert: drop root VERSION file, use git describe for versioning (#1756) --- .github/workflows/ci.yaml | 21 +++++-------------- .github/workflows/promotion.yaml | 3 ++- .github/workflows/rest-prepare-build-info.yml | 14 +++---------- VERSION | 4 ---- 4 files changed, 10 insertions(+), 32 deletions(-) delete mode 100644 VERSION diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d322795a56..68fe027c37 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -150,27 +150,16 @@ jobs: set -euo pipefail - # Get short SHA + git fetch --tags --force SHORT_SHA=$(git rev-parse --short=7 HEAD) echo "short_sha=${SHORT_SHA}" >> $GITHUB_OUTPUT - - VERSION_FILE="VERSION" - if [ ! -f "$VERSION_FILE" ]; then - VERSION_FILE="rest-api/VERSION" - fi - - if [ ! -f "$VERSION_FILE" ]; then - echo "::error::VERSION file not found" - exit 1 - fi - - SEMANTIC_VERSION=$(grep -v '^#' "$VERSION_FILE" | tr -d '[:space:]') - VERSION="${SEMANTIC_VERSION}-${SHORT_SHA}" - HELM_VERSION="${VERSION}" + echo "Using Git describe to extract version as VERSION and HELM_VERSION" + VERSION=$(git describe --tags --first-parent --always --long) + HELM_VERSION_BASE="${VERSION#v}" + HELM_VERSION=$(echo "$HELM_VERSION_BASE" | sed 's/\(.*\)-/\1./') echo "version=${VERSION}" >> $GITHUB_OUTPUT echo "helm_version=${HELM_VERSION}" >> $GITHUB_OUTPUT - echo "Using ${VERSION_FILE} to calculate VERSION and HELM_VERSION" echo "Calculated VERSION: ${VERSION}" echo "Calculated HELM_VERSION: ${HELM_VERSION}" diff --git a/.github/workflows/promotion.yaml b/.github/workflows/promotion.yaml index f689a7bad9..8e10389839 100644 --- a/.github/workflows/promotion.yaml +++ b/.github/workflows/promotion.yaml @@ -44,7 +44,8 @@ jobs: run: | set -euo pipefail - HELM_VERSION="${VERSION#v}" + HELM_VERSION_BASE="${VERSION#v}" + HELM_VERSION="$(echo "${HELM_VERSION_BASE}" | sed 's/\(.*\)-/\1./')" echo "helm_version=${HELM_VERSION}" >> "$GITHUB_OUTPUT" echo "Promoting chart version ${HELM_VERSION} from release ${VERSION}" diff --git a/.github/workflows/rest-prepare-build-info.yml b/.github/workflows/rest-prepare-build-info.yml index fbaccc1e5f..11930d80a2 100644 --- a/.github/workflows/rest-prepare-build-info.yml +++ b/.github/workflows/rest-prepare-build-info.yml @@ -70,17 +70,9 @@ jobs: - name: Generate version and build information id: generate-version run: | - VERSION_FILE="VERSION" - if [ ! -f "$VERSION_FILE" ]; then - VERSION_FILE="rest-api/VERSION" - fi - - if [ -f "$VERSION_FILE" ]; then - SEMANTIC_VERSION=$(grep -v '^#' "$VERSION_FILE" | tr -d '[:space:]') - else - echo "ERROR: VERSION file not found" - exit 1 - fi + 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) diff --git a/VERSION b/VERSION deleted file mode 100644 index 2280e2b09e..0000000000 --- a/VERSION +++ /dev/null @@ -1,4 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -1.5.0 From ede0dcf1d3450cf2cb789087bcb15fe22efd7f4f Mon Sep 17 00:00:00 2001 From: Larry Chen Date: Wed, 20 May 2026 12:40:44 +0800 Subject: [PATCH 17/20] ci(rest): switch test to make rest-api/test-; skip 3 untestable modules (#1756) --- .github/workflows/rest-lint-and-test.yml | 113 +---------------------- 1 file changed, 2 insertions(+), 111 deletions(-) diff --git a/.github/workflows/rest-lint-and-test.yml b/.github/workflows/rest-lint-and-test.yml index 81deb42107..77c1ee11ea 100644 --- a/.github/workflows/rest-lint-and-test.yml +++ b/.github/workflows/rest-lint-and-test.yml @@ -200,6 +200,7 @@ jobs: strategy: fail-fast: false matrix: + # TODO: add flow / powershelf-manager / nvswitch-manager when rest-api/Makefile gets test- targets module: - api - auth @@ -207,39 +208,10 @@ jobs: - common - db - ipam - - powershelf-manager - - nvswitch-manager - - flow - site-agent - site-manager - site-workflow - workflow - - workflow-schema - - services: - postgres: - image: postgres:14.4-alpine - env: - POSTGRES_DB: nicotest - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_HOST_AUTH_METHOD: trust - ports: - - 30432:5432 - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - - env: - CI: "false" - DB_NAME: nicotest - DB_USER: postgres - DB_PASSWORD: postgres - DB_HOST: localhost - DB_PORT: 30432 - CGO_ENABLED: 1 steps: - name: Checkout code @@ -252,87 +224,6 @@ jobs: cache: true cache-dependency-path: rest-api/go.sum - - name: Download dependencies - run: go mod download - - - name: Install go-junit-report - run: go install github.com/jstemmer/go-junit-report/v2@latest - - - name: Tune PostgreSQL for faster tests - run: | - docker exec ${{ job.services.postgres.id }} \ - psql -U postgres -d nicotest -v ON_ERROR_STOP=1 \ - -c "ALTER SYSTEM SET fsync = 'off'" \ - -c "ALTER SYSTEM SET synchronous_commit = 'off'" \ - -c "ALTER SYSTEM SET full_page_writes = 'off'" \ - -c "ALTER SYSTEM SET autovacuum = 'off'" \ - -c "SELECT pg_reload_conf()" - - name: Run unit tests for ${{ matrix.module }} - run: | - set -o pipefail - echo "Testing module: ${{ matrix.module }}" - - if [ "${{ matrix.module }}" = "site-agent" ]; then - make nico-mock-server-start - make flow-mock-server-start - fi - - go test -v -race -p 1 ./${{ matrix.module }}/... -coverprofile=coverage-${{ matrix.module }}.txt -covermode=atomic 2>&1 | tee test-output-${{ matrix.module }}.txt - - if [ "${{ matrix.module }}" = "site-agent" ]; then - make nico-mock-server-stop - make flow-mock-server-stop - fi - - - name: Generate JUnit report - if: always() - run: | - cat test-output-${{ matrix.module }}.txt | go-junit-report -set-exit-code > report-${{ matrix.module }}.xml || true - - - name: Display coverage summary - if: always() - run: | - if [ -f coverage-${{ matrix.module }}.txt ]; then - go tool cover -func coverage-${{ matrix.module }}.txt - fi - - - name: Upload test report artifact - uses: actions/upload-artifact@v4 - if: always() - with: - name: test-report-${{ matrix.module }} - path: rest-api/report-${{ matrix.module }}.xml - retention-days: 7 - - publish-test-results: - name: Publish Test Results - runs-on: ubuntu-latest - needs: [test] - if: always() - defaults: - run: working-directory: . - permissions: - contents: read - checks: write - pull-requests: write - steps: - - name: Download all test report artifacts - uses: actions/download-artifact@v4 - with: - pattern: test-report-* - path: test-reports - merge-multiple: true - - - name: Publish Test Results - uses: EnricoMi/publish-unit-test-result-action@v2 - if: always() - with: - files: test-reports/**/*.xml - check_name: Test Results - comment_mode: always - compare_to_earlier_commit: true - job_summary: true - report_individual_runs: true - report_suite_logs: info + run: make rest-api/test-${{ matrix.module }} From 2c1e90b48b80d7a9bff1c2405fc68a23b26c75ae Mon Sep 17 00:00:00 2001 From: Larry Chen Date: Wed, 20 May 2026 14:06:29 +0800 Subject: [PATCH 18/20] ci(rest): restore CI=false + DB env vars so tests use localhost postgres (#1756) --- .github/workflows/rest-lint-and-test.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/rest-lint-and-test.yml b/.github/workflows/rest-lint-and-test.yml index 77c1ee11ea..58685fba33 100644 --- a/.github/workflows/rest-lint-and-test.yml +++ b/.github/workflows/rest-lint-and-test.yml @@ -213,6 +213,16 @@ jobs: - 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 From 590d19e4e3f7afeab490aa70c1307598dd5a2835 Mon Sep 17 00:00:00 2001 From: Larry Chen Date: Wed, 20 May 2026 18:45:38 +0800 Subject: [PATCH 19/20] ci: add per-workflow aggregators + dorny base:main + symmetric escape hatch (#1756) --- .github/workflows/ci.yaml | 40 ++++++++++++++++++++++++++++ .github/workflows/rest-ci.yml | 49 ++++++++++++++++++++++++++++++++--- 2 files changed, 86 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 68fe027c37..e22f2b4a07 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -44,11 +44,15 @@ jobs: 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: @@ -1425,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/rest-ci.yml b/.github/workflows/rest-ci.yml index 487c675b8e..4e8f5af51c 100644 --- a/.github/workflows/rest-ci.yml +++ b/.github/workflows/rest-ci.yml @@ -29,14 +29,17 @@ jobs: 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: - - 'VERSION' - 'rest-api/**' - '.github/workflows/rest-*.yml' @@ -44,12 +47,17 @@ jobs: 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}" + 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" @@ -79,7 +87,7 @@ jobs: uses: ./.github/workflows/rest-build-binaries.yml security-secret-scan: - name: Secret Scan with TruffleHog + name: REST Secret Scan with TruffleHog needs: prepare runs-on: linux-amd64-cpu4 timeout-minutes: 30 @@ -134,3 +142,38 @@ jobs: 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)" From 4136d64a8ec144459bb4cc0d3d228dd188dc2e0e Mon Sep 17 00:00:00 2001 From: Larry Chen Date: Thu, 21 May 2026 13:33:43 +0800 Subject: [PATCH 20/20] test(phase2.3): mixed path-trigger smoke marker (#1756) --- PHASE2_TEST.md | 3 +++ rest-api/PHASE2_TEST.md | 3 +++ 2 files changed, 6 insertions(+) create mode 100644 PHASE2_TEST.md create mode 100644 rest-api/PHASE2_TEST.md diff --git a/PHASE2_TEST.md b/PHASE2_TEST.md new file mode 100644 index 0000000000..746dce053c --- /dev/null +++ b/PHASE2_TEST.md @@ -0,0 +1,3 @@ +# Phase 2 path-trigger smoke marker — mixed (core half) + +Throwaway file for #1756 Phase 2.3 validation. Paired with `rest-api/PHASE2_TEST.md` to trigger both pipelines. Delete after validation. diff --git a/rest-api/PHASE2_TEST.md b/rest-api/PHASE2_TEST.md new file mode 100644 index 0000000000..43dea33cb0 --- /dev/null +++ b/rest-api/PHASE2_TEST.md @@ -0,0 +1,3 @@ +# Phase 2 path-trigger smoke marker — mixed (rest half) + +Throwaway file for #1756 Phase 2.3 validation. Touching files on BOTH sides so both gates fire and both aggregators report green. Delete after validation.