feat(evergreen-tracks): manual-only standard/trailing promotion, drop daily cron - #36652
Conversation
… daily cron standard/trailing now move only on a manual evergreen-tracks-promote dispatch — a human running the action at a maintenance-window tag update is the cadence gate, so tracks never re-point off-window. No cron until the CX team green-lights automatic promotion. latest keeps moving per-release via --tracks latest. - Remove the daily schedule from cicd_evergreen-tracks-promote.yml; dispatch is dry-run by default, apply=true to move tags. - Add regression test pinning --tracks latest to move ONLY latest, never standard/trailing (concrete-scope item 3). - Refresh stale "daily cron" comments in cli.py and cicd_6-release.yml. - Document the cadence in the evergreen-tracks README. The 2-week maintenance-window planner rework in the original issue scope is intentionally NOT built: manual invocation replaces it, so the planner's age thresholds are unchanged. Closes: #36520 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L6UZqGwpjPQrnYyZfYAxiq
|
Claude finished @sfreudenthaler's task in 6m 8s —— View job Code Review — manual-only standard/trailing promotionReviewed the full diff against New Issues
Resolved
Notes (non-blocking, verified correct)
No blocking issues. The three prior findings are genuinely resolved; the only open item is the operational prerequisite (required-reviewer config), which is a repo-settings action rather than a code change. • |
…environment Manual dispatch now runs plan (dry-run, prints the moves) then apply, where the apply job waits on the evergreen-tracks-apply environment's required-reviewer gate. Nothing moves until a human reviews the plan and approves. Drops the now- redundant apply input. latest still moves unattended via the release pipeline. One-time repo setup: Settings > Environments > evergreen-tracks-apply > Required reviewers. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L6UZqGwpjPQrnYyZfYAxiq
…iation path cmd_promote has two tag-mutation paths: plan() moves (scoped by filtering the tracks list) and the held-track reconciliation loop (scoped only by `held = held & wanted`). The existing isolation test only exercised the former. Add a test where standard is held and drifted, asserting a release-style `--tracks latest` run never reconciles it — so the release job stays latest-only even with a hold marker present. Verified: deleting `held = held & wanted` makes this test fail. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L6UZqGwpjPQrnYyZfYAxiq
…ing the wait Review finding #36652: the workflow-level `evergreen-tracks-registry` concurrency lock was held for the ENTIRE promote dispatch — including the minutes-to-days the approval gate sits waiting. cicd_6-release.yml's promote-latest shares that group (cancel-in-progress: false), so a GA shipping while a standard/trailing approval was pending would queue `latest` behind the human, breaking the "latest moves the instant a GA ships" invariant. A job waiting on an environment required-reviewer gate acquires its concurrency slot BEFORE the gate evaluates (community discussion #17401), so job-level concurrency on `apply` alone is insufficient. Split the gate out: a no-lock `gate` job carries the environment approval; `apply` (needs [plan, gate]) holds the registry lock at job level and only enters concurrency evaluation after approval, so the lock covers just the fast mutation — never the human wait. Also documents (Medium finding) that `apply` re-derives its plan from live registry state: the approval authorizes the operation, not the exact digest set the plan job printed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L6UZqGwpjPQrnYyZfYAxiq
Addressed the review findings (commit b8fde8e)🟠 High — approval wait held the shared registry lock, could stall unattended Fix: split the approval into its own lock-free 🟡 Medium — approved plan ≠ what runs. Documented in the workflow + README that Verified end-to-end against |
Enforces the previously-advisory Medium finding: apply now re-derives the plan from live registry state and fails if it no longer matches the approved plan (a GA landed, a hold/taint changed, a digest was re-tagged), so it can never move tags nobody reviewed. On drift it prints both plans and exits non-zero; re-dispatch to review the new plan. - cli.py: log to stdout (not stderr) so the workflow can capture the plan text cleanly (2>/dev/null drops uv's chatter). - plan job: capture the dry-run and expose it as a job output. - apply job: re-derive, compare (sorted — held-track lines come from a set), fail on mismatch; also fail on an empty plan so a broken capture can't silently pass. Verified locally: stdout capture yields clean plan lines, and the comparison passes on identical/reordered/no-move plans and fails on drift/empty. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L6UZqGwpjPQrnYyZfYAxiq
🟡 Medium now enforced, not advisory (commit 2328ea4)Per follow-up: Implementation (kept minimal):
Verified: comparison logic passes on identical/reordered/no-move plans and fails on drift/empty (local); and an end-to-end dispatch (run 29799379094) logged |
…atest churn can't fail the drift check Review finding: the manual dispatch planned all tracks (no --tracks), so the drift check diffed the full plan including `latest`. But `latest` is owned by the release pipeline and moves unattended during the approval wait — exactly what the separate gate job was built to allow. A GA shipping mid-approval would change the re-derived plan's `latest ->` line and fail the apply, forcing a re-dispatch even though nothing about standard/trailing changed, undoing the concurrency decoupling at the plan-diff layer. Scope all three promote invocations (plan capture, apply re-derive, apply) to `--tracks standard,trailing`. This workflow never moves `latest` anyway, so the scoping is also more correct — it matches the workflow's documented purpose and keeps the "one engine, two triggers" model intact. Verified: the scoped plan emits no `latest` line. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L6UZqGwpjPQrnYyZfYAxiq
🟡 Scoped manual promote to
|
|
Tick the box to add this pull request to the merge queue (same as
|
What
standard/trailingtrack tags now move only on a manualevergreen-tracks-promotedispatch — there is no cron. A human running the action at a maintenance-window tag update is the cadence gate, so tracks can never re-point off-window.latestis unaffected: it still moves per-release, unattended, viacicd_6-release.yml(--tracks latest).This reflects the launch decision to hold off on any scheduled promotion until the customer-experience team green-lights automatic (e.g. daily) movement.
Manual dispatch is a two-stage, human-approved flow
planjob — always runs a dry-run and prints the intended tag moves.applyjob — waits on theevergreen-tracks-applyenvironment's required-reviewer gate. Nothing moves until a human reviews the plan output and approves the deployment.Why the 2-week planner rework in the issue was NOT built
The original issue scoped a "compute the target as of a 2-week maintenance window" planner rework. Manual invocation makes that unnecessary — the operator is the scheduler — so the planner's age-threshold logic is unchanged. Simplest thing that satisfies the launch decision.
Changes
cicd_evergreen-tracks-promote.yml— drop the dailyschedule:cron; split intoplan(dry-run) →apply(gated onevergreen-tracks-applyenvironment). Removed the redundantapplyinput.--tracks latestto move onlylatest, never standard/trailing (concrete-scope item 3: a release cut must not roll pods on those tracks).cli.py/cicd_6-release.yml— refreshed stale "daily cron ages standard/trailing" comments.Out of scope (tracked separately)
Test
uv run pytest→ 65 passed (includes the new isolation test).Closes: #36520
🤖 Generated with Claude Code