feat(forge): per-user forge connections — keyring, host-keyed resolution, project pin (#2778)
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / TypeScript typecheck (push) Successful in 41s
CI & Build / integration (push) Successful in 37s
CI & Build / Python tests (push) Successful in 1m4s
CI & Build / Build & push image (push) Successful in 40s
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / TypeScript typecheck (push) Successful in 41s
CI & Build / integration (push) Successful in 37s
CI & Build / Python tests (push) Successful in 1m4s
CI & Build / Build & push image (push) Successful in 40s
A forge token is a user's credential, not an instance's. The single admin-settings config is replaced by per-user keyring rows (one per forge host), and every server-side forge read runs on the PROJECT OWNER's keyring: - forge_connections table + projects.forge_connection_id pin (migration 0078, which also carries the existing admin config into the first admin's row and deletes the old setting keys — no legacy dual-read) - get_forge() replaced by get_forges(owner_id, project_id) -> ForgeSelector; resolve(repo) picks the connection whose host serves the repo. A pinned project uses ONLY its pinned connection; a stale pin (ownership moved) is ignored, never honored across users - env FORGE_* config survives as an implicit entry for admin owners only; a stored row for the same host beats it - consumers threaded: pull-time freshness (owner of the note), coverage (owner of the project), coverage routes' configured flag - routes: /api/settings/forge-connections CRUD + per-connection test (own-rows only, tokens never returned); /api/admin/forge shrinks to /api/admin/forge-webhook (secret only); PUT /api/projects/<id>/forge pins, owner-or-admin asking, owner's connections only - UI: Git Forges card moves to Settings -> Integrations as a connection list; webhook secret stays in the admin Config tab; owner-only forge select on the project coverage card - backups exclude forge_connections (credentials, api_keys precedent) and the pin, so restores fall back to keyring resolution Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -194,6 +194,14 @@ def _forge(tar_bytes: bytes):
|
||||
)
|
||||
|
||||
|
||||
def _selector(tar_bytes: bytes):
|
||||
"""The keyring shape compute_coverage consumes since #2778 — one owner
|
||||
keyring holding the mocked Gitea adapter."""
|
||||
from scribe.services.forge import ForgeSelector
|
||||
|
||||
return ForgeSelector((_forge(tar_bytes),))
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def _dispose_engine():
|
||||
from scribe.models import engine
|
||||
@@ -250,9 +258,9 @@ async def test_coverage_measures_the_tree_exactly_and_caches(seeded):
|
||||
)
|
||||
|
||||
uid, pid = seeded["uid"], seeded["pid"]
|
||||
forge = _forge(_tarball(TREE))
|
||||
selector = _selector(_tarball(TREE))
|
||||
|
||||
coverage = await compute_coverage(uid, pid, forge=forge)
|
||||
coverage = await compute_coverage(uid, pid, selector=selector)
|
||||
assert coverage is not None
|
||||
assert coverage["total"] == 4
|
||||
assert coverage["recorded"] == 2
|
||||
@@ -266,7 +274,7 @@ async def test_coverage_measures_the_tree_exactly_and_caches(seeded):
|
||||
# Nothing computed → nothing cached; refresh writes; the cache reads back
|
||||
# byte-equal, because enter_project will serve exactly this.
|
||||
assert await cached_coverage(uid, pid) is None
|
||||
stored = await refresh_coverage(uid, pid, forge=forge)
|
||||
stored = await refresh_coverage(uid, pid, selector=selector)
|
||||
assert (await cached_coverage(uid, pid)) == json.loads(json.dumps(stored))
|
||||
|
||||
|
||||
@@ -284,7 +292,7 @@ async def test_enter_project_surfaces_the_line_only_once_computed(seeded):
|
||||
before = await enter_project(project_id=pid)
|
||||
assert before["pattern_coverage"] is None
|
||||
|
||||
await refresh_coverage(uid, pid, forge=_forge(_tarball(TREE)))
|
||||
await refresh_coverage(uid, pid, selector=_selector(_tarball(TREE)))
|
||||
after = await enter_project(project_id=pid)
|
||||
line = after["pattern_coverage"]
|
||||
assert line.startswith(
|
||||
@@ -314,4 +322,4 @@ async def test_unservable_binding_measures_nothing(seeded):
|
||||
await s.commit()
|
||||
await set_binding(uid, "https://github.com/somebody/else.git", other_pid)
|
||||
|
||||
assert await compute_coverage(uid, other_pid, forge=_forge(_tarball(TREE))) is None
|
||||
assert await compute_coverage(uid, other_pid, selector=_selector(_tarball(TREE))) is None
|
||||
|
||||
Reference in New Issue
Block a user