Skip to content

Adopting the Gates omits three preconditions of validate-task, and each fails silently or confusingly #1516

Closed

Description

@ptr727

Found while adopting validate-task.yml in ptr727/aiopurpleair, against hub main at 9d5a0c0 (2.0.606). Three preconditions an adopting repository meets are not stated in "Adopting the Gates", and each one is silent in a different way. Filed together because they are one class: the adoption section describes the caller stub thoroughly and the task's own contract not at all.

1. A bare uv run mypy needs the pyproject to declare a target, and nothing says so

validate-task.yml's "Type check Python step" runs uv run mypy with no path argument. Mypy resolves its target from files, packages or modules in [tool.mypy], and with none of them it does not check the tree, it exits 2:

mypy: error: Missing target module, package, files, or command.

Reproduced on aiopurpleair, whose [tool.mypy] declared strict, python_version and plugins but no files, because its own task passed the target on the command line as uv run mypy src. The lint job fails immediately on adoption.

"Adopting the Pure Functions" does discuss [tool.mypy], at docs/reusable-workflows.md line 430, but only along the interpreter dimension: "mypy and pyright each defaulting their target to the one they run under where the repo's own [tool.mypy] or [tool.pyright] section does not name a version". That sentence is about the Python version, and a reader checking their config against it comes away satisfied while the file target is still undeclared.

The fix in the adopting repository is one line (files = ["src"]), so this is a documentation gap rather than a task defect. What makes it worth filing is the failure mode: an adopter reads a mypy usage message, which reads as a broken task rather than as a missing declaration in their own config.

2. mypy and pyright are mutually exclusive, and only a comment in the task says so

The same step takes mypy wherever [tool.mypy] is present and reaches pyright only in its absence. The task comments this clearly:

Mypy takes precedence where a pyproject declares both sections, matching every repo that runs mypy in CI today. A repo that genuinely wants both enforced adds the second as its own validate hook step.

That is the right design and the right escape hatch. The problem is where it is written. "Adopting the Gates" contains no occurrence of mypy, pyright, or a pointer to the hook for this purpose, so a repository that runs both today and follows the adoption section will drop one without any signal: the job stays green, and the check is simply gone.

aiopurpleair runs both deliberately, mypy strict over the shipped surface and pyright over sources and tests, so adopting without a hook would have stopped type-checking its test suite while reporting success. It now carries a validate hook for pyright, which is exactly what the comment prescribes, but it got there by reading the task rather than the adoption doc.

3. The prose gate does not run on a push-triggered call, and a shipped stub is push-triggered

validate-task.yml's "Check prose step" carries:

if: github.event.pull_request != null

with the rationale "A publish run's content is already gated by the pull request that introduced it." That holds for a publish run. It does not hold for a caller whose CI trigger is push, and the hub ships one: catalog/snippets/workflows/test-pull-request.yml, the no-build stub, triggers on push: branches: [develop] as well as pull_request, and "Adopting the Gates" describes that shape as intended, since a direct push to develop is the operational model's documented allowance.

On those push runs the prose gate silently does not execute. Meanwhile the rollout section at line 151 describes the lint job as hosting "the fleet doc-lint block, language lint by tree detection, the prose gate, and the repo gate" with no caveat, and the repo gate beside it has no such condition, so the two behave differently for the same caller with nothing saying which.

The visible consequence is that an operational repository's direct-to-develop commits, the ones that never pass through a pull request at all, are the exact commits the prose gate never sees. That may be the intended trade, since a diff-scoped gate needs a base to diff against and a push has no obvious one. If so it is worth a sentence, because as it stands the only way to learn it is to read the if:.

Suggested fix

A short "what the task expects of your repository" block in "Adopting the Gates" covering all three: declare a mypy target, expect one type checker with the hook as the route to a second, and know which gates are conditional on the event that reached them. Items 1 and 2 are documentation only. Item 3 may be documentation or may be a design question about whether the prose gate should resolve a base for a push event.

Environment

  • Hub main at 9d5a0c0, release 2.0.606
  • Adopting repository: ptr727/aiopurpleair, a python + pypi release-model repo running mypy and pyright both
  • Reproduced item 1 locally against the repository's committed uv.lock

