Accept renamed nvidia-smi banner fields #2972
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. | |
| # | |
| # Licensed under the Apache License, Version 2.0 (the "License"); | |
| # you may not use this file except in compliance with the License. | |
| # You may obtain a copy of the License at | |
| # | |
| # http://www.apache.org/licenses/LICENSE-2.0 | |
| # | |
| # Unless required by applicable law or agreed to in writing, software | |
| # distributed under the License is distributed on an "AS IS" BASIS, | |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| # See the License for the specific language governing permissions and | |
| # limitations under the License. | |
| # Aggregate merge gate — the ONLY required status check in the main branch | |
| # ruleset. Every PR-gating CI check is either run or explicitly skipped here, | |
| # and the final "gate" job aggregates all results into a single pass/fail. | |
| # | |
| # To add a new gating check: | |
| # 1. Add a real job + companion -skip job with inverse if: conditions | |
| # 2. Add both job names to the gate job's needs: list | |
| # 3. No ruleset change needed — "gate" is the only required check. | |
| # | |
| # GPU tests are intentionally excluded (advisory only) until flake rate is | |
| # assessed. They can be added later with the same real/skip pattern. | |
| name: Merge Gate | |
| on: | |
| pull_request: | |
| branches: | |
| - main | |
| workflow_dispatch: {} | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| # --------------------------------------------------------------------------- | |
| # Path classification — determines which checks run vs skip | |
| # --------------------------------------------------------------------------- | |
| check-paths: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 2 | |
| outputs: | |
| code: ${{ steps.code.outputs.code }} | |
| actions: ${{ steps.changes.outputs.actions }} | |
| deps: ${{ steps.changes.outputs.deps }} | |
| renovate: ${{ steps.changes.outputs.renovate }} | |
| bom: ${{ steps.changes.outputs.bom }} | |
| tuning: ${{ steps.changes.outputs.tuning }} | |
| notices: ${{ steps.changes.outputs.notices }} | |
| steps: | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| persist-credentials: false | |
| # Detect targeted path categories (default: any match = true) | |
| - uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2 | |
| id: changes | |
| with: | |
| filters: | | |
| actions: | |
| - '.github/workflows/**' | |
| - '.github/actions/**' | |
| deps: | |
| - 'go.mod' | |
| - 'go.sum' | |
| - 'vendor/**' | |
| - '.github/workflows/verify-licenses.yaml' | |
| renovate: | |
| # Watch every input the verify-renovate job consumes — including | |
| # the Makefile target that runs the validator — so changes to the | |
| # Make target or its pinned image trigger the gate. | |
| - '.github/renovate.json5' | |
| - 'Makefile' | |
| bom: | |
| # Inputs to the committed-BOM version-freshness check. The full | |
| # `tests` job already runs it for code changes, but a docs-only | |
| # PR editing the committed BOM (or a registry change that somehow | |
| # lands as a docs-only diff) yields code=false and skips `tests`, | |
| # so gate the freshness test on the doc + registry directly. The | |
| # generator sources are included so a generator change also | |
| # re-runs the check independently of the full suite. | |
| - 'docs/user/container-images.md' | |
| - 'recipes/registry.yaml' | |
| - 'tools/bom/**' | |
| - 'pkg/bom/**' | |
| tuning: | |
| # Inputs that can change the committed tuning-status table | |
| # WITHOUT tripping pkg/tuning's TestCompute_Structure — which runs | |
| # in the `tests` job on any non-docs change and already asserts | |
| # every row's service, accelerator, profile, setup-name and | |
| # tuning-name. That test is version-agnostic, so the only table | |
| # changes it misses are a manifest pin version bump, a | |
| # renderer/extractor change, or a hand edit of the committed doc. | |
| # Structural changes driven by overlays, mixins, registry or | |
| # pkg/recipe already fail that test, so they are deliberately NOT | |
| # listed here (keeping this gate off unrelated recipe PRs). | |
| - 'docs/integrator/components/nodewright.md' | |
| - 'recipes/components/nodewright-customizations/**' | |
| - 'pkg/tuning/**' | |
| - 'tools/tuning/**' | |
| notices: | |
| # Inputs to the committed THIRD_PARTY_NOTICES.md freshness check. | |
| # The file is the union of every vendored dependency's license | |
| # across the released OS/arch matrix, so any dependency-graph | |
| # change can alter it. Watch the dependency manifests, the vendor | |
| # tree, and the generator itself (plus the committed file, so a | |
| # hand edit is re-verified). .goreleaser.yaml is included because | |
| # the generator asserts its PLATFORMS matrix matches the release | |
| # targets defined there — a new goos/goarch must re-run this check. | |
| # Makefile is included because the `notices`/`notices-check` | |
| # recipes are the generation/check contract — a change to them | |
| # must not be able to skip its own gate. .settings.yaml is included | |
| # because the generator's output depends on its pins: a | |
| # linting.go_licenses bump can change license classification/URL | |
| # derivation, and a languages.go bump changes the `go list std` | |
| # ignore set. This is a standalone job — the full `tests` suite | |
| # does NOT regenerate the file. | |
| - 'Makefile' | |
| - '.settings.yaml' | |
| - '.goreleaser.yaml' | |
| - 'go.mod' | |
| - 'go.sum' | |
| - 'vendor/**' | |
| - 'tools/generate-notices' | |
| - 'THIRD_PARTY_NOTICES.md' | |
| # code = true iff at least one changed file is NOT docs/markdown/LICENSE. | |
| # | |
| # Exclusion idiom: dorny's `predicate-quantifier: 'every'` requires a file | |
| # to match EVERY pattern in the filter, so a file matches `code` only when | |
| # it matches '**' AND none of the docs patterns. The filter output is true | |
| # when ANY changed file is code — so a docs-only PR yields code=false. | |
| # | |
| # NOTE: 'every' does NOT mean "every changed file matches"; the prior | |
| # `only: [**.md, docs/**, LICENSE]` form required a single file to match | |
| # all three at once (impossible), so it was always false and code was | |
| # always true — docs-only PRs ran full qualification. '**' matches | |
| # dotfiles too (dorny sets picomatch dot:true), so '.golangci.yaml', | |
| # '.settings.yaml', etc. still count as code. | |
| - uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2 | |
| id: code | |
| with: | |
| predicate-quantifier: 'every' | |
| filters: | | |
| code: | |
| - '**' | |
| - '!**.md' | |
| - '!docs/**' | |
| - '!LICENSE' | |
| # --------------------------------------------------------------------------- | |
| # Qualification (Test, Lint, CLI E2E, E2E, Security Scan) | |
| # --------------------------------------------------------------------------- | |
| tests: | |
| needs: [check-paths] | |
| if: needs.check-paths.outputs.code == 'true' | |
| uses: ./.github/workflows/qualification.yaml | |
| permissions: | |
| actions: read | |
| contents: read | |
| id-token: write | |
| security-events: write | |
| with: | |
| coverage_report: true | |
| tests-skip: | |
| needs: [check-paths] | |
| if: needs.check-paths.outputs.code != 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 1 | |
| steps: | |
| - run: echo "Docs/non-code change — qualification tests not required" | |
| # --------------------------------------------------------------------------- | |
| # CodeQL (analyze) | |
| # --------------------------------------------------------------------------- | |
| analyze: | |
| needs: [check-paths] | |
| if: needs.check-paths.outputs.code == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| permissions: | |
| actions: read | |
| contents: read | |
| security-events: write | |
| steps: | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| persist-credentials: false | |
| - uses: ./.github/actions/load-versions | |
| id: versions | |
| - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 | |
| with: | |
| go-version: ${{ steps.versions.outputs.go }} | |
| cache: false | |
| - uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 | |
| with: | |
| languages: go | |
| - run: go build ./... | |
| env: | |
| GOFLAGS: -mod=vendor | |
| - uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 | |
| analyze-skip: | |
| needs: [check-paths] | |
| if: needs.check-paths.outputs.code != 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 1 | |
| steps: | |
| - run: echo "Docs/non-code change — CodeQL analysis not required" | |
| # --------------------------------------------------------------------------- | |
| # Malware scan (ClamAV) | |
| # --------------------------------------------------------------------------- | |
| malware-scan: | |
| needs: [check-paths] | |
| if: needs.check-paths.outputs.code == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| permissions: | |
| actions: read | |
| contents: read | |
| security-events: write | |
| steps: | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| persist-credentials: false | |
| - uses: ./.github/actions/malware-scan | |
| with: | |
| scan_path: '.' | |
| category: 'clamav' | |
| malware-scan-skip: | |
| needs: [check-paths] | |
| if: needs.check-paths.outputs.code != 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 1 | |
| steps: | |
| - run: echo "Docs/non-code change — malware scan not required" | |
| # --------------------------------------------------------------------------- | |
| # Actionlint (GitHub Actions workflow linting) | |
| # --------------------------------------------------------------------------- | |
| actionlint: | |
| needs: [check-paths] | |
| if: needs.check-paths.outputs.actions == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| persist-credentials: false | |
| - name: Install actionlint | |
| # Pin actionlint to a specific version and verify the downloaded | |
| # binary's SHA256 against the upstream-published checksum file. | |
| # Replaces `bash <(curl … main …)`, which executed a mutable script | |
| # from the default branch and downloaded the binary with no | |
| # checksum verification (the upstream installer pipes | |
| # `curl … | tar xvz` directly). | |
| env: | |
| ACTIONLINT_VERSION: '1.7.11' | |
| run: | | |
| set -euo pipefail | |
| BASE="https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}" | |
| TAR="actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz" | |
| TMP="$(mktemp -d)" | |
| curl -fsSL -o "${TMP}/${TAR}" "${BASE}/${TAR}" | |
| curl -fsSL -o "${TMP}/checksums.txt" "${BASE}/actionlint_${ACTIONLINT_VERSION}_checksums.txt" | |
| EXPECTED=$(awk -v t="${TAR}" '$2 == t {print $1}' "${TMP}/checksums.txt") | |
| if [[ -z "${EXPECTED}" ]]; then | |
| echo "::error::no checksum entry for ${TAR} in upstream checksums.txt" | |
| exit 1 | |
| fi | |
| ACTUAL=$(sha256sum "${TMP}/${TAR}" | awk '{print $1}') | |
| if [[ "${ACTUAL}" != "${EXPECTED}" ]]; then | |
| echo "::error::actionlint checksum mismatch: expected ${EXPECTED}, got ${ACTUAL}" | |
| exit 1 | |
| fi | |
| tar -xzf "${TMP}/${TAR}" -C "${TMP}" actionlint | |
| mv "${TMP}/actionlint" "${PWD}/actionlint" | |
| rm -rf "${TMP}" | |
| echo "${PWD}" >> "$GITHUB_PATH" | |
| - name: Run actionlint | |
| run: actionlint -color -shellcheck= | |
| actionlint-skip: | |
| needs: [check-paths] | |
| if: needs.check-paths.outputs.actions != 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 1 | |
| steps: | |
| - run: echo "No workflow/action changes — actionlint not required" | |
| # --------------------------------------------------------------------------- | |
| # License verification | |
| # --------------------------------------------------------------------------- | |
| verify-licenses: | |
| needs: [check-paths] | |
| if: needs.check-paths.outputs.deps == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| persist-credentials: false | |
| - uses: ./.github/actions/load-versions | |
| id: versions | |
| - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 | |
| with: | |
| go-version: ${{ steps.versions.outputs.go }} | |
| cache: true | |
| cache-dependency-path: | | |
| go.sum | |
| vendor/modules.txt | |
| - name: Install go-licenses | |
| run: go install github.com/google/go-licenses/v2@${{ steps.versions.outputs.go_licenses }} | |
| - name: Report licenses | |
| env: | |
| GOFLAGS: -mod=vendor | |
| run: | | |
| echo "=== Dependency Licenses ===" | |
| go-licenses report ./... 2>/dev/null | sort -t',' -k3 | column -t -s',' | |
| echo "" | |
| echo "=== License Summary ===" | |
| go-licenses report ./... 2>/dev/null | cut -d',' -f3 | sort | uniq -c | sort -rn | |
| - name: Check licenses | |
| env: | |
| GOFLAGS: -mod=vendor | |
| run: make license-check | |
| verify-licenses-skip: | |
| needs: [check-paths] | |
| if: needs.check-paths.outputs.deps != 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 1 | |
| steps: | |
| - run: echo "No dependency changes — license check not required" | |
| # --------------------------------------------------------------------------- | |
| # Renovate config validation | |
| # --------------------------------------------------------------------------- | |
| verify-renovate: | |
| needs: [check-paths] | |
| if: needs.check-paths.outputs.renovate == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| persist-credentials: false | |
| - name: Validate .github/renovate.json5 | |
| run: make lint-renovate | |
| verify-renovate-skip: | |
| needs: [check-paths] | |
| if: needs.check-paths.outputs.renovate != 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 1 | |
| steps: | |
| - run: echo "No Renovate config changes — validation not required" | |
| # --------------------------------------------------------------------------- | |
| # BOM version freshness — ensures the committed container-images.md matches | |
| # the registry pins even on docs-only PRs (which skip the full `tests` job). | |
| # --------------------------------------------------------------------------- | |
| bom-freshness: | |
| needs: [check-paths] | |
| if: needs.check-paths.outputs.bom == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| persist-credentials: false | |
| - uses: ./.github/actions/load-versions | |
| id: versions | |
| - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 | |
| with: | |
| go-version: ${{ steps.versions.outputs.go }} | |
| cache: false | |
| - name: Verify committed BOM versions and variants match the recipes | |
| env: | |
| GOFLAGS: -mod=vendor | |
| # `go test -run <name>` exits 0 with "no tests to run" if a test is | |
| # renamed or removed, which would let this gate silently no-op. Assert | |
| # each named test actually executed by requiring its PASS line — a | |
| # docs-only PR must not be able to forge either the Components table | |
| # or the Version variants table. | |
| run: | | |
| set -uo pipefail | |
| out=$(go test ./tools/bom/... -run '^(TestCommittedBOMVersionsMatchRegistry|TestCommittedBOMVariantsMatchRecipePins)$' -v -count=1 2>&1) && rc=0 || rc=$? | |
| echo "$out" | |
| if [ "$rc" -ne 0 ]; then | |
| exit "$rc" | |
| fi | |
| for t in TestCommittedBOMVersionsMatchRegistry TestCommittedBOMVariantsMatchRecipePins; do | |
| if ! grep -q "^--- PASS: $t" <<<"$out"; then | |
| echo "::error::$t did not execute (renamed or removed?); the BOM freshness gate would silently pass. Update this job to run the freshness check." | |
| exit 1 | |
| fi | |
| done | |
| bom-freshness-skip: | |
| needs: [check-paths] | |
| if: needs.check-paths.outputs.bom != 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 1 | |
| steps: | |
| - run: echo "No BOM/registry changes — BOM freshness check not required" | |
| # --------------------------------------------------------------------------- | |
| # Tuning-status doc freshness — ensures the committed tuning table in | |
| # docs/integrator/components/nodewright.md matches the recipe catalog and the | |
| # nodewright package pins, on PRs touching its inputs (mirrors bom-freshness). | |
| # --------------------------------------------------------------------------- | |
| tuning-freshness: | |
| needs: [check-paths] | |
| if: needs.check-paths.outputs.tuning == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| persist-credentials: false | |
| - uses: ./.github/actions/load-versions | |
| id: versions | |
| - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 | |
| with: | |
| go-version: ${{ steps.versions.outputs.go }} | |
| cache: false | |
| - name: Verify committed tuning-status table is up to date | |
| env: | |
| GOFLAGS: -mod=vendor | |
| # `make tuning-check` regenerates the table into a temp dir and splices | |
| # it into the doc, then fails if the working tree differs — so a stale | |
| # committed table (or missing splice markers) fails closed here. | |
| run: make tuning-check | |
| tuning-freshness-skip: | |
| needs: [check-paths] | |
| if: needs.check-paths.outputs.tuning != 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 1 | |
| steps: | |
| - run: echo "No tuning-status inputs changed — freshness check not required" | |
| # --------------------------------------------------------------------------- | |
| # Third-party notices freshness — ensures the committed THIRD_PARTY_NOTICES.md | |
| # matches the vendored dependency graph across every released OS/arch target, | |
| # on PRs touching its inputs (go.mod/go.sum/vendor + the generator). | |
| # --------------------------------------------------------------------------- | |
| notices-freshness: | |
| needs: [check-paths] | |
| if: needs.check-paths.outputs.notices == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| persist-credentials: false | |
| - uses: ./.github/actions/load-versions | |
| id: versions | |
| - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 | |
| with: | |
| go-version: ${{ steps.versions.outputs.go }} | |
| cache: true | |
| cache-dependency-path: | | |
| go.sum | |
| vendor/modules.txt | |
| - name: Install go-licenses | |
| run: go install github.com/google/go-licenses/v2@${{ steps.versions.outputs.go_licenses }} | |
| - name: Verify committed THIRD_PARTY_NOTICES.md is up to date | |
| env: | |
| GOFLAGS: -mod=vendor | |
| # `make notices-check` regenerates the notices file (union across the | |
| # release OS/arch matrix, so it is host-independent) and fails if the | |
| # working tree differs — a stale committed file fails closed here. | |
| run: make notices-check | |
| notices-freshness-skip: | |
| needs: [check-paths] | |
| if: needs.check-paths.outputs.notices != 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 1 | |
| steps: | |
| - run: echo "No dependency/generator changes — notices freshness check not required" | |
| # --------------------------------------------------------------------------- | |
| # Aggregate gate — the single required status check | |
| # --------------------------------------------------------------------------- | |
| gate: | |
| if: always() | |
| needs: | |
| - check-paths | |
| - tests | |
| - tests-skip | |
| - analyze | |
| - analyze-skip | |
| - malware-scan | |
| - malware-scan-skip | |
| - actionlint | |
| - actionlint-skip | |
| - verify-licenses | |
| - verify-licenses-skip | |
| - verify-renovate | |
| - verify-renovate-skip | |
| - bom-freshness | |
| - bom-freshness-skip | |
| - tuning-freshness | |
| - tuning-freshness-skip | |
| - notices-freshness | |
| - notices-freshness-skip | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 1 | |
| steps: | |
| - name: Check gate | |
| run: | | |
| echo "Gate results:" | |
| echo '${{ toJSON(needs) }}' | jq -r 'to_entries[] | "\(.key): \(.value.result)"' | |
| # check-paths must succeed — if it fails, all downstream jobs are | |
| # skipped (not failed), which would silently pass the gate. | |
| if [ "${{ needs.check-paths.result }}" != "success" ]; then | |
| echo "::error::check-paths did not succeed (result: ${{ needs.check-paths.result }})" | |
| exit 1 | |
| fi | |
| # Fail if ANY other dependency failed or was cancelled. | |
| # Skipped is OK — it means the inverse job ran instead. | |
| if echo '${{ toJSON(needs.*.result) }}' | jq -e 'map(select(. == "failure" or . == "cancelled")) | length > 0' > /dev/null; then | |
| echo "::error::One or more gating checks failed or were cancelled" | |
| exit 1 | |
| fi |