Skip to content

fix(argo): move shepherd's post-deploy-verify verdict onto the artifact channel - #1187

Merged
mashkovd merged 2 commits into
mainfrom
fix/shepherd-verify-status-artifact
Sep 11, 2026
Merged

mashkovd merged 2 commits into
mainfrom
fix/shepherd-verify-status-artifact

Conversation

@mashkovd

Copy link
Copy Markdown
Member

Closes #1186.

Problem

Every shepherd run whose Telegram leg fired appended

⚠️ post-deploy-verify flagged: unknown (verification did not complete)

even when post-deploy-verify ran to completion and found nothing wrong.
Three consecutive runs on 2026-09-11 (d6c4ded3, 94ea75cf, 4f1402b6)
carried that line; all three were healthy.

4f1402b6's verify node: phase=Succeeded, exitCode 0, log

Sleeping 300s for ArgoCD to reconcile post-merge…
Listing applications that became Degraded after 2026-09-11T05:48:23Z…
✓ No persistent Degraded apps detected after shepherd run.

and the global reached workflow scope — status.outputs held
{"name":"degraded_apps","value":""}. The onExit pod started 18s later and
still got the raw {{...}} literal in WORKFLOW_DEGRADED_APPS. The guard at
:1257-1265 rewrote that to the "unknown" text, and since the warning was
appended on any non-empty value, the guard's own placeholder is what
triggered it. A correctly resolved empty string would have been silent — so
the alert appearing was proof the substitution failed, not that anything was
degraded.

This matters because post-deploy-verify is the step meant to catch a merge
that degraded an ArgoCD app, and its all-clear was indistinguishable from its
"I never ran". Seven incident summaries already carry the string.

Change

