assistant-api: many repositories per workspace + REPO_URLS injection#590
Merged
Conversation
A workspace bound to exactly one repository (the denormalised
repository_id / repoUrl), so the orchestrator could only ever emit a
single REPO_URL. The runner now understands a REPO_URLS list, but
nothing produced it.
A workspace_repositories junction (V10) records the additional repos a
workspace clones beyond its primary; the primary is backfilled with
is_primary = TRUE so it can be excluded without a second lookup. A
WorkspaceRepositoryRepository port plus its jOOQ implementation mirror
the existing project_repositories pattern. Attach and detach commands
manage the membership — attach validates the workspace and repository
exist; detach refuses to remove the primary.
Fabric8AgentRunnerOrchestrator now injects REPO_URLS from the
workspace's non-primary attached repos (resolved to their repo URLs),
joined by spaces, and only when there is at least one — so a single-repo
workspace's Pod env is unchanged. REPO_URL still carries the primary.
The two new providers are injected via ObjectProvider, consistent with
the existing deploy-key / github-link wiring.
WorkspaceController gains POST /{id}/repositories and
DELETE /{id}/repositories/{repositoryId}. Handler unit tests cover the
attach/detach guards; the orchestrator integration-test helper passes
the new providers (defaulted empty so existing cases are untouched).
The highest existing assistant-api migration is V12; V10/V11 are already taken (chat_session_kind, repository_access_verification). The new junction migration is renumbered V10 -> V13 so it is strictly incrementing and unique, satisfying the migration guard.
The attach/detach commands, handlers, junction, jOOQ adapter, migration, and REPO_URLS orchestrator injection land here — the complete data layer and runner-facing plumbing. The two REST endpoints (and their request DTO) are split into a follow-up because exposing them regenerates the committed services/assistant-api/openapi.json + assistant-ui generated types, which the springdoc export task can only produce from a fully-booted app (Testcontainers). Keeping this PR free of any HTTP- contract change keeps the OpenAPI contract job green; the endpoints are a thin layer on top of the commands that already exist and are tested here.
Covers attach (incl. is_primary), idempotent re-attach, detach, and findAllByWorkspaceId against a real Postgres, mirroring JooqProjectRepositoryRepositoryIntegrationTest. Keeps the new persistence class above the 80% jacoco bar (infrastructure/k8s is already excluded, so the orchestrator REPO_URLS path needs no unit coverage).
This was referenced Jun 6, 2026
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.
Why
A workspace was bound to exactly one repository (the denormalised
repository_id/repoUrl), so the orchestrator could only emit a singleREPO_URL. PR #589 taught the runner to clone aREPO_URLSlist, but nothing produced it.Changes
workspace_repositories(workspace_id, repository_id, is_primary, attached_at)junction. Existing repo-backed workspaces are backfilled with their primary repo (is_primary = TRUE). Mirrors the existingproject_repositoriesshape.WorkspaceRepositoryRepositoryport +JooqWorkspaceRepositoryRepository—attach/detach/findAllByWorkspaceId, a 1:1 mirror of the testedJooqProjectRepositoryRepository.NoSuchElementException); detach refuses to remove the primary (IllegalArgumentException).Fabric8AgentRunnerOrchestratorinjectsREPO_URLSfrom the workspace's non-primary attached repos (resolved to repo URLs), space-joined, and only when there is at least one — so a single-repo workspace's Pod env is byte-identical to before.REPO_URLstill carries the primary. The two new dependencies useObjectProvider, matching the existing deploy-key / github-link wiring.Scope: REST endpoints deferred
The attach/detach REST endpoints (and their request DTO) are intentionally not in this PR. Exposing them regenerates the committed
services/assistant-api/openapi.jsonand theassistant-uigenerated types, which the springdocexportOpenApiSpectask can only produce from a fully-booted app context (Testcontainers/Docker). Keeping this PR free of any HTTP-contract change keeps the OpenAPI Contract job green. The endpoints are a thin layer over the commands that already exist and are unit-tested here; the follow-up adds them plus the regenerated contract (one./gradlew :services:assistant-api:exportOpenApiSpec+pnpm --filter @personal-stack/assistant-ui contract:generateon a Docker host).Verification
generateJooqregenerates cleanly against V13 (backfill uses plainINSERT … SELECT; jOOQ's DDL parser rejectsON CONFLICT, redundant on a fresh table).ktlintCheckclean; no HTTP-contract change so the OpenAPI Contract job is unaffected.Completes the producer side of the multi-repo loop that PR #589 consumes. Independent of the council PRs (#586/#587/#588).