Skip to content

feat(shepherd): post-deploy ArgoCD verify after merge - #143

Merged
mashkovd merged 4 commits into
mainfrom
feat/shepherd-post-deploy-verify
May 6, 2026
Merged

mashkovd merged 4 commits into
mainfrom
feat/shepherd-post-deploy-verify

Conversation

@mashkovd

@mashkovd mashkovd commented May 6, 2026

Copy link
Copy Markdown
Member

Summary

  • Add a fourth step `post-deploy-verify` to `cwft-mctl-agents-shepherd`. Sleeps 300s, then lists Applications whose `status.health.lastTransitionTime` is newer than `workflow.creationTimestamp` AND `status.health.status==Degraded`.
  • Non-empty list → exit 1 → existing `notify-telegram` onExit handler fires a Telegram alert naming the affected apps. Healthy run → step exits 0, no extra noise.
  • No RBAC change: `argo-workflow-sa` already has `applications: get/list/patch` in `platform-gitops/argo-workflows/config/sa-and-rbac.yaml`.

Why

The external-secrets incident (2026-05-01 → 2026-05-07): `eso-cve-patch` implementer-driven merge broke a downstream Application; the shepherd happily flipped it to `merged` and the breakage went unnoticed for 6 days. Without this verify step the shepherd is a one-way door that can't tell when its merge wedged the cluster. See plan `~/.claude/plans/streamed-soaring-fox.md`.

Design notes

  • Threshold = `workflow.creationTimestamp` so pre-existing Degraded apps (carried over from prior incidents) don't false-positive every tick.
  • `set +e` on the kubectl call: a transient API-server hiccup must not flip a healthy run to Failed. Only a non-empty parsed result triggers exit 1.
  • Image: `bitnami/kubectl:latest` — already used elsewhere in this CWFT collection (`wft-deploy-service.yaml`, `wft-create-tenant.yaml` etc.). Has `jq` baked in.
  • Auto-revert intentionally deferred to v2 (needs commit-SHA → owning-Application correlation). v1 = signal only.

Test plan

  • `python3 -c yaml.safe_load_all` parses cleanly.
  • Templates declared: `shepherd-proposals`, `clone-gitops`, `run-shepherd`, `commit-and-push`, `post-deploy-verify`, `notify-telegram` (6 total).
  • After merge, dry-run a shepherd workflow against mctl-preprod (manual submit with `dry_run=true` parameter, but still through this CWFT) and confirm `post-deploy-verify` step runs, sleeps, exits 0 cleanly.
  • Synthetic-Degraded smoke: temporarily put any tenant Application into Degraded (e.g. invalid `targetRevision`) before the next scheduled shepherd tick → confirm `post-deploy-verify` exits 1 → Telegram alert lists that Application.
  • Verify the existing `commit-and-push` step's no-op short-circuit still works (cron tick with nothing to merge → `post-deploy-verify` should sleep + check + exit 0, not fail because there's nothing to verify).

After the shepherd commits .status.yaml transitions back to gitops main
(merge / rejected / review-fixing), pause 300s for ArgoCD to reconcile
and then list any Application that flipped to Health=Degraded since
the workflow started. A non-empty result fails the workflow, which the
existing notify-telegram onExit handler turns into a Telegram alert
naming the affected apps.

Closes the gap exposed by the external-secrets incident
2026-05-01 → 2026-05-07: the eso-cve-patch implementer-driven merge
broke a downstream Application and the shepherd had no signal to halt
its run or alert anyone — the breakage stayed Degraded for 6 days. See
~/.claude/plans/streamed-soaring-fox.md for full context.

Threshold = workflow.creationTimestamp so that pre-existing Degraded
apps (carried over from previous incidents) don't false-positive every
shepherd run. argo-workflow-sa already has applications: get/list/patch
in platform-gitops/argo-workflows/config/sa-and-rbac.yaml — no RBAC
change needed.

Auto-revert is intentionally NOT in this PR. v2 follow-up will need
commit-SHA → owning-Application correlation; for now the Telegram
alert lands and an operator decides.
@mashkovd

mashkovd commented May 6, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ffc09a46ae

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +434 to +436
if [ "$RC" -ne 0 ] || [ -z "$RAW" ]; then
echo "⚠️ kubectl get applications failed (rc=$RC); skipping verify rather than failing run."
exit 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Treat non-transient verify query failures as errors

This branch exits 0 for any kubectl get applications.argoproj.io failure, not just transient API hiccups. In cases like missing RBAC, missing CRD, or broken cluster auth, the post-deploy safeguard is silently bypassed on every run, so merges keep succeeding without the intended degraded-app check. The workflow should fail (or at least explicitly alert) on persistent query failures and only suppress clearly transient errors.

Useful? React with 👍 / 👎.

Comment on lines +447 to +449
echo "❌ Newly Degraded ArgoCD apps after shepherd run: $BAD"
echo "Threshold (workflow.creationTimestamp): $THRESHOLD"
exit 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Pass degraded app names into the exit notification

The degraded application list is only echoed in this step and never exported as a workflow output, so the notify-telegram onExit template cannot include the affected app names. Because onExit runs in a separate pod and currently sends only status/subject/duration/URL, operators won't receive the app list in the alert even when verify fails, which undermines the stated goal of naming impacted apps in notifications.

Useful? React with 👍 / 👎.

Codex review on PR #143 surfaced two findings, both addressed here:

