Skip to content
Open
Show file tree
Hide file tree
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 Aug 14, 2026
41ce39d
ci: verify both dependency lock representations
seonghobae Aug 14, 2026
a5b957e
ci(dependabot): scope coupled groups to version updates
seonghobae Aug 14, 2026
5fc24ed
test: preserve atomic dependency update contracts
seonghobae Aug 14, 2026
81034ef
docs: record atomic dependency update policy
seonghobae Aug 14, 2026
3d6b79b
build(deps-dev): carry Ruff 0.16.2 into lock repair
seonghobae Aug 14, 2026
a3a60bd
ci: regenerate both Python locks in a one-shot verified job
seonghobae Aug 14, 2026
c167d9e
build: regenerate Python dependency locks
github-actions[bot] Aug 14, 2026
bba8c7f
build: keep Ruff at the current lock-consistent version
seonghobae Aug 14, 2026
13227c2
build(deps-dev): align Ruff with regenerated locks
seonghobae Aug 14, 2026
615b8ca
ci: prove exported lock is an exact uv export
seonghobae Aug 14, 2026
4c1f08f
ci: restore the pinned checkout action hash
seonghobae Aug 14, 2026
685de59
test: require exact dual-lock equivalence evidence
seonghobae Aug 14, 2026
14d2abb
test(ci): require header-insensitive lock equivalence
seonghobae Aug 15, 2026
50dd9c9
fix(ci): ignore uv export output path in lock comparison
seonghobae Aug 15, 2026
7e684e0
Merge remote-tracking branch 'origin/main' into HEAD
seonghobae Aug 25, 2026
f30c9b2
test(deps): require atomic security updates
seonghobae Aug 26, 2026
c593f8d
fix(deps): group exact-coupled security updates
seonghobae Aug 26, 2026
9173bf4
docs(deps): record security grouping contract
seonghobae Aug 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,27 @@ updates:
schedule:
interval: "weekly"
open-pull-requests-limit: 10
groups:
pydantic-runtime:
applies-to: "version-updates"
patterns:
- "pydantic"
- "pydantic-core"
httpx2-runtime:
applies-to: "version-updates"
patterns:
- "httpx2"
- "httpcore2"
Comment thread
seonghobae marked this conversation as resolved.
pydantic-runtime-security:
applies-to: "security-updates"
patterns:
- "pydantic"
- "pydantic-core"
httpx2-runtime-security:
applies-to: "security-updates"
patterns:
- "httpx2"
- "httpcore2"
Comment thread
seonghobae marked this conversation as resolved.
- package-ecosystem: "docker"
directory: "/services/account_unification"
schedule:
Expand Down
30 changes: 30 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,36 @@ jobs:
prune-cache: true
- name: Install locked dependencies
run: uv sync --locked --extra dev
- name: Verify uv lock dependency metadata
run: uv pip check
- name: Verify exported hash lock
run: |
exported_requirements="${RUNNER_TEMP}/keyverse-requirements-dev.txt"
tracked_requirements_body="${RUNNER_TEMP}/keyverse-tracked-requirements-body.txt"
exported_requirements_body="${RUNNER_TEMP}/keyverse-exported-requirements-body.txt"
uv export \
--locked \
--format requirements.txt \
--extra dev \
--no-emit-project \
--output-file "${exported_requirements}"

# uv records the chosen output path in its two-line generated-file
# header. Compare the package graph and hashes while ignoring only
# that environment-specific provenance text.
tail -n +3 requirements-dev.txt > "${tracked_requirements_body}"
tail -n +3 "${exported_requirements}" > "${exported_requirements_body}"
cmp --silent "${tracked_requirements_body}" "${exported_requirements_body}" || {
diff --unified "${tracked_requirements_body}" "${exported_requirements_body}"
exit 1
}

export_lock_venv="${RUNNER_TEMP}/keyverse-export-lock"
uv venv "${export_lock_venv}"
VIRTUAL_ENV="${export_lock_venv}" uv pip install \
--require-hashes \
-r requirements-dev.txt
VIRTUAL_ENV="${export_lock_venv}" uv pip check
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- name: Lint
run: uv run ruff check app tests tools
- name: Docstrings
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ Keep a Changelog, and releases use semantic versioning.
- Added the product/technical gap baseline and its APA 7th doctoring record,
including the current exact-head PR/Issue inventory and explicit
`gap-not-claimed` runtime and release boundaries.
- Exact-coupled `pydantic`/`pydantic-core` and `httpx2`/`httpcore2` updates are
grouped into atomic Dependabot pull requests, while CI independently installs
and checks both `uv.lock` and the exported hash-locked requirements graph.
- Relying-party deployment controllers now send validated, secret-free metadata
to Keyverse desired-state PUT instead of applying client representations
directly to Keycloak; confidential credential placement remains a separate
Expand Down
38 changes: 38 additions & 0 deletions docs/doctoring/dependabot-coupled-security-updates.md
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
2 changes: 1 addition & 1 deletion services/account_unification/requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# This file was autogenerated by uv via the following command:
# uv export --format requirements.txt --extra dev --no-emit-project --output-file requirements-dev.txt
# uv export --locked --format requirements.txt --extra dev --no-emit-project --output-file requirements-dev.txt
annotated-doc==0.0.5 \
--hash=sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101 \
--hash=sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb
Expand Down
112 changes: 112 additions & 0 deletions services/account_unification/tests/test_dependency_update_policy.py
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
Loading