Summary
Nightly UAT release cells version-pin the aicr binary, the validator images, and the
snapshot-agent image to the release tag, but run the tests/uat/** fixtures and driver
scripts from main. Any breaking change to a lane's test config or harness therefore turns
that lane's release cell permanently red until the next tag - a compatibility artifact, not a
regression signal.
How it surfaced
UAT gcp-h100 training @ v0.21.1 failed six nights running, starting with the first release
cell after #2705 merged (2026-09-14). Example:
run 35451017035.
[cli] command failed: error=[INVALID_REQUEST] failed to parse config from
"/tmp/aicr-uat-tcpxo-config.aicr-uat-35451017035.yaml": yaml: unmarshal errors:
line 60: field gke not found in type config.RecipeConfigurationSpec exitCode=2
#2705 added spec.recipe.configuration.gke.tcpxoInterfaces to the training config and the
GKE field to config.RecipeConfigurationSpec in the same commit. v0.21.1 has neither, and
pkg/config/loader.go
sets dec.KnownFields(true), so the config is rejected outright. The product behaved
correctly - the harness handed it a config from the future.
Note that stripping the offending field would not have been sufficient. The same commit also
made tests/uat/gcp/run export TRAINJOB_RUNTIME=torch-distributed-tcpxo unconditionally and
changed the shared tests/uat/lib/phases.sh, and v0.21.1 ships no such
ClusterTrainingRuntime. The fixture and the harness move together, so there is no clean
seam between "test data" and "test driver" in tests/uat/**.
Current mitigation (deliberately not a fix)
Commit b99c8e453 gates gcp-h100 training to >= v0.22.0 via
nightly-intent-min-versions. That silences this instance and self-resolves when v0.22.0
ships, but it is per-incident: the next breaking fixture change on any lane needs the same
manual gate, and until a human notices, that cell is red and teaching people to ignore red.
Blast radius today is one lane - tests/uat/gcp/tests/h100-training-config.yaml is the only
one of the ten lane configs carrying a spec.recipe.configuration block - but the structural
gap applies to all of them.
Options considered
- Pin
tests/uat/** to the tag for release cells via a second, scoped checkout, keeping
.github/** from main. Gives release cells a coherent meaning (test what that release
actually shipped) and keeps coverage through breaking changes. The blocker: there is no
release/* branch on the remote, so a bug in a tag's UAT harness would be unfixable
without cutting a new tag.
- Repoint the existing
actions/checkout ref: to the tag. Rejected - uses: ./.github/actions/*
resolves against the checked-out workspace, and uat-gcp.yaml has six such references
including install-aicr-release itself, which runs after checkout. This would silently
swap the CI code that installs the release under test.
- Auto-skip a release cell when its fixtures moved since the tag
(git diff --quiet <tag>..HEAD -- tests/uat/<cloud>/ in Validate inputs). Self-maintaining
and makes skew visible as a skip rather than a red run. Two traps: the checkout is shallow
(fetch-depth defaults to 1, no tags present, so git diff <tag>..HEAD exits 128 not 1),
and rc=1 (differs) must be distinguished from rc!=0 (error) or the guard skips on
failure. Raising fetch-depth: 0 would slow every UAT job including the main cells.
- Generalize
nightly-intent-min-versions bookkeeping - keep the per-incident gate but
detect the need automatically and fail the nightly with a clear "gate me" message instead
of a red cell.
Option 1 is the most principled; option 3 is the cheapest that removes the false signal.
Option 1 becomes viable if release branches are introduced.
Acceptance criteria
Notes
- UAT is not one of the seven release quality gates in
RELEASE.md, so this never blocked a
release. The cost is degraded signal and normalized red.
- If Monday's release is numbered anything other than
v0.22.0, the interim gate's floor
must be corrected - an over-high floor silently skips a good release cell, which is the
bad direction.
Summary
Nightly UAT release cells version-pin the
aicrbinary, the validator images, and thesnapshot-agent image to the release tag, but run the
tests/uat/**fixtures and driverscripts from
main. Any breaking change to a lane's test config or harness therefore turnsthat lane's release cell permanently red until the next tag - a compatibility artifact, not a
regression signal.
How it surfaced
UAT gcp-h100 training @ v0.21.1failed six nights running, starting with the first releasecell after #2705 merged (2026-09-14). Example:
run 35451017035.
#2705 added
spec.recipe.configuration.gke.tcpxoInterfacesto the training config and theGKEfield toconfig.RecipeConfigurationSpecin the same commit. v0.21.1 has neither, andpkg/config/loader.gosets
dec.KnownFields(true), so the config is rejected outright. The product behavedcorrectly - the harness handed it a config from the future.
Note that stripping the offending field would not have been sufficient. The same commit also
made
tests/uat/gcp/runexportTRAINJOB_RUNTIME=torch-distributed-tcpxounconditionally andchanged the shared
tests/uat/lib/phases.sh, and v0.21.1 ships no suchClusterTrainingRuntime. The fixture and the harness move together, so there is no cleanseam between "test data" and "test driver" in
tests/uat/**.Current mitigation (deliberately not a fix)
Commit
b99c8e453gatesgcp-h100training to>= v0.22.0vianightly-intent-min-versions. That silences this instance and self-resolves when v0.22.0ships, but it is per-incident: the next breaking fixture change on any lane needs the same
manual gate, and until a human notices, that cell is red and teaching people to ignore red.
Blast radius today is one lane -
tests/uat/gcp/tests/h100-training-config.yamlis the onlyone of the ten lane configs carrying a
spec.recipe.configurationblock - but the structuralgap applies to all of them.
Options considered
tests/uat/**to the tag for release cells via a second, scoped checkout, keeping.github/**frommain. Gives release cells a coherent meaning (test what that releaseactually shipped) and keeps coverage through breaking changes. The blocker: there is no
release/*branch on the remote, so a bug in a tag's UAT harness would be unfixablewithout cutting a new tag.
actions/checkoutref:to the tag. Rejected -uses: ./.github/actions/*resolves against the checked-out workspace, and
uat-gcp.yamlhas six such referencesincluding
install-aicr-releaseitself, which runs after checkout. This would silentlyswap the CI code that installs the release under test.
(
git diff --quiet <tag>..HEAD -- tests/uat/<cloud>/inValidate inputs). Self-maintainingand makes skew visible as a skip rather than a red run. Two traps: the checkout is shallow
(
fetch-depthdefaults to 1, no tags present, sogit diff <tag>..HEADexits 128 not 1),and
rc=1(differs) must be distinguished fromrc!=0(error) or the guard skips onfailure. Raising
fetch-depth: 0would slow every UAT job including themaincells.nightly-intent-min-versionsbookkeeping - keep the per-incident gate butdetect the need automatically and fail the nightly with a clear "gate me" message instead
of a red cell.
Option 1 is the most principled; option 3 is the cheapest that removes the false signal.
Option 1 becomes viable if release branches are introduced.
Acceptance criteria
tests/uat/**fixtures or harness does not produce ared release cell for pre-change tags.
explicit gate) rather than silent.
affected intent.
docs/contributor/uat.mddocuments the chosen contract.gcp-h100training gate added byb99c8e453is removed or subsumed once thegeneral mechanism lands.
Notes
RELEASE.md, so this never blocked arelease. The cost is degraded signal and normalized red.
v0.22.0, the interim gate's floormust be corrected - an over-high floor silently skips a good release cell, which is the
bad direction.