Activity

  1. ptr727 commented on Sep 10, 2026

    @ptr727
    OwnerAuthor

    A fourth item of the same class, found completing the adoption in ptr727/aiopurpleair (merged as c9d889e). This one is a defect in the shipped stub rather than a documentation gap, and it affects every repository that adopts the stub as written.

    The base-branch filter strands a retargeted pull request

    Both caller stubs in "Adopting the Gates" filter the trigger by base branch:

    on:
      pull_request:
        branches: [main, develop]

    Changing a pull request's base fires the pull_request activity type edited. That type is not in the default set (opened, synchronize, reopened), and neither stub declares types:. So no run follows a retarget.

    The consequence appears when a stacked pull request is retargeted, which is the ordinary way a stack is unwound:

    1. feature/b is opened against feature/a. The base is not main or develop, so the filter excludes it and no run happens. Correct so far, since no ruleset targets a feature branch and nothing is blocked.
    2. feature/a merges into develop.
    3. feature/b is retargeted onto develop. Now a ruleset does apply and requires the aggregator's context, but the retarget fired edited, so no run posts it.

    The pull request sits at "Expected, waiting for status to be reported". It is not permanently stuck, since a later push (synchronize) or a close and reopen clears it, but nothing in the stub or the docs says so, and the state gives no hint that a no-op push is the remedy.

    What aiopurpleair did, and the tradeoff it makes

    Dropped the filter, so the trigger is a bare pull_request:. Every pull request then gets its run at opened, and the aggregator's context lands on the head SHA, which a retarget does not change, so the check carries across the retarget with no new run needed.

    Verified rather than assumed, since the whole fix rests on it: for a pull_request event the check run associates with head.sha and not with merge_commit_sha. Confirmed against a live run in this repository, whose head_sha matched the pull request's head.sha while its merge_commit_sha differed, with the aggregator's context listed under the former.

    The tradeoff is worth stating plainly, and aiopurpleair records it in its own WORKFLOW.md rather than leaving it implicit: the context carried across a retarget was earned against the old base, and with a non-strict required-status-checks policy nothing forces revalidation, so the check is green on a merge that was never attempted. That is a widening of the gap a non-strict policy already has rather than a new one, and a push clears it.

    Options for the fleet

    Offered as alternatives rather than a recommendation, since the right answer depends on whether the fleet wants stacked pull requests to be a supported shape at all.

    1. Drop the filter in both stubs. Costs runner minutes on pull requests into feature branches. Removes the trap entirely.
    2. Keep the filter and add types: [opened, synchronize, reopened, edited]. Keeps the filter's savings, at the cost of a run on every title and body edit, since edited does not distinguish a base change from either.
    3. Keep the filter and document the trap, naming the no-op push as the remedy. Cheapest, and it leaves a maintainer to recognize a stalled check for what it is.

    PhotoCleaner carries the filtered form today, so whichever option is taken reaches at least one more repository.

  2. ptr727 commented on Oct 11, 2026

    @ptr727
    OwnerAuthor

    New evidence for item 3, read at hub main 50b6bc3 (2.0.972). The condition is wider than the Check prose step.

    • .github/workflows/validate-task.yml:457 (Read comments label step), :478 (Resolve prose base step), and :492 (Check prose step) all carry if: github.event.pull_request != null.
    • A caller triggered only by push (for example push to ** as its pull request check, with no pull_request trigger) therefore never runs the prose gate in CI, and its comments label is never read.
    • Still undocumented: "Adopting the Gates" in docs/reusable-workflows.md and the lint job summary at validate-task.yml:7 say the lint job "also runs the prose gate" without the event condition.

    Suggested addition to the fix: state in "Adopting the Gates" that the prose gate needs a pull_request trigger on the caller, or resolve a base for a push event (for example the previous head or the default branch merge base).

    Found while: adopting validate-task.yml in ptr727/homeassistant-purpleair

    🤖 Generated with Claude Code

  3. ptr727 commented on Oct 29, 2026

    @ptr727
    OwnerAuthor

    Item 1 recurred on 2026-10-29, on the lint-only profile this time. It turned up while PlexCleaner adopted validate-task.yml at 2.0.1251 for a lint-only Python directory.

    When config_dir equals dir, the type-check step leaves targets empty (validate-task.yml near line 443). So the uvx mypy@latest ... invocation near line 460 runs with no path. A [tool.mypy] with no files key then exits 2 with "Missing target module, package, files, or command". The fix on the carrier side was files = ["."].

    Neither "Adopting the Gates" nor the python-codestyle lint-only profile tells a carrier it owes that key. The hub's own pyproject.toml sets files, which is why the hub never sees it. The fix for this issue should name the key in both places, or have the step pass . when no target is configured.

  4. ptr727 commented on Nov 1, 2026

    @ptr727
    OwnerAuthor

    Decision triage split the undecided item out of this issue into #3744, which carries the decision label. Everything else here stays as ordinary work an agent can do without the maintainer.

  5. ptr727 commented on Nov 1, 2026

    @ptr727
    OwnerAuthor

    Maintainer answer to the split decision #3744 (decision triage, 2026-11-01): Option 1: drop the pull_request base-branch filter in both catalog stubs, the docs/reusable-workflows.md example, and the hub's own test-pull-request.yml, updating tests/test_release_guards.py with them. The prose-gate-on-push sub-question follows #3334's answer.

  6. ptr727 commented on Nov 1, 2026

    @ptr727
    OwnerAuthor

    Released (2026-11-02): worktree and branch removed, so this claim holds no files. Its fix merged to develop in PR #3796.

    Backlog burndown claim (round 2, group R2G3). Branch: feature/burn-3334. Issues: #3334, #1516, #3487, #1485, #1938.

    Predicted file set:

    • .github/workflows/test-pull-request.yml, .github/workflows/validate-task.yml, .github/workflows/build-release-task.yml
    • catalog/snippets/workflows/test-pull-request.yml, catalog/snippets/workflows/test-pull-request-smoke.yml
    • docs/reusable-workflows.md, WORKFLOW.md, spec/audit.py, spec/files.json, scripts/repo_gate.py
    • spec/project-types.json, spec/fidelity-model.md (added in adjudication)
    • tests/test_audit.py, tests/test_audit_helpers.py, tests/test_release_guards.py, tests/test_repo_gate.py
    • .agents/skills/workflow-ci-contract/ and its generated trees (.github/skills/workflow-ci-contract/, .claude-plugin/fleet-skills/skills/workflow-ci-contract/, .claude-plugin/fleet-skills/.source-digests/workflow-ci-contract)
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