Drop the dependency on workflow.outputs.parameters resolution inside an exit
handler rather than chase why a recorded global does not resolve there.

  • post-deploy-verify writes verify_status (clean / degraded /
    kubectl-failed) and the filtered degraded-app list into the artifact it
    already ships to /onexit/verify. Both are written before anything that can
    fail.
  • notify-telegram reads both off that artifact, exactly how it already reads
    activity and shepherd_status. A missing file reads as incomplete — the
    state the old string was trying to express, now distinct from a clean run
    instead of identical to a broken substitution.
  • The warning fires only for degraded / kubectl-failed. incomplete is
    silent on a Succeeded workflow (verify has no continueOn, so Succeeded
    means it finished) and reads post-deploy-verify did not run otherwise.
  • Telegram and incident legs now share one VERIFY_NOTE, so they cannot
    disagree.
  • Both workflow.outputs.parameters declarations are removed — they existed
    only to keep the now-removed reference passing Argo's static validator. The
    file now has no output parameters at all, which also retires the one-slot
    budget the 2026-07-06 incident (fix(agents): skip Telegram notify on no-op Succeeded runs #530/fix(agents): pass activity via workdir PVC, not workflow.outputs #531) imposed on it.

Verification

  • argo lint --offline cwft-mctl-agents-shepherd.yaml → clean. This is the
    check that matters: an unresolvable reference here is what put the
    CronWorkflow into SpecError on 2026-05-06 and again on 2026-07-06, and CI
    does not run argo lint (only kubeconform).
    • Worth knowing for future edits: argo lint resolves template tags inside
      rendered script text too, so even naming the old expression in a shell
      comment fails the lint. There is a note in the file saying so.
  • shellcheck on both modified script bodies → clean.
  • yamllint (repo's relaxed config) → only the four pre-existing colons
    warnings, none on changed lines.

Behaviour on a real degradation is unchanged end-to-end: verify still exits 1,
the workflow still fails, and the alert still names the apps verbatim — it now
gets them from degraded.txt instead of an env var.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JoFpun8LVPAKJE6QhHSvdA

…ct channel

Every shepherd run whose Telegram leg fired appended

    ⚠️ post-deploy-verify flagged: unknown (verification did not complete)

even when post-deploy-verify ran to completion and found nothing. Three
consecutive runs on 2026-09-11 (d6c4ded3, 94ea75cf, 4f1402b6) carried that
line; all three were healthy, and 4f1402b6's verify node finished clean at
exit 0 with `degraded_apps: ""` recorded in the workflow's status.outputs.

The onExit pod started 18s later and still received the raw
`{{...}}` literal in WORKFLOW_DEGRADED_APPS. notify-telegram's guard against
raw template syntax rewrote that to the "unknown" text, and because the
warning was appended on any non-empty value, the guard's own placeholder
triggered it. A correctly resolved empty string would have stayed silent —
so the alert appearing was proof the substitution failed, not that the
cluster was degraded.

Rather than chase why a recorded global does not resolve inside an exit
handler, drop the dependency. post-deploy-verify now writes verify_status
(clean / degraded / kubectl-failed) and the degraded-app list into the
artifact it already ships to /onexit/verify, and notify-telegram reads both
the same way it already reads activity and shepherd_status. A missing file
now means "incomplete" — the state that string was trying to express — and
it is distinct from a clean run instead of identical to a broken
substitution.

The warning is emitted only for degraded / kubectl-failed. `incomplete` is
silent on a Succeeded workflow (verify has no continueOn, so Succeeded means
it finished) and reported as "post-deploy-verify did not run" otherwise. The
Telegram and incident legs now share one VERIFY_NOTE so they cannot
disagree.

Both workflow.outputs.parameters declarations go with it — the one on the
spec and the one on shepherd-proposals existed only to keep the removed
reference passing Argo's static validator. With no reference left, this file
has no output parameters at all, which also retires the one-slot budget the
2026-07-06 incident imposed on it.

Verified with `argo lint --offline` (clean) and shellcheck on both modified
script bodies.

Closes #1186

Claude-Session: https://claude.ai/code/session_01JoFpun8LVPAKJE6QhHSvdA
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@claude

claude Bot commented Sep 11, 2026 •

Copy link
Copy Markdown

Claude finished @mashkovd's task in 10m 59s —— View job


Review complete — approved

  • Gather context (diff, file contents)
  • Understand the request
  • Review correctness/logic
  • Review Argo Workflows specifics (params, dependencies)
  • Review security/secrets
  • Post inline findings (none needed)
  • Submit verdict

Verdict: APPROVE — 0 P1, 0 P2, 0 P3.

What I checked

  • The new artifact contract is self-consistent. post-deploy-verify writes verify_status + degraded.txt + degraded_unfiltered.txt into /tmp/onexit, shipped as the single verify-signals artifact ({{workflow.name}}/onexit-degraded.tgz). notify-telegram mounts it at /onexit/verify and reads the exact same three filenames. No stale references to the old degraded-unfiltered artifact name remain.
  • Write-before-failure ordering holds. verify_status is seeded clean and degraded.txt/degraded_unfiltered.txt are truncated at the very top of the script, before the 300s sleep, the kubectl retry loop, or the grace-period recheck — so a mid-script crash still leaves onExit a truthful (if stale) state rather than nothing.
  • The incomplete branch logic is correct. case "$VERIFY_STATUS" in degraded|kubectl-failed) only fires the warning on real findings; incomplete is silenced when WORKFLOW_STATUS = Succeeded (verify has no continueOn, so reaching Succeeded means it ran to completion) and surfaces "post-deploy-verify did not run" otherwise. Matches the DAG: commit → verify is a plain sequential step group with no continueOn on commit, so a commit failure skips verify entirely and the overall status can't be Succeeded in that case.
  • WORKFLOW_DEGRADED_APPS_UNFILTERED gating for the incident-fingerprint resolve (used further down, outside this diff's hunk) still reads from the same degraded_unfiltered.txt filename — untouched by the rename, still correct.
  • Removed workflow.outputs.parameters blocks have no other referents in this file — grepped for workflow.outputs.parameters.degraded_apps and confirmed both remaining hits are inside #-comments (which YAML strips before Argo ever templates them), not inside a source: script body, consistent with the PR's own note that argo lint resolves tags inside rendered script text only.
  • Noted, non-blocking: cwft-mctl-agents-implement.yaml (not touched here) has a comment claiming a lone workflow.outputs.parameters entry "resolves fine" inside an onExit handler — a claim this PR's own investigation now contradicts. Worth a follow-up doc fix if that template ever grows a similar output-parameter pattern, but out of scope for this diff.

No functional, security, or operational-safety issues found. The fix directly addresses the false-positive alert described in #1186 and the artifact-channel approach avoids the workflow.outputs.parameters resolution problem entirely rather than working around it.

@github-actions

Copy link
Copy Markdown
Contributor

Antigravity (agy) review — informational

Model: gemini-3.1-pro-high · commit ccb8ae6.

Severity: P2
File: platform-gitops/argo-workflows/cluster-templates/cwft-mctl-agents-shepherd.yaml (approx line 1007)
Anchor: + printf 'clean' > /tmp/onexit/verify_status
Concrete failure scenario: Eagerly seeding verify_status as clean masks mid-flight failures of the verification step. If the script crashes after this point—for instance, if the pod is OOMKilled or preempted during the 5-minute wait, or if a transient kubectl error returns malformed JSON triggering jq to fail and pipefail/set -e to abort the script—the step will exit with an error. However, the wait container still uploads the artifact, delivering the premature clean status to notify-telegram. Because the status is clean rather than incomplete, the onExit handler silently suppresses the newly added post-deploy-verify did not run warning. The operator receives a generic workflow failure alert and falsely assumes the cluster verification passed, potentially missing a broken cluster state.
Proposed fix: Seed the status as incomplete initially, and explicitly overwrite it with clean only at the very end of the script when verification has fully succeeded.

          mkdir -p /tmp/onexit
-         printf 'clean' > /tmp/onexit/verify_status
+         printf 'incomplete' > /tmp/onexit/verify_status
          : > /tmp/onexit/degraded.txt
...
          echo "✓ No persistent Degraded apps detected after shepherd run."
+         printf 'clean' > /tmp/onexit/verify_status

…e end

agy P2 on this PR, and it is correct: seeding `clean` before the 300s sleep
reintroduces the bug one layer down. Argo's wait container uploads output
artifacts for a FAILED pod too, so a verify step OOMKilled during the sleep —
or tripped by a jq error on malformed kubectl output — would ship a `clean`
verdict it never reached. onExit suppresses the warning on `clean`, so the
operator would get a bare workflow-failure alert and reasonably assume the
post-merge check had passed.

`verify_status` now starts as `incomplete` and is promoted to `clean` by the
last line of the script, after the unfiltered snapshot. Every path that can
fail is above that line, so a killed step reports `incomplete` — the same
thing an absent file reports, which is what makes the two unreachable paths
agree.

The Failed/Error wording changes from "did not run" to "did not complete",
which is now literally true: the step may well have started.

`argo lint --offline` clean; shellcheck clean on both modified scripts.

Claude-Session: https://claude.ai/code/session_01JoFpun8LVPAKJE6QhHSvdA
@claude

claude Bot commented Sep 11, 2026 •

Copy link
Copy Markdown

Claude finished @mashkovd's task in 4m 2s —— View job


Reviewing PR #1187

  • Gather context (diff, file contents)
  • Understand the request
  • Review correctness/logic
  • Review Argo Workflows specifics (params, dependencies)
  • Review security/secrets
  • Post inline findings (if any)
  • Submit verdict

Scope reviewed: git diff 3785a041...4deb3039 on cwft-mctl-agents-shepherd.yaml (149/-111, single file). Commit 4deb3039 is the second commit in this PR and already fixes the P2 the Antigravity bot flagged on commit ccb8ae6a (eager clean seed) — verify_status is now seeded incomplete at the top of post-deploy-verify's script and only promoted to clean on the script's last line, after every fallible step.

Traced the full artifact-channel rewrite:

  • post-deploy-verify writes verify_status / degraded.txt / degraded_unfiltered.txt under /tmp/onexit before the 300s sleep; the kubectl-failed and degraded exit-1 paths correctly overwrite verify_status before exiting, so no path leaves a stale incomplete unaccounted for.
  • notify-telegram reads all three off the consolidated verify-signals artifact; the case gating VERIFY_NOTE to degraded|kubectl-failed (always) vs incomplete (only when WORKFLOW_STATUS != Succeeded) matches the invariant the PR relies on — verify has no continueOn, so a Succeeded workflow can only reach notify-telegram with verify_status already resolved to clean/degraded/kubectl-failed, never incomplete.
  • Grepped for every remaining reference to degraded.txt / degraded_unfiltered.txt / verify_status / the renamed verify-signals artifact (both the producing and consuming template) — all consistent, nothing left pointing at the removed workflow.outputs.parameters.degraded_apps/activity globals.
  • WORKFLOW_DEGRADED_APPS / VERIFY_NOTE / SUMMARY all flow into jq -nc --arg ... before hitting Telegram/incident APIs — no raw shell interpolation into JSON, so no injection surface from an app name containing quotes.
  • cwft-mctl-agents-implement.yaml's one hit for degraded_apps is a comment cross-reference only, not a live dependency on this file's removed output parameter.

No P1/P2 findings. One P3 nit, not worth blocking on:

  • degraded|kubectl-failed vs incomplete messaging text differs slightly from the PR description's own wording ("post-deploy-verify did not run" in the description vs. "post-deploy-verify did not complete" in the actual script at line 1353) — purely cosmetic, doesn't affect behavior.

Could not execute argo lint --offline, shellcheck, or yamllint in this sandbox (no tool access to invoke them here) — relied on manual trace of every artifact/variable reference instead of re-running the checks the PR description already reports as clean.

gh api repos/mctlhq/mctl-gitops/pulls/1187/reviews --method POST -f commit_id="4deb3039c46108dc0f4117a4e6939cb912cdffe7" -f event="APPROVE" -f body="No P1/P2 findings (1 P3 cosmetic nit). Verified artifact-channel rewrite end-to-end and confirmed the agy P2 (eager clean seed) is already fixed in this commit. Good to merge."

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No P1/P2 findings (0 P3). The artifact-channel/verify_status contract is internally consistent: same file names written and read on both ends, order-of-writes puts verify_status/degraded.txt before anything that can fail, and the removed workflow.outputs.parameters blocks have no other consumers in this file. Good to merge.

@github-actions

Copy link
Copy Markdown
Contributor

Antigravity (agy) review — informational

Model: gemini-3.1-pro-high · commit 4deb303.

Severity: P3
File: platform-gitops/argo-workflows/cluster-templates/cwft-mctl-agents-shepherd.yaml
Line: 981
Anchor: + - name: verify-signals
Scenario: The PR changes how the post-deploy-verify step's signals are passed to the notify-telegram exit handler, moving from workflow-level output parameters to an S3 artifact (verify-signals), and introduces new bash logic to serialize and deserialize this state. There is no test visible in this diff to verify that the new script logic works correctly across all scenarios (e.g., degraded apps, kubectl persistent failures, or step crashes).
Fix: Add tests under tests/ to cover the changed behavior of the artifact generation and the corresponding parsing logic in the notification step.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No P1/P2 findings (1 P3 cosmetic nit). Verified artifact-channel rewrite end-to-end and confirmed the agy P2 (eager clean seed) is already fixed in this commit. Good to merge.

@mashkovd
mashkovd merged commit 51b83c2 into main Sep 11, 2026
22 checks passed
@mashkovd
mashkovd deleted the fix/shepherd-verify-status-artifact branch September 11, 2026 06:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

shepherd: post-deploy-verify reports "unknown (verification did not complete)" on runs where it completed cleanly

1 participant