Skip to content

Non-draft PR publication is refused while the head run is in flight, so runs fall back to draft handoffs #1189

Description

@yxlyx

Supersedes #1179 with a full diagnosis.

Symptom

Runs that publish a PR right after their change lands repeatedly come back as draft PRs — including work that is eventually merged — where non-draft PRs are expected. Superseded duplicates pile up: a draft PR is closed and re-created as another draft ("replacement") once its checks finish. The runs behave as if a freshly pushed commit can never be published non-draft.

How it shows up

  1. Commit a change and push the branch (the repo's pre-push hook already encourages verifying before pushing).
  2. Immediately run gh pr create with a body that carries the required verification section.
  3. The write is refused; the sanctioned exit in the refusal is --draft; the retried draft create succeeds. The run then ends as a caveated draft handoff, and because a draft cannot satisfy verified completion, the cycle tends to repeat as a replacement draft later.

This is deterministic in that flow, not exceptional: the push-triggered workflows start on the branch push (workflows configured with on: push and cancel-in-progress: true), so the exact head SHA's run is always queued or in progress at the moment the PR is created.

Harness error text

Non-draft create refused at the preflight while the head run is in flight:

PR publication preflight: the GitHub write was NOT performed. non-draft publication blocked: the exact head SHA still has a pending branch run. Inspect the exact head SHA before `gh pr create`; `gh pr checks --watch` after create is not this gate. Use --draft when readiness is unresolved.

Two neighboring refusals funnel to the same fallback:

PR publication preflight: observed local check has no successful completion: <command>. Rerun it successfully or publish a draft; write NOT performed.
PR publication preflight: claim review could not establish readiness from the committed source and tests; write NOT performed. Keep a draft while evidence is unresolved.

Root cause in code terms

  • publish_gate.observe gathers head evidence with a single gh run list --commit <head-sha> sample taken before the write, mapped through pr_publish.headStatusFromRunList.
  • pr_publish.decide classifies an in-flight run like failing evidence: pending/failed map to block (or draft_only when the failure is disclosed as base-reproduced), unknown blocks, and only none/passed allow. A queued run is therefore treated strictly worse than no run at all — none ("no pre-PR run", PR-only workflows) is explicitly allowed per ADR 0100 — even though a queued run is simply evidence that has not arrived yet.
  • Because the natural order is push-then-create and push starts the runs, the refusal above fires on every ordinary publish. There is no re-read of run state before refusing, and no supported path to non-draft in that moment other than waiting outside the harness.
  • Every refusal points at --draft, and decide(draft = true, …) short-circuits to .allow — it skips the verification-section requirement, head evidence, the observed-local-check gate (State.unresolved is only consulted under !draft), and claim review. --draft is thus the only write that reliably succeeds once any gate has refused, which is why drafts become the default output rather than an explicit choice.
  • The draft then cannot close the loop: pr_verify.decision treats a draft receipt as unverified, pr_verify.completionGate defers completion ("draft: verified readiness requires a ready PR"), and attempt_completion stamps draft handoffs with "Draft handoff — CI is not verified.". Runs end as caveated drafts; a moved head or re-created PR restarts the cycle.

Expected behavior

A non-draft gh pr create immediately after pushing produces a non-draft PR while its first run is still going. The verification requirement stays enforced where it is decisive: pr_verify is armed at publication and completion requires current-head checks to pass (ADR 0120/0123 already guarantee this). Concluded failures and unreadable run evidence keep blocking the write. Drafts remain an explicit choice, not the harness's default output.

Where a fix could land

  • Decision semantics: treat a queued/in-flight head run as evidence-not-yet-arrived (decide it like none), keeping failed/unknown blocked and leaving verified completion as the enforcement point. This revises the in-flight part of ADR 0100 and needs an ADR record.
  • Evidence gathering: re-read run state before refusing (or resolve the run first) so a refusal only happens on concluded evidence, not on an instantaneous sample.
  • Refusal messaging: stop routing recoverable refusals (body/claim/local-check problems) to --draft, so the unconditional draft bypass is not the advertised default exit.

All three can stand alone; the first two address the deterministic refusal, the third addresses the learned fallback.

Notes

  • --draft bypassing every check is consistent with ADR 0120's "draft is the unresolved-readiness fallback", but combined with refusals that fire on ordinary states it makes drafts the path of least resistance in every run.
  • Body-verification parsing and claim review can also refuse recoverable inputs in practice; they share the same draft-pointing exit text.

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

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions