Fix: stop the published constraints files from downgrading google-adk - #77
Open
AmaadMartin wants to merge 6 commits into
Open
Fix: stop the published constraints files from downgrading google-adk#77AmaadMartin wants to merge 6 commits into
AmaadMartin wants to merge 6 commits into
Conversation
added 5 commits
August 1, 2026 07:01
--all-extras pulls in the `community` extra, whose google-adk-community depends back on google-adk, so uv emitted a `google-adk==` pin into every constraints file. README.md documents `pip install google-adk -c constraints-<ver>.txt`, and these files are regenerated only when pyproject.toml dependencies change, so the pin goes stale on the next release and silently downgrades the very package the user asked to install. Pass --no-emit-package google-adk and regenerate. google-adk-community stays pinned: it is a genuine transitive dependency, not the package under installation. The header guard test is tightened to require the new flag, and two tests are added for the artifact contract itself: no google-adk self-pin, and google-adk-community still pinned.
The Constraints Check job diffs uv's output byte-for-byte against the committed files, so an unpinned resolver means a new uv release can turn the job red without any dependency having drifted. Pin it to the version that generated the committed files, with a note to bump it alongside a regeneration. Removing the pre-commit hook took away the (broken) discovery mechanism for these files, so CONTRIBUTING.md now documents the manual workflow: when to regenerate, that update mode exits 1 whenever it rewrites, and the --check command CI runs.
Matches the other per-version guards in the module and reports the failing interpreter individually instead of stopping at the first one.
The guard was anchored `^#\s+-c .*\.tmp$`, but uv writes the constraint source
*indented inside* the "# via" block (" # -c constraints-3.11.txt"), so `#`
is never at column 0. Against a genuinely corrupted file the pattern scored 0
matches while the corrected one scored 333-382 per file, i.e. the guard could
never fail.
Assuming a .tmp path was a second hole: passing the committed file to
--constraint directly leaks its real name and no .tmp, which an anchor-only fix
still misses. Match any `-c` source instead; the clean files contain none, so
there is no false positive.
This matters because the test is the only guard for this regression. Verified
by reintroducing --constraint in the generator and regenerating: the files pick
up 382 leaked annotations per interpreter, `--check` still exits 0 (a polluted
file and its polluted candidate compare equal, so CI stays green), and only
this test fails - 5 failures, one per interpreter, where the old pattern left
the suite at 27 passed.
Also report a count plus one sample rather than dumping all 382 matches.
…writes
Four post-run `rm -f "$NEW_FILE"` calls were already covered by
`cleanup() { rm -f constraints-*.tmp; }` + `trap cleanup EXIT`, since
$NEW_FILE is constraints-<ver>.txt.new.tmp. Removed. The one before the
seeding `cp` stays: it covers the skipped-cp case, where a candidate left by
a run killed before the trap fired would otherwise seed uv.
The `--output-file` and `--output-file=` sed rewrites were unreachable.
GENERATION_CMD is built in this script and always ends in `-o $TARGET_FILE`,
so only the `-o` rewrite ever fires.
Verified no temp file survives any path the removed rm calls used to handle:
resolution failure (unsatisfiable dependency, 5/5 reported, no clobbering),
check-mode OUT OF DATE, and update-mode rewrite all leave no constraints-*.tmp
and exit as before. The constraints files stay byte-identical, confirming the
dropped seds were no-ops.
CONTRIBUTING.md: the drift-check paragraph named the command inline and then
repeated it in a fenced block; collapsed to one sentence. It also now states
that regeneration must use the uv version CI pins, since the check is a
byte-for-byte diff of uv's output and a contributor on a different resolver
would otherwise hit drift they cannot reproduce.
AmaadMartin
force-pushed
the
fix/update-constraints-precommit-hook
branch
from
August 1, 2026 14:08
1aa5450 to
7f10983
Compare
AmaadMartin
force-pushed
the
fix/publish-constraints-files
branch
from
August 1, 2026 14:08
3753094 to
325c9ce
Compare
The generator built GENERATION_CMD itself and then regex-rewrote its own literal to retarget the output file. That only worked because no other token in the command contained "-o "; a future flag that did would have silently redirected uv's output with no error. Build the destination-independent part once and let each use append its own -o. Also drops a cross-test private import: the module pulled _find_pyproject from a sibling test module, which was the suite's only from tests.unittests.test_* import. It bought nothing here -- its sibling build-directory handling only reaches layouts where the script is absent and the module already skips -- so the repo root is now derived lexically, keeping the symlink-safety that motivated avoiding resolve(). The emitted uv invocation is byte-identical, confirmed by --check still diffing clean against all five committed files.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Please ensure you have read the contribution guide before creating a pull request.
Link to Issue or Description of Change
Closes: #issue_number
Related: #issue_number
Problem: The constraints files published by #56 pin
google-adkitself, so the flowREADME.mddocuments —pip install google-adk -c constraints-<ver>.txt— installs an older google-adk than the user asked for. This is live today, not hypothetical:google-adkpip install google-adk(no constraints)pip install google-adk -c constraints-3.11.txt(as published by #56)Reproduced with a real install against real PyPI, no mocks:
Cause.
--all-extraspulls in thecommunityextra, whosegoogle-adk-communitydepends back ongoogle-adk, so uv emits agoogle-adk==pin into the output. Combined with the deliberate 4-day--exclude-newersupply-chain buffer and the fact that these files are only regenerated whenpyproject.tomldependencies change, that pin is structurally guaranteed to lag the current release — it can never be anything but a downgrade or a no-op.Solution: pass
--no-emit-package google-adktouv pip compileand regenerate. uv's own footer now records the exclusion:google-adk-communitydeliberately stays pinned — it is a genuine transitive dependency, not the package under installation. The regenerated files are otherwise byte-identical apart from the header; no dependency version moved (git diff --statis 9 lines per file), so this does not disturb #56's resolution.Two loose ends from the same change are closed here:
.github/workflows/constraints-check.ymlpinsuvto0.11.8. That job is a byte-for-byte diff of uv's output against the committed files, so an unpinned resolver means a future uv release turns it red with no dependency having drifted.0.11.8is the version that generated the committed files (uv --version), andsetup-uv@37802adcaccepts aversioninput at that pinned SHA. Comment tells the next person to bump it alongside a regeneration.CONTRIBUTING.mddocuments the manual workflow. Fix: unbreak the permanently-failing update-constraints pre-commit hook #56 removed theupdate-constraintspre-commit hook (correctly), which was the only — broken — discovery mechanism for these files. The runbook now says when to regenerate, that update mode exits 1 whenever it rewrites (a real trip hazard), and the--checkcommand CI runs.Collision check (per contribution process). Run before writing any code:
Five open PRs touch this area:
google-adkself-pin) plus the two loose ends above. Nothing in Fix: unbreak the permanently-failing update-constraints pre-commit hook #56 is duplicated or reverted.stages: [manual]mitigation that Fix: unbreak the permanently-failing update-constraints pre-commit hook #56 supersedes; Fix: stop running the constraints auto-updater in the lint job #49 additionally carries ~55 files of unrelated mypy churn. Not stacked on either.scripts/update_constraints.sh(missing-snapshot-date guard; a scheduled regeneration workflow). Both are orthogonal to the--no-emit-packageline and neither touches the constraints artifacts, so they can land in any order; the only possible conflict is textual, in theGENERATION_CMDneighbourhood.Testing Plan
Please describe the tests that you ran to verify your changes. This is required for all PRs that are not small documentation or typo fixes.
Unit Tests:
[x] I have added or updated unit tests for my change.
[x] All unit tests pass locally.
+10 cases: two new tests x five interpreters.
Two tests added to #56's
tests/unittests/test_constraints_files.py, both parametrized over the five interpreters:test_constraints_files_do_not_pin_google_adk— the artifact contract users consume.test_constraints_files_still_pin_google_adk_community— guards the other direction, that--no-emit-packagedid not over-exclude and silently drop a real transitive pin.Two existing tests were strengthened, not weakened. No test was deleted, skipped, or disabled.
_HEADER_LINE_2intest_constraints_header_is_script_generatednow additionally requires--no-emit-package google-adk. One more mandatory token in the same regex; every assertion it made before still holds._LEAKED_ANNOTATIONintest_constraints_files_have_no_annotation_leakagewas a vacuous guard — it could never fail. It was anchored^#\s+-c .*\.tmp$, but uv writes the constraint source indented inside the# viablock, so#is never at column 0:Against a genuinely
--constraint-corrupted file the old pattern scored 0 matches; the corrected^\s*#\s+-c .*$scores 330–382 per file. Assuming a.tmppath was a second hole: passing the committed file to--constraintdirectly leaks its real name with no.tmp, which an anchor-only fix still misses (measured: 0 vs 382). Matching any-csource closes both, and the clean files contain none, so there is no false positive.This is load-bearing because the test is the only guard for that regression — CI cannot catch it. Proved by reintroducing
--constraintin the generator and regenerating:./scripts/update_constraints.sh --check^#pattern27 passed— the regression ships silently5 failed, 22 passed— one per interpreter--checkpasses because a polluted committed file and its equally-polluted candidate compare equal — exactly the "fixed point after a second update run has polluted the files" state. The failure message now reports a count plus one sample instead of dumping all 382 matches.Every test was proven able to fail. The headline mutations are real source-level reverts, not hand-edits:
git stashthe--no-emit-packagehunk out ofscripts/update_constraints.shand regeneratetest_constraints_header_is_script_generated[3.10-3.14]+test_constraints_files_do_not_pin_google_adk[3.10-3.14]constraints-3.10.txt pins google-adk itself: ['google-adk==2.5.0']...andconstraints-3.10.txt line 2 is not the command ./scripts/update_constraints.sh reconstructs...--constraint "$STABLE_FILE"in the generator and regeneratetest_constraints_files_have_no_annotation_leakage[3.10-3.14]constraints-3.11.txt carries 382 uv constraint-source annotations, e.g. ' # -c constraints-3.11.txt.stable.tmp'...--constraint-generated file with no.tmpin the pathtest_constraints_files_have_no_annotation_leakage[3.12]...e.g. ' # -c constraints-3.11.txt'google-adk==2.5.0intoconstraints-3.13.txt(bypassing the generator)test_constraints_files_do_not_pin_google_adk[3.13]constraints-3.13.txt pins google-adk itself: ['google-adk==2.5.0']google-adk-community==line fromconstraints-3.14.txttest_constraints_files_still_pin_google_adk_community[3.14]constraints-3.14.txt no longer pins google-adk-community. --no-emit-package must exclude google-adk only.Rows 1 and 2 are the important ones: both are reintroductions of the actual defect in the generator, confirming the guards catch the bug at its source rather than only a doctored artifact. The script and constraints files were restored after each mutation (
git status --porcelainclean).Coverage. No Python source changed — this is a Bash + YAML + Markdown change plus a regenerated artifact — so there is no new production line to cover. The added tests are themselves the coverage of the changed behaviour, and the
Constraints Up To DateCI job is the permanent regression test.Integration — real tooling, real PyPI, no mocks. Every command run on this branch:
bash -n scripts/update_constraints.sh./scripts/update_constraints.sh(regenerate)./scripts/update_constraints.sh(again)./scripts/update_constraints.sh --checkgrep -E '^google-adk==' constraints-*.txtgrep -c 'stable\.tmp' constraints-*.txt0for all fivegrep -E ' +$' constraints-*.txtpre-commit run --all-filesDead code removed from the generator, with the paths it touched re-exercised. Four post-run
rm -f "$NEW_FILE"calls were already covered by the existingcleanup() { rm -f constraints-*.tmp; }+trap cleanup EXIT($NEW_FILEisconstraints-<ver>.txt.new.tmp), and the--output-file/--output-file=sed rewrites were unreachable becauseGENERATION_CMDis built in this script and always ends in-o $TARGET_FILE. Removing them is only safe if the trap really covers every path therms used to, so each was exercised:rmused to handleconstraints-*.tmp, no committed file clobberedOUT OF DATEconstraints-*.tmp, 0 constraints files modifiedconstraints-*.tmpThe five committed files stayed byte-identical throughout, which is the proof that the dropped
sedrewrites were genuine no-ops. The singlermbefore the seedingcpis kept: it covers the skipped-cpcase, where a candidate left behind by a run killed before the trap could fire would otherwise seed uv.The last of those
sedrewrites is now gone too, at its root. The script builtGENERATION_CMDitself and then regex-rewrote its own literal (s/-o [^ ]+/-o $NEW_FILE/) to retarget the output. That worked only because no other token in the command happened to contain-o; a future flag that did would have silently redirected uv's output with no error. The destination-independent part is now built once asCOMPILE_CMD, and each use appends its own-o— the header keeps-o $TARGET_FILE, the run appends-o $NEW_FILE:The emitted invocation is byte-identical, and
--checkis the proof: it rebuilds each header fromGENERATION_CMDand byte-diffs it against the committed file, so a single changed character would surface asOUT OF DATE. It reports exit 0, 5/5 up-to-date. The failure branch — the line actually edited — was re-exercised by injecting an unsatisfiable dependency intopyproject.toml: exit 1, all five reported (no abort on the first), noconstraints-*.tmpresidue, no committed file clobbered,pyproject.tomlrestored.Test-only cleanup in the same commit. The module imported the private
_find_pyprojectfrom a sibling test module — the suite's onlyfrom tests.unittests.test_*import (verified by grep). It bought nothing here: its sibling-build-directory handling only reaches layouts wherescripts/update_constraints.shis absent, and the module alreadypytest.skips there, so both forms behave identically._REPO_ROOTis nowPath(__file__).parents[2], still lexical so the symlinked-test-tree safety that motivated avoiding.resolve()is preserved. To prove the new resolution really reads the committed files rather than passing vacuously, a third mutation was added: corrupting line 2 ofconstraints-3.14.txt(--all-extras→--all-extra) failstest_constraints_header_is_script_generated[3.14]. All 27 tests pass; the two original mutations still kill their guards.Drift detection still detects drift (the error path). Temporarily capping a dependency below its resolved pin (
tenacity>=9,<10→tenacity>=9,<9.1; resolved pin is9.1.4):Experiment reverted (
git checkout -- pyproject.toml constraints-*.txt).Note: a first attempt using the plan's suggested
tenacity>=9,<10→tenacity>=9.1,<10did not produce drift, because the resolvedtenacity==9.1.4already satisfies the raised floor. A ceiling below the resolved pin is required to force a resolution change.Manual End-to-End (E2E) Tests:
Please provide instructions on how to manually test your changes, including any necessary setup or configuration.
The user-facing promise — no downgrade. Two real venvs, real PyPI:
The constraints file still constrains — the fix must not turn it into a no-op.
uvicornlatest on PyPI is0.52.0; the file pins0.51.0; the install above landed onuvicorn 0.51.0. Supply-chain pinning is intact — onlygoogle-adkitself is exempt. At the resolver level,uv pip compile 'google-adk[community]' -c constraints-3.11.txtyieldsgoogle-adk==2.6.0(uncapped) alongsidegoogle-adk-community==0.5.0(pinned).CI — no workflow runs on this PR, by design of the stack. Both
constraints-check.ymlandcontinuous-integration.ymlarepull_request: branches: [main], and this PR's base isfix/update-constraints-precommit-hook, so neither triggers. Onlyagent-triage-pull-requestappears, and it skips. That is absent, not green — so the CI jobs were reproduced locally on the exact pushed commit325c9ceinstead:Constraints Up To Date./scripts/update_constraints.sh --checkwith uv0.11.8— the exact version this PR pins the job toPre-commit Linterpre-commit run --all-filespytest tests/unittests/test_constraints_files.pyWorking tree clean before and after all three. These jobs will run for real once the stack is flattened onto
main.The stack's base PR Fix: unbreak the permanently-failing update-constraints pre-commit hook #56 does run the real jobs, and they confirm the fix. Fix: unbreak the permanently-failing update-constraints pre-commit hook #56 targets
main, socontinuous-integration.ymlandconstraints-check.ymlboth trigger there. On Fix: unbreak the permanently-failing update-constraints pre-commit hook #56's head (7f10983, the commit this PR is stacked on):Pre-commit LinterConstraints Up To DateMypy Check(3.10/3.11/3.13)A2A v0.3 Tests(3.10–3.14)Unit Tests(3.10–3.14)The
Pre-commit Linterresult is the direct evidence for the headline claim, because there is a control: open PRs branched from amainthat lacks this stack (Fix: restore the mdformat-flattened YAML frontmatter in the local_environment_skill sample SKILL.md #121, Fix: correct the ListSessionsResponse docstring to match implemented state/events behaviour #122, Fix: do not record a group's help fallback as a failed CLI command run #124) all havePre-commit Linterfailing, and the failure is exactly the hook this stack removes —Same workflow, same runner image; the only difference is this stack. That is the lint gate going from red to green.
The
Unit Testsfailures are not from this change and must not be attributed to it. They reproduce identically on unrelated PRs (Fix: restore the mdformat-flattened YAML frontmatter in the local_environment_skill sample SKILL.md #121, Fix: correct the ListSessionsResponse docstring to match implemented state/events behaviour #122, Fix: do not record a group's help fallback as a failed CLI command run #124), and none of them involve a constraints file — they areImportError: Blocked import of regex from current working directory(nltk's CWD import guard, addressed separately) and click-8.2 CLI exit-code assertions. No test intests/unittests/test_constraints_files.pyappears anywhere in the failure log; that file's 27 cases pass.Scope note — one documented deviation from the plan. The plan lists
CONTRIBUTING.mdas out of scope on the grounds that it "remains accurate after this change". It stays accurate, but it becomes incomplete: removing theupdate-constraintshook removes the only (broken) mechanism by which a contributor would ever discover that these files need regenerating. The 22-line runbook added here closes exactly the gap the hook removal opens, and nothing more. Every value in it is taken from the repo rather than from memory — the pinneduv 0.11.8matches.github/workflows/constraints-check.yml, and the "exits 1 whenever it rewrites" warning is the script's ownEXIT_CODE=1behaviour.Checklist
[x] I have read the CONTRIBUTING.md document.
[x] I have performed a self-review of my own code.
[x] I have commented my code, particularly in hard-to-understand areas.
[x] I have added tests that prove my fix is effective or that my feature works.
[x] New and existing unit tests pass locally with my changes.