Skip to content

git-push's budget is a per-repo fact, so it reads from ops.git-push.budget now — refused, never clamped, against the op's own timeout (#1631) - #1646

Merged
fdaviddpt merged 2 commits into
masterfrom
fix/1631
Aug 14, 2026
Merged

git-push's budget is a per-repo fact, so it reads from ops.git-push.budget now — refused, never clamped, against the op's own timeout (#1631)#1646
fdaviddpt merged 2 commits into
masterfrom
fix/1631

Conversation

@fdaviddpt

Copy link
Copy Markdown
Contributor

git-push's 300s budget was a module constant, and this repo's own pre-push hook takes 309.86s — so every master push here timed out on a healthy machine with a green suite, and paid 300s to learn it again. Measured tonight: two failures at 302.70s and 302.90s, then budget=1500 landed with 12868 passed, 51 skipped in 309.86s.

Ten seconds of margin is why "raise the constant" is the wrong fix: a bumped default moves this repo from always fails to sometimes works, and a repo with no pre-push hook wants a budget shorter than 300, because there a long one only delays an honest failure. The number is per-repo in both directions, which is what makes it config.

Precedence and validation

Flag > config > 300 default, and 300 is still the answer when neither is set.

_config_budget() has three states and refuses rather than clamps: non-int (bool is an int), ≤ 0, > 1800, ≥ ops.git-push.timeout read from the same merged entry, or an unreadable timeout — naming both numbers each time. Clamping would convert "you asked for one number and got another" into a discovery made at the moment a push cannot be verified, which is what #399's strict-under constraint exists to prevent.

_read_json / _UNREADABLE keep "did not answer" separate from "answered with nothing": the core stops walking on [] but continues past a parse failure, so collapsing the two would resolve a budget out of a config the core never read.

The judgment call

Both keys are read off disk rather than taking budget from SUPERTOOL_BUDGET, which the core exports for free. timeout is in the core's _RESERVED_KEYS and never reaches the subprocess (pinned by tests/test_custom_ops.py:463), so validating an env-supplied budget against a disk-read timeout would be two answers to one question checked against each other — and it would lose any project that overrides timeout too.

Reviewer

Independent Sonnet pass against the committed diff. Correctness, vacuous tests and cross-platform: none. Three accepted on the fourth category, all about the receipt pointing at the wrong lever — _budget_advice() opened "That budget is _PUSH_TIMEOUT in presets/git/push.py" while a configured budget was the clock, and two doc lines said the same. Each is now pinned; the pins were proved red against the pre-review commit.

Adjacent, fixed

  • .supertool.json gains ops.git-push.budget = 1500. Without it this PR ships the capability and the maintainer still pays 300s per master push. tests/test_op_registry_1356.py's register of live partial overrides updated with the reason, as its own docstring instructs; the assertion was never relaxed. That test was the single full-suite failure and is the reason the suite was worth running.
  • tests/test_git_push_budget_1530.py's reset fixture cleared _BUDGET["seconds"] but not source, so a driven push in that file decided which remedy a later receipt test saw.

Withdrawn during review, worth stating

The first version of test_the_budget_source_is_cleared_by_the_main_prologue drove push.main() with cwd at the checkout — i.e. it attempted a real push of this repository, and errored eleven neighbouring tests. Replaced with an assertion on main()'s prologue source, which is what conftest.PRESET_SELF_CLEARING_GLOBALS credits anyway. Stated here because it generalises: a test in this repo that calls push.main() outside a sandbox pushes the repo.

Suite

39 passed on the new file; 227 passed across the git-push family and registry under random order; full suite in a disposable clone with origin re-pointed: 12921 passed, 97 skipped, 15 warnings, 2 subtests passed in 314.08s. Windows unverified locally.

RED before implementation was 31 failed, 4 passed — the four passing are unchanged-behaviour pins (flag still wins, no config means no Push budget: line, _PUSH_TIMEOUT_MAX < op timeout).

Closes #1631

…re-push hook runs its suite timed out on every master push (#1631)

`_PUSH_TIMEOUT = 300` was reachable only from the per-call `:budget=SECONDS`
flag, and its own comment argued against itself: what decides the right number
"is not visible from here". It is visible from the project's config — whether a
pre-push hook runs a suite is a property of the repo. Measured: 12868 passed in
309.86s against the 300s default, so two pushes failed at 302.70s and 302.90s
with nothing sent before a third at :budget=1500 landed.

Adds `ops.git-push.budget`, merged over the preset entry key-by-key like every
other per-op key. Precedence is flag > config > 300, and 300 is still the answer
when neither is set. Refused, never clamped: validated against
ops.git-push.timeout from the SAME merged entry — both keys from one read,
because core exports `budget` to the subprocess but reserves `timeout`, and
checking one against the other would be two answers to one question. A value
that is not a whole positive number of seconds, is above 1800, is at or above
the op timeout, or whose timeout could not be read at all, refuses the push
before anything is sent and names both numbers (#399, #1615).

Ten seconds of margin is also the argument against a bigger constant: it would
move this repo from always-fails to sometimes-fails, and a repo with no hook
wants a shorter budget for the same reason.

Co-Authored-By: Max <noreply>
…as the clock, and two doc sites still described the flag as the only lever (#1631)

Found by the independent review of the first commit.

`_budget_advice` is printed by three receipts and opened with "That budget is
_PUSH_TIMEOUT in presets/git/push.py". With `ops.git-push.budget` in force that
sends the caller to raise a number that did not cut — #1530's own defect one
indirection further in. `_BUDGET` gains a `source`, reset in `main()`'s
prologue like the rest of it, and the advice names the lever that was actually
pulled; every arm still hands back a runnable `git-push:budget=SECONDS`.

`tests/test_git_push_budget_1530.py`'s reset fixture cleared `seconds` and not
`source`, so a driven push decided which remedy a later receipt offered.

docs/presets/git.md: the new section called `:budget=SECONDS` "per-call" two
paragraphs above the #1615 section stating it is NOT a per-call timeout; and
the hook-relay paragraph still named `_PUSH_TIMEOUT` as the clock a push
outlasts.

Co-Authored-By: Max <noreply>
@fdaviddpt
fdaviddpt merged commit 11ff0ad into master Aug 14, 2026
37 of 39 checks passed
@fdaviddpt
fdaviddpt deleted the fix/1631 branch August 14, 2026 05:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant