Skip to content

feat(ci): collect UAT cluster debug bundle on failure - #1764

Merged
mchmarny merged 2 commits into
NVIDIA:mainfrom
njhensley:ci/uat-cluster-debug-bundle
Jul 15, 2026
Merged

mchmarny merged 2 commits into
NVIDIA:mainfrom
njhensley:ci/uat-cluster-debug-bundle

Conversation

@njhensley

Copy link
Copy Markdown
Member

Summary

On UAT failure, snapshot live cluster state into a cluster-debug/ bundle (nodes, taints/reboot fingerprint, events, operator CRs incl. Skyhook, operator/check-Job logs) before teardown — so a failed run's artifact explains why it failed, not just what check failed.

Motivation / Context

The existing Upload failure debug artifact captured only runner-local files (report.json, recipe.yaml, snapshot.yaml, dry-run.json) — zero live cluster state. Two recent UAT-AWS runs (29393800033, 29402956254) failed at the expected-resources deployment check on a non-monotonic skyhook re-tuning race (Skyhook /tuning status.status goes complete → in_progress after the readiness gate certifies it, and the tuning reboot can evict the check Job's pod). Root-causing that required reconstructing the timeline from raw CI step logs, because the bundle contained none of the Skyhook status history, node events, or reboot markers needed. This closes that gap.

Fixes: N/A
Related: N/A

Type of Change

  • Build/CI/tooling

Component(s) Affected

  • Other: UAT harness (tests/uat/**) + UAT workflows (.github/workflows/uat-*.yaml)

Implementation Notes

  • tests/uat/lib/collect-debug.sh (new shared, cloud-agnostic lib):
    • collect_cluster_debug — full bundle: a MANIFEST.yaml (failing checks parsed straight from report.json), node conditions + reboot fingerprint (bootID / kernel / Ready lastTransitionTime) + taints, cluster events (reboots, pod evictions), operator CRs (Skyhook full YAML first), and per-namespace describe/events + operator/check-Job logs incl. --previous.
    • capture_skyhook_snapshot — fast, focused snapshot invoked inline the moment a validate failure is detected, seconds after the failing check (while status.status is most likely still in_progress), vs. the teardown-time full bundle taken minutes later.
  • New debug phase wired into tests/uat/{aws,gcp,azure}/run (dispatch + usage/header docs).
  • phase_install now persists the readiness-gate status.status time-series (each attempt, timestamped) into the bundle — so complete → in_progress tuning flips are captured as they happen, not lost to a single snapshot.
  • phase_conformance captures the validate exit code and, on failure, calls capture_skyhook_snapshot before propagating (was: set -e aborted immediately).
  • A failure()-gated Collect cluster debug bundle step (8-minute bound, before teardown) added to uat-{aws,gcp,azure}.yaml, with cluster-debug/** added to the upload artifact.
  • Everything is best-effort — every kubectl call is guarded, the collector never fails the run, and the step is bounded so it can't eat the teardown budget or leak a GPU node. Secrets are not dumped.

Follow-up (out of scope, noted for reviewers): aws/run and gcp/run are functionally identical (differ only in 3 comment lines) and azure/run is the same core plus an az_federated_relogin hook — strong candidate for a further tests/uat/lib/phases.sh consolidation in a dedicated PR.

Testing

# Shell + YAML only; no Go touched.
shellcheck -x tests/uat/lib/collect-debug.sh          # clean
shellcheck tests/uat/{aws,gcp,azure}/run              # clean (SC1091 source-resolution info only)
yamllint -c .yamllint.yaml .github/workflows/uat-*.yaml  # clean
actionlint .github/workflows/uat-*.yaml               # no new findings

Also smoke-tested the collector against the real report.json from run 29393800033 with no cluster reachable: it no-ops gracefully and the MANIFEST.yaml correctly surfaces both failing checks and points at the new time-series artifacts (readinessGateSeries, skyhookAtFailure).

Risk Assessment

  • Low — Isolated, best-effort diagnostics that only run on failure() before teardown; no change to the pass path or to aicr itself. The phase_conformance change preserves the prior failure semantics (still exits non-zero on a validate failure).

Rollout notes: No migration. The new artifact fields appear on the next failed UAT run.

Checklist

  • Tests pass locally (make test with -race) — N/A, no Go changes
  • Linter passes (make lint) — shellcheck + yamllint + actionlint clean
  • I did not skip/disable tests to make CI green
  • I added/updated tests for new functionality — N/A (shell harness; validated via shellcheck + no-cluster smoke test)
  • I updated docs if user-facing behavior changed — N/A (internal UAT harness, not user-facing aicr surface)
  • Changes follow existing patterns in the codebase (mirrors serve_debug() / phase_prep snapshot-debug idioms)
  • Commits are cryptographically signed (git commit -S)
@njhensley
njhensley requested review from a team as code owners July 15, 2026 17:41
@njhensley njhensley self-assigned this Jul 15, 2026
@njhensley njhensley added the theme/ci-dx CI pipelines, developer experience, and build tooling label Jul 15, 2026
@coderabbitai

coderabbitai Bot commented Jul 15, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: 5c2243d9-e45f-44d9-8a80-6eff197f9cf3

📥 Commits

Reviewing files that changed from the base of the PR and between 2e388d1 and 0dc2f85.

📒 Files selected for processing (7)
  • .github/workflows/uat-aws.yaml
  • .github/workflows/uat-azure.yaml
  • .github/workflows/uat-gcp.yaml
  • tests/uat/aws/run
  • tests/uat/azure/run
  • tests/uat/gcp/run
  • tests/uat/lib/collect-debug.sh

📝 Walkthrough

Walkthrough

The PR adds a shared best-effort Kubernetes cluster debug collector and integrates it into AWS, Azure, and GCP UAT runners through a new debug phase. Readiness-gate attempts are persisted, and conformance failures capture Skyhook snapshots. Failure-only workflow steps run the collector before teardown and upload cluster-debug/** with existing failure artifacts.

Estimated code review effort: 4 (Complex) | ~45 minutes

Suggested reviewers: mchmarny

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: collecting UAT cluster debug bundles on failure.
Description check ✅ Passed The description is directly related to the changeset and explains the failure diagnostics and cluster-debug artifact work.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/uat-aws.yaml:
- Around line 627-644: Increase the job timeout budget or revise its budget
calculation to include the new 8-minute “Collect cluster debug bundle” step
while preserving sufficient teardown headroom. Apply the same adjustment to
`.github/workflows/uat-aws.yaml` lines 627-644,
`.github/workflows/uat-azure.yaml` lines 701-716, and
`.github/workflows/uat-gcp.yaml` lines 543-558; update each job’s
`timeout-minutes` declaration and associated budget comment consistently.

In `@tests/uat/lib/collect-debug.sh`:
- Around line 112-122: Update the _cd_section function so each "$@" command is
executed through the repository’s established timeout mechanism, with a bounded
duration that prevents hung kubectl calls from blocking later sections. Preserve
the existing output capture, non-zero suppression, and continuation behavior
while ensuring timeout failures are handled like other command failures.
- Around line 22-35: Update the secrecy claim in the collector documentation
near the kubectl diagnostics to state only that Kubernetes Secret objects are
not fetched; do not claim that pod descriptions or current/previous logs are
free of credentials or PII. Keep the existing best-effort collection behavior
unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: 9ff8ad7c-a6f1-4b56-bb5f-971e1c20ab26

📥 Commits

Reviewing files that changed from the base of the PR and between 2e0bc38 and c5561ee.

📒 Files selected for processing (7)
  • .github/workflows/uat-aws.yaml
  • .github/workflows/uat-azure.yaml
  • .github/workflows/uat-gcp.yaml
  • tests/uat/aws/run
  • tests/uat/azure/run
  • tests/uat/gcp/run
  • tests/uat/lib/collect-debug.sh
Comment thread .github/workflows/uat-aws.yaml
Comment thread tests/uat/lib/collect-debug.sh Outdated
Comment thread tests/uat/lib/collect-debug.sh
@njhensley
njhensley force-pushed the ci/uat-cluster-debug-bundle branch from c5561ee to 2e388d1 Compare July 15, 2026 18:01
@njhensley

Copy link
Copy Markdown
Member Author

Thanks @coderabbitai — all three findings were valid and are addressed in the amended commit:

  1. Job timeout budget (uat-{aws,gcp,azure}.yaml) — bumped each job timeout-minutes 270→280 and updated the budget math to account for the new failure-path debug step (190 UAT + ~30 setup + ~8 debug + ~40 teardown = ~268), restoring ~12m teardown headroom so an oversized failure-path run can't cancel the always()-run Destroy Cluster and leak the GPU node.

  2. Security/privacy claim (collect-debug.sh) — narrowed the claim: it now states only that no Secret objects are fetched, and explicitly warns that describe pods (plain env values) and logs --previous can carry credentials/PII, that the artifact is a private 30-day-retention bundle to be treated as sensitive, and that redaction is deliberately not attempted (fragile for an internal UAT diagnostic on a private repo).

  3. Per-command timeout (_cd_section) — added CLUSTER_DEBUG_CMD_TIMEOUT (30s) and a portable _cd_bounded helper. Every external kubectl/bash -c call now runs under timeout when available (Linux CI) and directly otherwise (macOS dev boxes lack timeout), so one hung call can't starve the high-value per-namespace pod-log sections. Applied to _cd_section, the reboot-fingerprint, capture_skyhook_snapshot, and the per-namespace log loop. Note: timeout can't invoke a shell function, so the function-based fingerprint section is routed directly and self-bounds its own kubectl.

Validated: shellcheck + bash -n clean, yamllint + actionlint clean (no new findings), and the no-cluster smoke test still produces the bundle + MANIFEST correctly (the macOS run also exercised the no-timeout fallback path).

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@njhensley Thanks for the detailed writeup — all three fixes sound solid:

  1. The revised timeout budget (280m with ~12m teardown headroom) properly protects the always() teardown/destroy step from being starved by the new debug step.
  2. Narrowing the security claim to "no Secret objects fetched" while explicitly flagging describe pods and logs --previous as potential credential/PII leak vectors is the right call — better to be honest about the residual risk on a private, short-retention artifact than to overstate the guarantee.
  3. The CLUSTER_DEBUG_CMD_TIMEOUT/_cd_bounded approach with the platform fallback (Linux timeout vs. macOS direct execution) is a sensible way to bound hung calls without breaking dev-box compatibility, and routing the fingerprint function directly since timeout can't wrap a shell function is a good catch.

Nice work getting shellcheck/yamllint/actionlint clean and re-validating the smoke test on both paths. This all looks good to me.

ヽ(・∀・)ノ

@mchmarny mchmarny left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This closes a real gap — the two recent UAT-AWS failures took raw-CI-log archaeology to root-cause, and the three-layer design here (gate time-series during install, inline Skyhook snapshot at failure, full bundle before teardown) captures exactly the non-monotonic signal that made those hard. The shell discipline is good throughout: guarded kubectl, bounded step, unchanged pass-path semantics in the gate loop, and the errexit-suspension trap in the relogin helper is explicitly handled.

Two comments inline: one medium (the azure debug phase skips az_federated_relogin, so an expired kubelogin token yields a silently empty bundle in exactly the long-failure case this targets) and one low (describe-pods env exposure to public artifacts). Neither changes the design; the azure one is worth fixing before merge since it hollows out the feature on one of the three clouds. Branch is behind main — rebase before merge.

Comment thread tests/uat/azure/run Outdated
Comment thread tests/uat/lib/collect-debug.sh
The UAT `Upload failure debug` artifact captured only runner-local files
(report.json, recipe, snapshot); it carried zero live cluster state, so a
deployment-phase failure had to be reconstructed from raw CI step logs.
This adds a cloud-agnostic collector so a failed run's artifact explains
WHY, not just WHAT failed.

- tests/uat/lib/collect-debug.sh: shared `collect_cluster_debug` (full
  bundle) + `capture_skyhook_snapshot` (fast, inline-on-failure). Captures
  a MANIFEST (failing checks parsed from report.json), node conditions +
  reboot fingerprint (bootID/kernel/Ready-transition) + taints, cluster
  events (reboots, pod evictions), operator CRs (Skyhook full YAML first),
  and per-namespace describe/events + operator/check-Job logs incl.
  --previous. Best-effort throughout; never fails the run.
- New `debug` phase wired into tests/uat/{aws,gcp,azure}/run.
- phase_install now persists the readiness-gate status.status time-series
  (each attempt, timestamped) into the bundle, so complete->in_progress
  tuning flips are captured as they happen — not lost to a single
  teardown-time snapshot.
- phase_conformance captures the validate exit code and, on failure,
  snapshots the Skyhook CR + node reboot fingerprint inline (seconds after
  the failing check, while status is most likely still in_progress).
- A failure()-gated `Collect cluster debug bundle` step (8m bound, before
  teardown) added to uat-{aws,gcp,azure}.yaml, with cluster-debug/** added
  to the upload artifact.

Motivated by two UAT-AWS runs (29393800033, 29402956254) that failed at
expected-resources on a non-monotonic skyhook re-tuning race; the current
bundle lacked the Skyhook status history, node events, and reboot markers
needed to root-cause it conclusively.

Signed-off-by: Nathan Hensley <nhensley@nvidia.com>
@njhensley
njhensley force-pushed the ci/uat-cluster-debug-bundle branch from 2e388d1 to 0dc2f85 Compare July 15, 2026 19:19
@github-actions github-actions Bot added size/XL and removed size/L labels Jul 15, 2026
@njhensley

Copy link
Copy Markdown
Member Author

Thanks @mchmarny — both are real and now fixed in the amended commit.

1. Dead credential at collect time (azure/run + AWS/GCP workflows). Spot on — a failure that surfaces after the token expires is exactly the long tuning-race case this bundle targets, so an empty-but-silent bundle would defeat the purpose. Fixed the credential refresh at all three collection entry points:

  • Azure: debug) now runs az_federated_relogin || true; collect_cluster_debug (your suggestion verbatim) — mirrors the phases; no-op outside CI.
  • AWS & GCP: the same gap exists on a long run (the 1h AWS session has expired mid-job before — it caused a teardown leak previously). Added a failure()-gated Refresh AWS credentials / Re-authenticate to GCP for debug collection step immediately before each Collect step, reusing the existing configure-aws-credentials / google-github-actions/auth pattern. Also dropped the now-incorrect "session still valid inside the 1h window" comment on the AWS collect step.

2. describe pods env leak across all namespaces on a public repo. Correct — and the repo being public makes it worse than my earlier "private repo" note claimed (that note was wrong; fixed). describe pods is now limited to the CLUSTER_DEBUG_LOG_NAMESPACES operator/check allowlist (same gate as log collection); every other namespace gets only get pods + get jobs + get events. Went with the allowlist over a sed mask since masking describe output is fragile. The header privacy note now states honestly: no Secret objects fetched, describe scoped to the allowlist, but describe/logs on those namespaces can still carry app-emitted creds/PII, and the artifact is public — treat it as sensitive.

Re-validated: shellcheck / bash-syntax / yamllint / actionlint clean, and the no-cluster smoke test still produces the bundle (incl. the no-timeout fallback path). Appreciate the careful read.

@mchmarny
mchmarny enabled auto-merge (squash) July 15, 2026 19:21
@mchmarny
mchmarny merged commit 28d3c51 into NVIDIA:main Jul 15, 2026
36 checks passed
njhensley added a commit to njhensley/aicr that referenced this pull request Jul 15, 2026
The three UAT runners (tests/uat/{aws,gcp,azure}/run) were near-identical:
aws and gcp differed only in three comment lines, and azure was the same
body plus a federated-session refresh. Every change to a phase had to be
applied three times (as the debug-bundle work in NVIDIA#1764 showed).

Extract the cloud-agnostic body — the prep/install/conformance/train/serve/
verify phases, inject_push_target/serve_debug helpers, env defaults, and the
phase dispatcher — into tests/uat/lib/phases.sh. Each runner is now a thin
shim that sources the lib and calls `uat_main "$@"`.

Cloud-specific behavior is injected through one hook: cloud_refresh_credentials
(default no-op) plus CLOUD_REFRESH_INTERVAL_SECONDS. The Azure runner overrides
the hook with az_federated_relogin (moved verbatim) and lowers the interval;
the readiness gate calls the hook periodically and the debug phase forces it
before collecting. AWS/GCP keep the no-op default (their sessions outlast a
phase and the workflows refresh before teardown).

Net: ~2400 lines across three files → ~790 shared + ~30 per shim. Behavior is
preserved: every extracted phase function is byte-identical to the previous
aws/run (verified), except phase_install's new hook call and two comments
genericized for the multi-cloud context. az_federated_relogin is moved verbatim.
No workflow or collect-debug.sh changes.

Signed-off-by: Nathan Hensley <nhensley@nvidia.com>
njhensley added a commit to njhensley/aicr that referenced this pull request Jul 16, 2026
The three UAT runners (tests/uat/{aws,gcp,azure}/run) were near-identical:
aws and gcp differed only in three comment lines, and azure was the same
body plus a federated-session refresh. Every change to a phase had to be
applied three times (as the debug-bundle work in NVIDIA#1764 showed).

Extract the cloud-agnostic body — the prep/install/conformance/train/serve/
verify phases, inject_push_target/serve_debug helpers, env defaults, and the
phase dispatcher — into tests/uat/lib/phases.sh. Each runner is now a thin
shim that sources the lib and calls `uat_main "$@"`.

Cloud-specific behavior is injected through one hook: cloud_refresh_credentials
(default no-op) plus CLOUD_REFRESH_INTERVAL_SECONDS. The Azure runner overrides
the hook with az_federated_relogin (moved verbatim) and lowers the interval;
the readiness gate calls the hook periodically and the debug phase forces it
before collecting. AWS/GCP keep the no-op default (their sessions outlast a
phase and the workflows refresh before teardown).

Net: ~2400 lines across three files → ~790 shared + ~30 per shim. Behavior is
preserved: every extracted phase function is byte-identical to the previous
aws/run (verified), except phase_install's new hook call and two comments
genericized for the multi-cloud context. az_federated_relogin is moved verbatim.
No workflow or collect-debug.sh changes.

Signed-off-by: Nathan Hensley <nhensley@nvidia.com>
mohityadav8 pushed a commit to mohityadav8/aicr that referenced this pull request Jul 21, 2026
Signed-off-by: Nathan Hensley <nhensley@nvidia.com>
Co-authored-by: Mark Chmarny <mchmarny@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/ci area/tests size/XL theme/ci-dx CI pipelines, developer experience, and build tooling

2 participants