-
Notifications
You must be signed in to change notification settings - Fork 0
ci: keep exact-coupled Python dependency updates atomic #101
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
seonghobae
wants to merge
19
commits into
main
Choose a base branch
from
fix/dependabot-coupled-lock-groups
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
f78063d
ci(dependabot): group exact-coupled Python packages
seonghobae 41ce39d
ci: verify both dependency lock representations
seonghobae a5b957e
ci(dependabot): scope coupled groups to version updates
seonghobae 5fc24ed
test: preserve atomic dependency update contracts
seonghobae 81034ef
docs: record atomic dependency update policy
seonghobae 3d6b79b
build(deps-dev): carry Ruff 0.16.2 into lock repair
seonghobae a3a60bd
ci: regenerate both Python locks in a one-shot verified job
seonghobae c167d9e
build: regenerate Python dependency locks
github-actions[bot] bba8c7f
build: keep Ruff at the current lock-consistent version
seonghobae 13227c2
build(deps-dev): align Ruff with regenerated locks
seonghobae 615b8ca
ci: prove exported lock is an exact uv export
seonghobae 4c1f08f
ci: restore the pinned checkout action hash
seonghobae 685de59
test: require exact dual-lock equivalence evidence
seonghobae 14d2abb
test(ci): require header-insensitive lock equivalence
seonghobae 50dd9c9
fix(ci): ignore uv export output path in lock comparison
seonghobae 7e684e0
Merge remote-tracking branch 'origin/main' into HEAD
seonghobae f30c9b2
test(deps): require atomic security updates
seonghobae c593f8d
fix(deps): group exact-coupled security updates
seonghobae 9173bf4
docs(deps): record security grouping contract
seonghobae File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| # Coupled Dependabot security updates — doctoring record | ||
|
|
||
| ## Status | ||
|
|
||
| Active pull-request evidence only. This record describes the dependency-update contract on the contributor branch and does not claim protected `main` ships it until normal protected integration completes. | ||
|
|
||
| ## Problem | ||
|
|
||
| Keyverse deliberately keeps `pydantic` with `pydantic-core` and `httpx2` with `httpcore2` in atomic Dependabot version-update pull requests because each parent/child pair is resolved together in the reviewed lock graph. The existing `groups` entries used `applies-to: version-updates`, so a Dependabot security update could still raise one member of a coupled pair independently. The dual-lock CI gate would then fail closed, but the security-update path would create a preventable blocked pull request rather than proposing one coherent graph. | ||
|
|
||
| ## Decision | ||
|
|
||
| Define a separately named security-update group for each exact-coupled pair while retaining the existing version-update group. GitHub documents that one group rule cannot apply to both version and security updates; matching criteria must be represented as separately named groups with `applies-to: security-updates` for the security path. | ||
|
|
||
| The executable regression requires all four groups and their exact package sets: | ||
|
|
||
| - `pydantic-runtime` — version updates for `pydantic` and `pydantic-core`; | ||
| - `httpx2-runtime` — version updates for `httpx2` and `httpcore2`; | ||
| - `pydantic-runtime-security` — security updates for `pydantic` and `pydantic-core`; and | ||
| - `httpx2-runtime-security` — security updates for `httpx2` and `httpcore2`. | ||
|
|
||
| This changes Dependabot proposal grouping only. It does not weaken the existing `uv.lock` / exported hash-lock equivalence check, vulnerability scanning, review requirements, or protected-branch merge gates. | ||
|
|
||
| ## TDD traceability | ||
|
|
||
| RED commit `f30c9b272af07e9592397d9e46d700493aeebe2b` expanded `test_exact_coupled_dependencies_update_atomically` to require the two security-update groups while the configuration still contained only the two version-update groups. GREEN commit `c593f8de3ca534bd58ee88d5f7b1bbbf15002d22` added the missing security-update groups without changing their package boundaries. | ||
|
|
||
| Hosted exact-head CI remains the authoritative execution evidence. Queued, skipped, stale, predecessor-head, synthetic-only, author-only, or model-only evidence is not promoted to passing. | ||
|
|
||
| ## Rollback | ||
|
|
||
| Rollback removes the two `*-security` group entries and this regression together. The operational consequence is not an unsafe merge: dual-lock CI remains fail-closed. Instead, Dependabot security updates for an exact-coupled parent/child pair may again arrive separately and become unnecessarily blocked. | ||
|
|
||
| ## References — APA 7th | ||
|
|
||
| GitHub, Inc. (2026a). *Configuring Dependabot security updates*. GitHub Docs. Retrieved August 26, 2026, from https://docs.github.com/en/code-security/how-tos/secure-your-supply-chain/secure-your-dependencies/configure-security-updates | ||
|
|
||
| GitHub, Inc. (2026b). *Dependabot errors*. GitHub Docs. Retrieved August 26, 2026, from https://docs.github.com/en/code-security/reference/supply-chain-security/troubleshoot-dependabot/dependabot-errors |
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
112 changes: 112 additions & 0 deletions
112
services/account_unification/tests/test_dependency_update_policy.py
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| """Contracts for atomic dependency updates and dual-lock verification.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from pathlib import Path | ||
|
|
||
| import yaml | ||
|
|
||
|
|
||
| def _repository_root() -> Path: | ||
| """Return the Keyverse repository root from this test module.""" | ||
| return Path(__file__).resolve().parents[3] | ||
|
|
||
|
|
||
| def _dependabot_document() -> dict[str, object]: | ||
| """Parse and return the repository Dependabot configuration.""" | ||
| document = yaml.safe_load( | ||
| (_repository_root() / ".github" / "dependabot.yml").read_text( | ||
| encoding="utf-8" | ||
| ) | ||
| ) | ||
| assert isinstance(document, dict) | ||
| return document | ||
|
|
||
|
|
||
| def _pip_update() -> dict[str, object]: | ||
| """Return the account-unification pip update configuration.""" | ||
| updates = _dependabot_document().get("updates") | ||
| assert isinstance(updates, list) | ||
|
|
||
| for update in updates: | ||
| if not isinstance(update, dict): | ||
| continue | ||
| if update.get("package-ecosystem") != "pip": | ||
| continue | ||
| if update.get("directory") != "/services/account_unification": | ||
| continue | ||
| return update | ||
| raise AssertionError("account-unification pip update configuration is missing") | ||
|
|
||
|
|
||
| def _ci_source() -> str: | ||
| """Return the repository CI workflow source.""" | ||
| return ( | ||
| _repository_root() / ".github" / "workflows" / "ci.yml" | ||
| ).read_text(encoding="utf-8") | ||
|
|
||
|
|
||
| def test_exact_coupled_dependencies_update_atomically() -> None: | ||
| """Packages with exact child pins stay atomic for version and security updates.""" | ||
| groups = _pip_update().get("groups") | ||
| assert isinstance(groups, dict) | ||
|
|
||
| expected_groups = { | ||
| "pydantic-runtime": ("version-updates", {"pydantic", "pydantic-core"}), | ||
| "httpx2-runtime": ("version-updates", {"httpx2", "httpcore2"}), | ||
| "pydantic-runtime-security": ( | ||
| "security-updates", | ||
| {"pydantic", "pydantic-core"}, | ||
| ), | ||
| "httpx2-runtime-security": ( | ||
| "security-updates", | ||
| {"httpx2", "httpcore2"}, | ||
| ), | ||
| } | ||
| for group_name, (update_scope, expected_patterns) in expected_groups.items(): | ||
| group = groups.get(group_name) | ||
| assert isinstance(group, dict) | ||
| assert group.get("applies-to") == update_scope | ||
| patterns = group.get("patterns") | ||
| assert isinstance(patterns, list) | ||
| assert set(patterns) == expected_patterns | ||
|
|
||
|
|
||
| def test_ci_proves_both_lock_representations_are_equivalent() -> None: | ||
| """CI compares lock bodies before installing and checking both forms.""" | ||
| workflow = _ci_source() | ||
|
|
||
| assert "uv sync --locked --extra dev" in workflow | ||
| assert "Verify uv lock dependency metadata" in workflow | ||
| assert "Verify exported hash lock" in workflow | ||
| assert workflow.count("uv pip check") == 2 | ||
| assert "uv export" in workflow | ||
| assert "--format requirements.txt" in workflow | ||
| assert "--extra dev" in workflow | ||
| assert "--no-emit-project" in workflow | ||
| assert '--output-file "${exported_requirements}"' in workflow | ||
| assert ( | ||
| 'tracked_requirements_body="${RUNNER_TEMP}/keyverse-tracked-requirements-body.txt"' | ||
| in workflow | ||
| ) | ||
| assert ( | ||
| 'exported_requirements_body="${RUNNER_TEMP}/keyverse-exported-requirements-body.txt"' | ||
| in workflow | ||
| ) | ||
| assert 'tail -n +3 requirements-dev.txt > "${tracked_requirements_body}"' in workflow | ||
| assert ( | ||
| 'tail -n +3 "${exported_requirements}" > "${exported_requirements_body}"' | ||
| in workflow | ||
| ) | ||
| assert ( | ||
| 'cmp --silent "${tracked_requirements_body}" "${exported_requirements_body}"' | ||
| in workflow | ||
| ) | ||
| assert ( | ||
| 'diff --unified "${tracked_requirements_body}" "${exported_requirements_body}"' | ||
| in workflow | ||
| ) | ||
| assert "--require-hashes" in workflow | ||
| assert "-r requirements-dev.txt" in workflow | ||
| assert 'VIRTUAL_ENV="${export_lock_venv}" uv pip install' in workflow | ||
| assert 'VIRTUAL_ENV="${export_lock_venv}" uv pip check' in workflow |
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.