P1 — non-transient kubectl failures were silently bypassed.
The previous `set +e` + `exit 0` fallback treated EVERY kubectl error
as transient, so missing RBAC, missing CRD, or broken cluster auth
would silently disable the safeguard on every shepherd run. Replace
with a 3× retry loop with 5s backoff: a single transient hiccup is
absorbed, but persistent failures escalate to exit 1 so the operator
notices the safeguard itself is broken.

P2 — affected app names weren't reaching the Telegram alert.
notify-telegram runs in a separate pod (onExit) and only sees
status/subject/duration/URL. The verify step's degraded list was
echoed to logs but never exported, so operators had to dig through
workflow logs to find which apps the shepherd had broken — defeats
the stated goal of naming impacted apps in alerts.

Fix: write the degraded list to /tmp/degraded.txt, declare it as a
step output parameter, hoist it to a workflow-level output via the
shepherd-proposals template, then read it as
{{workflow.outputs.parameters.degraded_apps}} in notify-telegram and
append a "⚠️ post-deploy-verify flagged: …" line when non-empty.
Empty case (clean run / verify never ran) keeps the existing alert
shape unchanged.
@mashkovd

mashkovd commented May 6, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a3294e0741

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

RC=1
for attempt in 1 2 3; do
set +e
RAW=$(kubectl get applications.argoproj.io -A -o json 2>/dev/null)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Query applications in an authorized scope

kubectl get applications.argoproj.io -A requests a cluster-wide list, but this workflow’s service account is only bound via a namespaced RoleBinding in argocd (platform-gitops/argo-workflows/config/sa-and-rbac.yaml, lines 76-100). With that RBAC, the call will be forbidden, the 3 retries will all fail, and post-deploy-verify exits 1 on every run even when nothing actually degraded. Use -n argocd (if all Applications live there) or grant a ClusterRole/ClusterRoleBinding for cluster-wide listing.

Useful? React with 👍 / 👎.

Codex P1 on PR #143 second pass: argo-workflow-sa is bound via a
namespaced RoleBinding in the argocd namespace
(platform-gitops/argo-workflows/config/sa-and-rbac.yaml lines 73-100),
NOT a ClusterRoleBinding. A cluster-wide `kubectl get
applications.argoproj.io -A` would 403 on every attempt — all 3
retries fail, the persistent-failure branch fires, and the step
exits 1 on every shepherd run regardless of actual cluster health,
exactly the silent-bypass anti-pattern the previous fix was meant
to eliminate.

All ArgoCD Applications live in the `argocd` namespace, so scoping
the kubectl call to `-n argocd` matches the existing RBAC without
needing to widen the binding.
@mashkovd

mashkovd commented May 6, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 47589a4698

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

# plus a buffer for the per-resource health re-evaluation pass
# that runs on the next tick after a sync.
echo "Sleeping 300s for ArgoCD to reconcile post-merge…"
sleep 300

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Move post-merge sleep out of global GitOps mutex

This new sleep 300 runs after commit-and-push, but the workflow still holds the top-level synchronization.mutex (mctl-gitops-main-writes) until completion, so every shepherd run now blocks other workflows that share that mutex for an extra 5 minutes even though no Git write is happening. That can unnecessarily queue run/implement workflows and slow down merge/fix cycles; the verify logic should run outside the global write lock (or use a narrower lock scope around commit/push only).

Useful? React with 👍 / 👎.

BAD=$(printf '%s' "$RAW" | jq -r --arg ts "$THRESHOLD" '
.items[]
| select(.status.health.status == "Degraded")
| select((.status.health.lastTransitionTime // "") > $ts)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Compare health transition times as timestamps, not strings

The degraded-app filter uses lexical string comparison for RFC3339 timestamps, which can misorder valid timestamps when one value includes fractional seconds and the other does not (for example, ...:30.5Z sorts before ...:30Z). In that case, apps that transitioned after workflow start can be missed and the safeguard silently fails; parse both values to epoch time (e.g., with fromdateiso8601) before comparing.

Useful? React with 👍 / 👎.

Codex P2 ×2 on PR #143 third pass:

1. Mutex held during 300s sleep blocked unrelated workflows.
   mctl-gitops-main-writes was at workflow spec-level so the entire
   shepherd run (clone → shepherd → commit-push → 300s verify sleep)
   held the global lock. cwft-mctl-agents-run and -implement share
   this mutex and would queue for the full 1-2 min duration of every
   shepherd tick, even though only commit-and-push actually writes
   to gitops main. Hoist the mutex to the commit-and-push template
   only — race-against-other-writers is still handled there by the
   existing 5×retry-with-rebase loop.

2. Lexical RFC3339 comparison breaks on fractional seconds.
   `(.status.health.lastTransitionTime // "") > $ts` is bytewise
   string comparison. "...:30.5Z" sorts BEFORE "...:30Z" because
   "." (0x2E) < "Z" (0x5A). Apps that transitioned just after
   workflow start with sub-second precision would slip through the
   filter. Replace with fromdateiso8601 epoch comparison; null /
   empty lastTransitionTime defaults to 1970-01-01 so unparsed apps
   never match the threshold.
@mashkovd

mashkovd commented May 6, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Chef's kiss.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@mashkovd
mashkovd merged commit a365f35 into main May 6, 2026
@mashkovd
mashkovd deleted the feat/shepherd-post-deploy-verify branch May 6, 2026 23:52
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.

1 participant