feat: profiles.json v2 parser with validation invariants (#103)

Add version-2 support to gitea_config: environment -> service -> identity
hierarchy flattened at load into v1-shaped profiles keyed by the canonical
dotted address {env}.{service}.{identity}, with aliases for legacy names
(mdcps, prgs-author, prgs-reviewer) and service-level defaults inherited by
identities.

Fail-closed validation: missing required version (v1 files must now declare
version: 1), unknown versions, malformed environment/service/identity
structure, dotted segment names, missing base_url, missing auth reference,
inline secrets in identities or auth entries, alias/address selector
conflicts, aliases to unknown targets, and unqualified operations that
cannot be normalized safely. TBD-* usernames fail closed at selection
without blocking other identities in the file.

Reviewer-identity deadlock rule enforced at load: any identity allowed
gitea.pr.approve or gitea.pr.merge must forbid gitea.pr.create and
gitea.branch.push (prevents the PR #102-style self-authored-PR deadlock).

Selector resolution is strict: exact alias -> exact dotted address -> fail
closed; no fuzzy matching. Minimal operation normalization only (the known
v1 unqualified Gitea ops and single-word non-Gitea ops); the full table and
enforcement matrix remain issue #106.

Tests: new tests/test_config_v2.py (29 cases) covering the acceptance
criteria; test_config.py missing-version case flipped to fail-closed per
the issue. resolve_token/auth_source_name proven against flattened v2
profiles.

Refs #100. Closes #103.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-02 18:49:30 -04:00
parent 790c2c80b1
commit 65ea7514d2
3 changed files with 628 additions and 12 deletions
+6 -3
View File
@@ -127,11 +127,14 @@ class TestLoadSelect(_ConfigBase):
gitea_config.resolve_profile()
self.assertIn("version", str(ctx.exception))
def test_missing_version_defaults_ok(self):
def test_missing_version_fails_closed(self):
# Changed by #103: an unversioned config is ambiguous between the v1
# and v2 shapes, so the loader now refuses to guess.
self._write({"profiles": {"prgs": {"base_url": "https://x"}}})
with patch.dict(os.environ, self._env("prgs"), clear=True):
self.assertEqual(
gitea_config.resolve_profile()["base_url"], "https://x")
with self.assertRaises(gitea_config.ConfigError) as ctx:
gitea_config.resolve_profile()
self.assertIn("version", str(ctx.exception))
# ---------------------------------------------------------------------------