Skip to content

feat(promotion-train): cut a candidate only on a dispatch - #2540

Merged
modelmirror merged 2 commits into
stagingfrom
feat/promotion-train-dispatch-cut
Oct 18, 2026
Merged

modelmirror merged 2 commits into
stagingfrom
feat/promotion-train-dispatch-cut

Conversation

@modelmirror-agent

Copy link
Copy Markdown
Contributor

What changed and why

The promotion train now cuts a candidate only when promotion-prep is dispatched (gh workflow run promotion-prep.yml --ref main), so the maintainer decides when a batch closes. The hourly schedule still carries an open candidate through every later step: dispatch the gate, mark it ready, re-fire, merge. With no candidate open, a scheduled tick reports in its run summary that a batch is ready to cut, names the dispatch command, and cuts nothing.

  • Decision (src/fedcourtsai/promotion_train.py)
    • Adds Observed.cut_requested, default False, so a caller that passes nothing never cuts.
    • Adds a CUT_COMMAND constant.
    • _cut_step checks the request last. A dispatch that can't cut (candidate open, sync pending, nothing to promote, tip already cut) gives the same reason a scheduled tick would. Only a batch that is otherwise ready gets the dispatch hint.
  • CLI: fedcourts promotion-train-step --cut-requested.
  • Workflow (promotion-prep.yml)
    • The read step passes --cut-requested when github.event_name == workflow_dispatch. The run gate already limits dispatches and re-runs to the logins in config/dispatchers.json.
    • A dispatch from a ref other than main can't cut, because write binds prod, which admits only main.
  • Rendered prose
    • The candidate body's "next one is cut…" line (promotion_body.py).
    • promote.yml's header and its step-summary hint.
  • Docs
    • docs/pipeline.md: the train section, the cut row, the closed-candidate paragraph, operator step 2, and the trigger table.
    • docs/cli.md.
    • The module docstring.
    • The docs now also state two edges:
      • A re-run of a dispatched run counts as a new dispatch and cuts at the tip as it is then.
      • GitHub keeps one pending run per concurrency group, so a dispatch queued behind a running tick can be cancelled by the next scheduled tick. A cancelled dispatch cut nothing and must be sent again.

Artifact served: the promote/<date> candidate and its promotion PR. The change controls when one is created.

Checks run

  • ruff format --check, ruff check, and mypy on a fresh cache: clean.
  • pytest on tests/test_promotion_train.py, test_workflow_promotion_train.py, test_workflow_promote.py and test_promotion_body.py: 218 passed.
  • The docs-tests lane: 9 passed.
  • New tests cover:
    • a scheduled tick reports "ready to cut" with the command and cuts nothing;
    • a dispatch that can't cut names the real reason, not the hint;
    • a scheduled tick on a batch that isn't ready names the real reason;
    • a dispatch with a candidate open advances it;
    • the CLI cuts only with the flag;
    • the workflow passes the flag only on workflow_dispatch;
    • the docs and the workflow header carry CUT_COMMAND verbatim.
  • actionlint 1.7.12 and zizmor 1.26.1 (--offline --persona=regular) on promotion-prep.yml and promote.yml: clean.
  • The full suite did not finish inside the sandbox's 10-minute limit, so CI is the evidence for it.

Review

  • workflow-reviewer: no blockers. Its recommended fixes are all applied: the stale promote.yml header and promotion_body.py line, and the cancelled-dispatch edge now documented. Its nits are applied too: reflowed comments, and "and a dispatch" added to the closed-candidate reason.
  • code-reviewer:
    • Its one blocker, the stale promote.yml header, is fixed.
    • Recommended fixes applied: the promote.yml step-summary hint now names the dispatch, and the re-run and cancelled-dispatch behaviour is documented.
    • On re-runs, I kept the behaviour where re-running a dispatch counts as a new dispatch and documented it rather than special-casing it. Only a login the run gate admits can re-run, and cutting at the current tip is what a fresh dispatch would do anyway.
    • Its nits are applied: a regex instead of exact-bash matching, a scheduled-but-blocked test, a docs↔CUT_COMMAND tie, and the history-leaning wording removed.

Left for the maintainer. This touches .github/workflows/, so it is maintainer-owned and needs your code-owner review. I have not merged it into staging.

Effect check (handover note)

Once promoted, no tick cuts unless it is a dispatch.

  1. After this batch merges and the sync lands, the next scheduled promotion-prep run's summary should read promotion train: wait — staging <sha> carries a batch over main, ready to cut; … gh workflow run promotion-prep.yml --ref main, and no new promote/ branch should appear:
    gh run list --workflow promotion-prep.yml --limit 3
    gh api repos/ModelMirrorAI/fedcourtsai/git/matching-refs/heads/promote/ --jq '.[].ref'
    
  2. Then gh workflow run promotion-prep.yml --ref main should cut promote/<date> and open its draft PR (gh pr list --base main --search head:promote/).

Generated by Claude Code

The hourly schedule cut a candidate whenever staging held something new
and the last promotion was synced, so a batch closed at whatever tip the
first eligible tick saw. Now only a dispatched promotion-prep tick cuts;
the schedule advances an open candidate to the merge as before and, with
none open, reports a batch ready to cut, naming the dispatch command.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019NtSiNeMSRGjEKSx7yTfdu
…scribed

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019NtSiNeMSRGjEKSx7yTfdu
@modelmirror
modelmirror enabled auto-merge October 18, 2026 14:47
@modelmirror
modelmirror merged commit b2e1473 into staging Oct 18, 2026
9 checks passed
@modelmirror
modelmirror deleted the feat/promotion-train-dispatch-cut branch October 18, 2026 14:54
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.

3 participants