Skip to content

canary gate: a v-scoped channel tag shadows the release tag in _gh_candidate_cut_date, so any cross-repo agent adopting v<M>-<tier> channels is permanently BLOCKED (indeterminate) — blocks #1592 #1046

Description

@don-petry

Blocks petry-projects/.github-private#1592 (D-1). Found by hitting it live while seeding the dev-lead/v139-* channel family, then rolled back. Nothing is currently broken — this is the landmine that must be defused before the v-scoped channels can be adopted for a cross-repo agent.

The bug

_gh_candidate_cut_date (scripts/canary-rollout.sh ~356) enumerates release tags with:

gh api "repos/$repo/git/matching-refs/tags/$agent/v"

That prefix matches every <agent>/v… ref — so it catches the channel tags <agent>/v<M>-<tier> as well as the immutable releases <agent>/v<M>.<m>.<p>. It then returns the first ref whose dereferenced object equals the candidate commit:

if [ "$csha" = "$commit" ]; then _to_z "$cdate"; return 0; fi

A channel tag is lightweight (type=commit), so the else branch sets csha="$obj"; cdate="". And <agent>/v<M>-<tier> always points at a real commit — that is its entire purpose.

So the moment a v-scoped next channel exists for a cross-repo agent, it points at exactly the candidate commit, matches first, and returns an empty cut date.

Ordering makes it deterministic, not intermittent

matching-refs returns refs sorted, and - (0x2D) sorts before . (0x2E), so v139-next always precedes v139.4.0:

62:refs/tags/dev-lead/v139-next     33451103a2cf   commit    <- matched, cdate=""
72:refs/tags/dev-lead/v139.4.0      (annotated)              <- never reached

Observed live

With dev-lead/v139-{next,ring0,ring1,stable} seeded (v139-next → 33451103a2cf, the same commit as release v139.4.0):

candidate (v139-next) = 33451103a2cf  cut=
next->ring0    BLOCKED   dwell=0h/0h  sample=0/0  cum_fail=0
::warning::state=BLOCKED (indeterminate) — could not resolve the candidate's cut date,
so the gate fails closed and holds (not a detected failure; cum_fail=0).

After deleting the four v139-* tags, the same command on the same candidate:

candidate (next) = 33451103a2cf  cut=2026-08-31T23:37:09Z

Same commit, same release tag — the only difference is the presence of the channel tag.

Why this is load-bearing

The gate fails closed on an unresolvable cut date, so the effect is a permanent BLOCKED (indeterminate) hold: an agent that adopts v-scoped channels can never be promoted again. Since #1592's fix requires exactly that adoption for dev-lead (7 live stubs), this silently converts the migration into a fleet-wide promotion freeze.

It is also the per-candidate cumulative window start (#548) — health is measured since the candidate's own cut — so an empty value degrades the whole reliability verdict, not just a display field.

Why it has not bitten yet

Only the cross-repo path is affected. candidate_cut_date branches on host != THIS_REPO:

  • Cross-repo (dev-lead, host .github-private) → _gh_candidate_cut_date, the API path, which yields "" for a lightweight tag. Broken.
  • Same-repo (apply-repo-settings, host .github) → local git for-each-ref with %(creatordate:iso-strict), which for a lightweight tag returns the commit date — non-empty, so it silently "works".

So apply-repo-settings, which now has v1-next/v1-ring0 seeded (#989), is unaffected today purely because it is same-repo. But its same-repo path is also subtly wrong: it reports the candidate's commit date as the cut date, not the release tag's tagger date. Those differ whenever a release is cut some time after the commit — e.g. v139.4.0 commit 22:55:46Z vs tagged 23:37:09Z, a 41-minute skew applied to a dwell window. Worth fixing in the same change.

Acceptance criteria

  • _gh_candidate_cut_date considers only immutable release tags — <agent>/v<major>.<minor>.<patch> — and skips channel tags <agent>/v<M>-<tier> and any other non-release <agent>/v… ref.
  • Same filter applied to the local git for-each-ref path in candidate_cut_date, so both paths agree.
  • The local path prefers the annotated tag's tagger date and does not silently substitute a lightweight tag's commit date.
  • An annotated release tag whose commit equals the candidate resolves correctly even when a lightweight channel tag points at the same commit and sorts earlier. This is the regression test — use the real shape: dev-lead/v139-next (lightweight → X) alongside dev-lead/v139.4.0 (annotated → X).
  • Cover both the cross-repo (API) and same-repo (for-each-ref) paths.
  • After the fix, re-seeding dev-lead/v139-* must yield a resolvable cut= — that is the exit criterion for unblocking #1592 D-1 step 2.

Reproduction

R=petry-projects/.github-private
# seed a v-scoped next channel at the same commit as the newest release
gh api --method POST repos/$R/git/refs -f ref=refs/tags/dev-lead/v139-next \
  -f sha=33451103a2cfa8f64449df9030e6e7628381b406
gh workflow run canary-rollout.yml -R petry-projects/.github -f command=evaluate -f agent=dev-lead
#   => candidate (v139-next) = 33451103a2cf  cut=          <-- empty
gh api --method DELETE repos/$R/git/refs/tags/dev-lead/v139-next
#   => candidate (next) = 33451103a2cf  cut=2026-08-31T23:37:09Z

Related: petry-projects/.github-private#1592, #989, #657 (major-scoped channels, F4), #548, #1049.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugBug reportsdev-leadFor dev-lead agent pickup

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions