From 9a5086cec9b5892afb93e6ae83a0a0bbc8f1b425 Mon Sep 17 00:00:00 2001 From: Stephen Freudenthaler Date: Wed, 12 Aug 2026 14:39:28 -0400 Subject: [PATCH] feat(evergreen-tracks): add a tracks input so latest can be moved out-of-band MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the 2026-08-12 release cut failed to move `latest` (#37025, Hub 403 on the unauthenticated tag walk), there was no supported way to repoint it: this workflow hardcoded `--tracks standard,trailing`, the admin workflow only does taint/hold, and re-running the failed release job replays the pre-fix commit. The only route left was cutting a throwaway release to move a tag. The new `tracks` input defaults to `standard,trailing`, so the daily cron and every existing dispatch behave exactly as before — a `schedule` event carries no inputs at all, which the `||` fallback covers. Dispatching with `tracks=latest` lands behind the same required-reviewer gate as any other manual move, and the apply job still holds the registry lock, so it cannot race the release pipeline's latest-promote. Also widens the moved-summary grep to include `latest`. It matched only `^(standard|trailing)`, so a latest move would have applied correctly but reported moved=false and posted no Slack notice — silent success is the one outcome this workflow is built to avoid. Verified: YAML parses, both steps resolve TRACKS identically (they must — apply diffs its re-derived plan against the approved one), the empty/cron path still yields standard,trailing, and the engine plans a latest-only scope correctly against the live registry in 2 calls. Closes: #37036 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01PqQqV1iv96Db8AzBSWTXV4 --- .../cicd_evergreen-tracks-promote.yml | 33 ++++++++++++++----- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/.github/workflows/cicd_evergreen-tracks-promote.yml b/.github/workflows/cicd_evergreen-tracks-promote.yml index a6b261e50a4..8a3c1f666e5 100644 --- a/.github/workflows/cicd_evergreen-tracks-promote.yml +++ b/.github/workflows/cicd_evergreen-tracks-promote.yml @@ -1,6 +1,12 @@ name: evergreen-tracks-promote -# Advances the floating `standard` / `trailing` tags. `latest` is NOT touched here — -# the release pipeline moves it on every GA cut (cicd_6-release.yml, --tracks latest). +# Advances the floating `standard` / `trailing` tags. `latest` is owned by the release +# pipeline, which moves it on every GA cut (cicd_6-release.yml, --tracks latest), so the +# daily cron deliberately leaves it alone. +# +# The `tracks` dispatch input can still aim this workflow at `latest` — the break-glass +# path for when a release cut moved everything EXCEPT latest (e.g. the 2026-08-12 Hub +# 403, #37025), where the alternative was cutting a throwaway release to repoint a tag. +# It is dispatch-only and lands behind the same approval gate as any other manual move. # # Two triggers, one engine: # * SCHEDULE (daily, 06:00 ET) — runs plan → apply unattended. No human in the loop. @@ -43,6 +49,10 @@ on: description: 'Min age (days) for the trailing track' required: false default: '28' + tracks: + description: 'Tracks to move (comma-separated: latest,standard,trailing)' + required: false + default: 'standard,trailing' permissions: contents: read @@ -81,21 +91,24 @@ jobs: REPO: ${{ github.event.inputs.repo || 'dotcms/dotcms' }} STANDARD_DAYS: ${{ github.event.inputs.standard_days || '14' }} TRAILING_DAYS: ${{ github.event.inputs.trailing_days || '28' }} + TRACKS: ${{ github.event.inputs.tracks || 'standard,trailing' }} # Hub refuses ANONYMOUS pagination past offset 1000, and dotcms/dotcms is # ~79 pages, so even this read-only dry-run needs credentials. DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} DOCKER_TOKEN: ${{ secrets.DOCKER_TOKEN }} run: | echo "repo=$REPO (dry-run plan; trigger=${{ github.event_name }})" - # --tracks standard,trailing: this workflow never moves `latest` — the - # release pipeline owns it and moves it unattended (possibly mid-approval). - # Scoping here keeps the drift check below immune to `latest` churn. + # Default scope is standard,trailing: the release pipeline owns `latest` and + # moves it unattended (possibly mid-approval), so leaving it out keeps the + # drift check below immune to `latest` churn. Dispatching with tracks=latest + # deliberately opts INTO that check — if a GA cut repoints latest while the + # approval sits, apply refuses rather than fighting the release pipeline. # stderr goes to a file, not /dev/null: it must stay off stdout (that is # the plan text, diffed in `apply`) but discarding it cost us a whole # morning once — a Hub 403 surfaced as a bare "exit code 1". if ! PLAN=$(uv run evergreen-tracks promote \ --repo "$REPO" \ - --tracks standard,trailing \ + --tracks "$TRACKS" \ --standard-days "$STANDARD_DAYS" \ --trailing-days "$TRAILING_DAYS" 2>plan.err); then echo "::error::evergreen-tracks promote (dry-run) failed:" @@ -180,6 +193,8 @@ jobs: STANDARD_DAYS: ${{ github.event.inputs.standard_days || '14' }} TRAILING_DAYS: ${{ github.event.inputs.trailing_days || '28' }} APPROVED_PLAN: ${{ needs.plan.outputs.plan }} + # Must match the plan job exactly — `apply` diffs the two plans. + TRACKS: ${{ github.event.inputs.tracks || 'standard,trailing' }} # Needed for the READ path too, not just the mutation — see the plan job. DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} DOCKER_TOKEN: ${{ secrets.DOCKER_TOKEN }} @@ -189,7 +204,7 @@ jobs: # between, fail rather than move tags nobody planned. if ! CURRENT_PLAN=$(uv run evergreen-tracks promote \ --repo "$REPO" \ - --tracks standard,trailing \ + --tracks "$TRACKS" \ --standard-days "$STANDARD_DAYS" \ --trailing-days "$TRAILING_DAYS" 2>current.err); then echo "::error::evergreen-tracks promote (re-plan) failed:" @@ -212,7 +227,7 @@ jobs: echo "repo=$REPO — plan unchanged; applying" uv run evergreen-tracks promote \ --repo "$REPO" \ - --tracks standard,trailing \ + --tracks "$TRACKS" \ --standard-days "$STANDARD_DAYS" \ --trailing-days "$TRAILING_DAYS" \ --apply @@ -223,7 +238,7 @@ jobs: # ("standard (held) -> reconcile to standard_hold"): the engine really # does re-point that tag, so it must not be silently omitted here. MOVES=$(printf '%s\n' "$CURRENT_PLAN" \ - | grep -E '^(standard|trailing) (\(held\) )?-> ' \ + | grep -E '^(latest|standard|trailing) (\(held\) )?-> ' \ | sed 's/ (sha256:.*//' || true) if [ -n "$MOVES" ]; then echo "moved=true" >> "$GITHUB_OUTPUT"