Skip to content

fix(ci): guard workflow triggers that are inert on dev until they reach main (#2814) - #2816

Merged
vybe merged 2 commits into
devfrom
fix/2814-workflow-trigger-parity
Sep 15, 2026
Merged

vybe merged 2 commits into
devfrom
fix/2814-workflow-trigger-parity

Conversation

@dolho

@dolho dolho commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Issue Status on PR Merge has not run since #2769 changed its trigger to pull_request_target — zero runs across six merges, three carrying a valid Fixes #N, every issue left in status-in-progress until a human moved it. Not red: absent. GitHub executes the base branch's copy of a pull_request_target workflow but registers the trigger from the default branch, and main still says pull_request. Neither copy can start a run.

Decision (2026-09-15): wait for the release cut rather than hotfix main. So this PR ships the guard for the class, not the trigger — AC#1 closes when dev → main lands, and the guard makes that delay a recorded decision instead of a silent one (AC#3).

Changes

  • tests/unit/test_2814_workflow_trigger_parity.py — for every workflow, each default-branch-registered event (schedule, workflow_dispatch, repository_dispatch, workflow_run, pull_request_target, issues, release, …) declared on this branch must also be declared by main's copy, or be in ACCEPTED_UNTIL_RELEASE with a reason. push/pull_request are out of scope by design — they read the ref being built. A stale allowlist entry fails too, so the record prunes itself when the release closes the gap.
    • Reads main via git show origin/main:, falls back to git fetch --depth=1 origin main — proven from a fresh --depth=1 clone with no origin/main (CI's shape): fetched 646c931, 30 passed. Skips loudly if main is unreachable rather than passing.
    • Mutation-checked: dropping the issue-status-on-merge entry → red; adding a bogus entry for a workflow main already has → red.
    • First run found three more unrecorded divergences beside the two schedule ones whose files already say "main only" in their own comments: workflow_dispatch on deploy-dev.yml (the manual no_cache rebuild), alembic-head-watch.yml, publish-images.yml. All six recorded with reasons.
  • .github/workflows/issue-status-on-merge.yml — comment only. "Runs the file as it exists on the BASE branch" now also says the trigger is registered from main; reading the first as the second is the inference that shipped this.
  • docs/memory/learnings.md — the class.

Backfill (AC#2)

Every PR merged to dev since 14:24 UTC 2026-09-14 with a same-repo closing keyword: #2799→#2794, #2798→#2795, #2797→#2742, #2769→#2767 (all already relabelled by hand), and #2731→#2730, which had never been promoted — now status-in-dev. #2794/#2795 carried a stale status-ready beside the hand-added label — stripped.

What is deliberately NOT here

The one-file hotfix to main that would restore promotion today (the #2406 precedent). Rejected in favour of the release cut; the allowlist entry names that decision and its date. Dual-trigger on dev was also rejected — test_2767 pins "both triggers would double-run the promotion".

Test Plan

  • cd tests && pytest unit/test_2814_workflow_trigger_parity.py -v — 30 passed
  • unit/test_2767_issue_status_fork_prs.py — 10 passed (comment edit only)
  • Fetch fallback verified from a git clone --depth=1 with no origin/main
  • After the next release cut: prune the issue-status-on-merge allowlist entry (the guard's stale-entry test will demand it) and confirm a Fixes #N merge produces a run

Fixes #2814

🤖 Generated with Claude Code

https://claude.ai/code/session_01VpvcfgWkmQPD7DrDLmATTf

…he divergence, record the accepted delays (#2814)

`Issue Status on PR Merge` has not run since #2769 changed its trigger to
`pull_request_target`: zero runs across six merges, three carrying a valid
`Fixes #N`, every one of those issues left in `status-in-progress` until a
human moved it. Not red — absent. GitHub executes the base branch's copy of
a `pull_request_target` workflow but REGISTERS the trigger from the default
branch, and `main` still says `pull_request`. Neither copy can start a run,
and the change is inert until the next release cut.

Decided (2026-09-15): wait for the release rather than hotfix `main`. This
PR therefore ships the guard for the class, not the trigger:

- tests/unit/test_2814_workflow_trigger_parity.py — for every workflow,
  each default-branch-registered event declared here must also be declared
  by `main`'s copy, or the delay is recorded in ACCEPTED_UNTIL_RELEASE with
  a reason. Reads `main` via `git show`, falls back to a `--depth=1` fetch
  (proven from a CI-shaped clone), skips loudly if unreachable. A stale
  allowlist entry fails too, so the record is pruned when the gap closes.
  First run found three MORE unrecorded divergences (`workflow_dispatch` on
  deploy-dev, alembic-head-watch, publish-images) beside the two `schedule`
  ones whose files already said "main only" in their own comments.
- issue-status-on-merge.yml — the comment that said "runs the file as it
  exists on the BASE branch" now also says it is registered from `main`,
  since reading the first as the second is the inference that shipped this.
- docs/memory/learnings.md — the class, stated once.

Backfill (AC#2): #2730 (merged via #2731 at 15:15 UTC 2026-09-14) had never
been promoted — now `status-in-dev`; #2794/#2795 had a stale `status-ready`
beside their hand-added `status-in-dev` — stripped.

Related to #2814 (AC#1 closes at the release cut, not here)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VpvcfgWkmQPD7DrDLmATTf
@dolho

dolho commented Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

/review — head d050b03b vs origin/dev (a7acc3dd)

Files: 3 (+261) · Scope: CLEAN against #2814 as decided (guard + backfill + comment; no trigger change by decision, recorded on the issue).
Plan completion: AC#3 DONE (test_2814_workflow_trigger_parity.py); AC#2 DONE outside the diff (#2730 relabelled, #2794/#2795 tidied); AC#1 deferred to the release cut and named in ACCEPTED_UNTIL_RELEASE.

Critical

None.

Informational

[I1] A stale local origin/main gives a stale verdict (Confidence 6/10)
_main_ref() prefers an existing origin/main ref over fetching. In CI the ref never pre-exists (fetch-depth: 1, PR ref only) so the fallback fetch is what runs and the verdict is fresh — verified from a --depth=1 clone. Locally, a clone whose origin/main predates a release would report a divergence that is already closed (a spurious red), or miss one that just opened. Not a CI hazard; a developer-experience one. If it bites, git fetch origin main first, or make the test always fetch and only fall back to the local ref when the fetch fails.

[I2] git fetch inside a unit test (Confidence 5/10)
One network call per pytest process (lru_cache, so once per xdist worker). Public repo, anonymous, not API-rate-limited. Skips loudly when unreachable rather than passing — the right direction — but it is the first test in tests/unit/ that needs the network, and tests/README.md's tier rules should probably say so if a second one appears.

Clean

  • Mutation-checked both ways: removing the issue-status-on-merge allowlist entry → red; a bogus entry for a workflow main already has → red (test_every_accepted_entry_names_a_real_divergence).
  • Scope of the event set is stated: push/pull_request deliberately excluded (test_pr_triggers_are_deliberately_out_of_scope), pull_request_target pinned in.
  • No behaviour change: the workflow edit is comment-only; test_2767 still passes (10/10).
  • The three newly found divergences (workflow_dispatch on deploy-dev, alembic-head-watch, publish-images) are recorded with reasons rather than silenced.

Verdict: READY.

Mechanical, per the merge-train note on the PR.

1. Merged dev. `docs/memory/learnings.md` was the only conflict — the routine
   same-day append collision; both sides kept, dev's two entries first.

2. `_git()` passed `timeout=90` but caught nothing, so the #2019 fail-soft
   contract its docstring promises held for only one of the two network
   failure modes: a *refusing* network returns non-zero and the caller skips,
   but a *hanging* one raised TimeoutExpired out of the test and reddened the
   PR — the offline-runner case the helper exists to avoid. Now returns a
   synthetic non-zero CompletedProcess for TimeoutExpired and OSError (the
   latter covers a missing git binary). Verified both arms by injection.

3. The ACCEPTED_UNTIL_RELEASE comment said a stale entry "is not harmful, only
   untidy", directly contradicting test_every_accepted_entry_names_a_real_
   divergence, which makes it a hard failure. The test is authoritative — the
   PR body's "the record prunes itself" is the decision — so the comment now
   says so, and names the expected post-release red on main's push CI.

tests/unit/test_2814_workflow_trigger_parity.py: 30 passed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@vybe

vybe commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

merge-train: mechanical fixes were pushed to this branch

Commit 2bfc21a3, plus a body edit. Nothing else was touched.

1. dev merged in. docs/memory/learnings.md was the only conflict — the routine same-day append collision. Both sides kept, dev's two entries first.

2. _git() now fails soft on a hang (tests/unit/test_2814_workflow_trigger_parity.py). It passed timeout=90 but caught nothing, so the #2019 contract its own docstring promises held for only one of the two network failure modes: a refusing network returns non-zero and the caller skips cleanly, but a hanging one — the classic blip shape — raised TimeoutExpired out of the test and reddened the PR, which is the offline-runner case the helper exists to avoid. Now returns a synthetic non-zero CompletedProcess for TimeoutExpired and OSError (the latter covering a missing git binary). Verified by injecting both.

3. The ACCEPTED_UNTIL_RELEASE comment contradicted its own test. It read "a stale entry is not harmful, only untidy", while test_every_accepted_entry_names_a_real_divergence makes a stale entry a hard failure. Your PR body already settled which is authoritative ("the record prunes itself"), so the comment now says that, and names the expected post-release red on main's push CI as the prune order rather than a surprise.

4. Body: Related to #2814 → Fixes #2814, so the issue promotes. (See the caveat below — promotion is currently broken anyway.)

tests/unit/test_2814_workflow_trigger_parity.py → 30 passed.

One note in case it saves you time: I briefly saw test_every_accepted_entry_names_a_real_divergence fail locally with all 7 entries reported stale. That was a corrupted local origin/main ref pointing at this branch's own tip (d050b03b6), not a real finding. Against the true origin/main (646c9314f), which declares pull_request, the allowlist is correct and the test passes. CI, working from a fresh clone, was never affected.

Validation notes not fixed — yours to judge

  • The guard is one-directional (here - on_main): an event main declares that this branch dropped is invisible, the mirror of bug(ci): issue promotion has not run since #2769 — the pull_request_target trigger is inert until it reaches main #2814. The PR's "diffs each workflow's on: against main's copy" oversells the symmetry.
  • :232 _main_copy(name and f".github/workflows/{name}") — name is always a non-empty str, so name and is a dead no-op.
  • Enforcement is advisory: this guard lands in backend-unit-test.yml, which is not among dev's four required checks, and schema-parity self-skips on a path regex matching neither .github/workflows/** nor this test. A future workflow-only PR can merge with this guard red — pre-existing, but it caps the guard for exactly the PR shape it targets.
  • First tests/unit/ test to do network I/O and write .git/FETCH_HEAD. Benign under --dist loadfile; would race if dist mode ever became load/loadscope.

Confirmed, and worth recording

The guard was mutation-tested three ways during validation, including a novel un-allowlisted trigger — so it catches the class, not just the recorded instance. Dropping the issue-status-on-merge allowlist entry reproduces #2769's exact state and goes red, i.e. this guard would have caught #2769 at PR time. And the JUnit XML from run 34974517854 proves it actually executed in CI (30 passed, 0 skipped, all three head seeds) rather than silently skipping behind the fetch-depth: 1 checkout — which mattered, because a silent skip is the very failure class this PR is about.

Separately: issue-status-on-merge.yml has still never fired — pull_request_target runs repo-wide are 0, ever. Roughly 12 merges are stranded without status-in-dev. This PR ships the guard, deliberately not the fix; I'm applying the label by hand for the PRs this merge train lands.

🤖 Generated with Claude Code

@vybe vybe left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

merge-train 2026-09-15: lane C, /review + /cso --diff clean; _git fail-soft and the ACCEPTED_UNTIL_RELEASE comment fixed mechanically; 30 passed, all checks green.

@vybe
vybe merged commit a9d03f8 into dev Sep 15, 2026
26 of 27 checks passed
vybe pushed a commit that referenced this pull request Sep 15, 2026
Mechanical. #2816 merged to dev between this branch's first resolution and its
merge slot, and both append to docs/memory/learnings.md — the routine same-day
collision. Both entries kept, dev's (#2814) first.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@vybe
vybe deleted the fix/2814-workflow-trigger-parity branch September 15, 2026 17:46
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.

2 participants