Skip to content

fix: keep workspaces tied to repository identity - #993

Merged
wesm merged 1 commit into
mainfrom
workspace-error-renamed
Sep 2, 2026
Merged

fix: keep workspaces tied to repository identity#993
wesm merged 1 commit into
mainfrom
workspace-error-renamed

Conversation

@wesm

@wesm wesm commented Aug 29, 2026

Copy link
Copy Markdown
Member

Workspaces now stay attached to the same repository when it is renamed. If another repository later takes the old owner/name route, Forge creates a separate workspace and checkout instead of rejecting the request or reusing the previous repository's files.

  • Store provider workspaces by stable repository ID, while continuing to show the repository's current owner and name.
  • Give new clones and worktrees identity-specific paths. Existing managed clones can be recovered across renames only when their history proves they belong to the same repository.
  • Check repository identity before and after network Git operations. If the route changes during a fetch, restore the existing clone instead of keeping data from the new occupant.
  • Refresh launch data, pull request state, and workspace associations by repository ID. Legacy rows that cannot be identified safely are retired without deleting uncommitted work.
  • Update migration 55 to backfill unambiguous rows, remove duplicate workspaces for the same item, and add the stable-ID constraint. No additional migration is introduced.

Tests cover route reuse, consecutive renames, clone rollback and recovery, dirty-workspace preservation, launch data, database migration and cleanup, and concurrent deletion handling. The relevant Go suites, vet, lint, migration-history check, and repository hooks pass locally.

@roborev-ci

This comment has been minimized.

@mariusvniekerk mariusvniekerk self-assigned this Aug 30, 2026
mariusvniekerk added a commit that referenced this pull request Aug 30, 2026
Workspace listing resolved each stable repository with a nested read while the outer result rows still held a pool connection. Concurrent callers could consume every read connection and stop all list requests. Resolve the current repository route in the list query instead.

Base-branch sync also acquired the reconciliation read lock again during repository verification. A queued writer could then block the nested read while the outer read prevented that writer from proceeding. Reuse the existing lock for verification.

The existing rename and route-reuse tests now assert the stable workspace ownership introduced by this pull request.

Review decision: the user marked route-keyed local Git storage as not an issue, so this commit makes no change for that finding. Source: roborev-ci comment #993 (comment).

Generated with Codex
Co-authored-by: Codex <noreply@openai.com>
@roborev-ci

This comment has been minimized.

mariusvniekerk added a commit that referenced this pull request Aug 30, 2026
Existing databases can contain the same repository item under both sides of a rename. Assigning both rows the same repository ID made the new unique index reject the migration and prevented startup. Keep the newest workspace and delete the older workspace as one unit instead of extending legacy route support.

Background pull request association and pushed-head refreshes also resolved mutable routes after loading a workspace. A replacement repository could therefore receive work for the old workspace. Carry the stable repository ID through refresh operations and stop when that repository is inactive.

Review source: #993 (comment)

Generated with Codex
Co-authored-by: Codex <noreply@openai.com>
@roborev-ci

This comment has been minimized.

@roborev-ci

roborev-ci Bot commented Aug 31, 2026

Copy link
Copy Markdown

roborev: Combined Review (290959e)

Verdict: 4 issues remain, including 1 High-severity and 3 Medium-severity findings.

High

  • internal/server/workspaceapi/workspace_pushed_head.go:138-147, internal/github/sync.go:11040-11043: A discarded route-fenced CI refresh returns nil, causing the completion handler to report success, mark the observer refresh complete, and broadcast a refresh despite no persisted CI state. Return an explicit skipped/stale result and only mark success or broadcast when the guarded update applies.

Medium

  • internal/workspace/manager.go:1499-1557: RefreshWorkspaceHeadRepoSnapshot resolves repositories by mutable owner/name, allowing an old workspace after route reuse to read and persist metadata from the replacement repository. Resolve via ws.RepoID and require the update to match the stable repository ID.

  • internal/workspace/manager.go:1125-1140: If a loaded workspace with RepoID == 0 is deleted, reload returns nil and verification falls through to route-based checks, potentially continuing setup with stale state. Return ErrWorkspaceNotFound whenever an ID-bearing workspace cannot be reloaded.

  • internal/github/sync.go:12716-12727, internal/github/sync.go:12865-12875: SyncMRForRepository validates expectedRepoID only after reconciliation may have mutated repository identity, allowing stale queued requests to alter route ownership before rejection. Validate or capture the stable route fence before reconciliation and make reconciliation honor it.


Reviewers: 2 done | Synthesis: codex, 10s | Total: 12m18s

mariusvniekerk added a commit that referenced this pull request Aug 31, 2026
Workspace listing resolved each stable repository with a nested read while the outer result rows still held a pool connection. Concurrent callers could consume every read connection and stop all list requests. Resolve the current repository route in the list query instead.

Base-branch sync also acquired the reconciliation read lock again during repository verification. A queued writer could then block the nested read while the outer read prevented that writer from proceeding. Reuse the existing lock for verification.

The existing rename and route-reuse tests now assert the stable workspace ownership introduced by this pull request.

Review decision: the user marked route-keyed local Git storage as not an issue, so this commit makes no change for that finding. Source: roborev-ci comment #993 (comment).

Generated with Codex
Co-authored-by: Codex <noreply@openai.com>
mariusvniekerk added a commit that referenced this pull request Aug 31, 2026
Existing databases can contain the same repository item under both sides of a rename. Assigning both rows the same repository ID made the new unique index reject the migration and prevented startup. Keep the newest workspace and delete the older workspace as one unit instead of extending legacy route support.

Background pull request association and pushed-head refreshes also resolved mutable routes after loading a workspace. A replacement repository could therefore receive work for the old workspace. Carry the stable repository ID through refresh operations and stop when that repository is inactive.

Review source: #993 (comment)

Generated with Codex
Co-authored-by: Codex <noreply@openai.com>
@mariusvniekerk
mariusvniekerk force-pushed the workspace-error-renamed branch from 290959e to c3e81d2 Compare August 31, 2026 17:17
@roborev-ci

roborev-ci Bot commented Aug 31, 2026

Copy link
Copy Markdown

roborev: Combined Review (c3e81d2)

Verdict: Two high-severity and one medium-severity issues remain around repository identity and legacy workspace refreshes.

High

  • internal/workspace/manager.go:477-481 — PR and issue workspaces created from launch specs still reject routes with historical occupants, preventing replacement repositories from creating workspaces on reused routes. Resolve the launch spec’s stable provider identity, assign RepoID, and validate that identity instead of applying the legacy route-occupancy fence.

  • internal/db/queries_workspace_launch_specs.go:72-95 — A repository replacement can occur between the manager’s route check and prepareWorkspaceInsert, allowing a workspace to bind to the replacement repository while its launch spec describes the original. Under the reconciliation read lock, verify that the active catalog repository’s provider ID matches spec.Repository.PlatformRepoID before inserting either row.

Medium

  • internal/workspace/pushed_head_observer.go:339-349, internal/server/workspaceapi/workspace_pushed_head.go:290-299 — Legacy workspaces with RepoID = 0 but a launch spec emit pushed-head updates with repository ID zero. Queued handlers then silently skip refreshes, and all such updates share one queue key. Resolve the launch spec’s provider ID to an active catalog repository and emit its internal ID; otherwise fail closed for ambiguous legacy routes.

Reviewers: 2 done | Synthesis: codex, 8s | Total: 17m2s

@roborev-ci

roborev-ci Bot commented Aug 31, 2026

Copy link
Copy Markdown

roborev: Combined Review (e7f5ae7)

Verdict: Two high-severity issues remain; reviewers otherwise found no additional problems.

High

  • internal/workspace/manager.go:1052-1060 — Clone storage remains keyed by platform/host/owner/name despite stable repository ID route reuse. A replacement repository may reuse the prior repository’s bare clone and fetch from its old origin. Partition clone paths and cleanup/provenance lookups by stable provider repository ID, or retain the route-reuse fence. Add an integration test covering two repository incarnations.

  • internal/workspace/launch_spec.go:131-132 — An unexpired launch spec can retain an old owner/name route after a repository rename. Because RequireWorkspaceLaunchSpec skips refresh while the lease is valid, strict route validation rejects the workspace and blocks setup and provider operations. Validate stable identity separately from mutable route fields, or refresh/adopt the launch spec when the stable repository is renamed while preserving provider-ID mismatch protection.


Reviewers: 2 done | Synthesis: codex, 7s | Total: 15m48s

@wesm
wesm force-pushed the workspace-error-renamed branch from e7f5ae7 to 0556cb8 Compare August 31, 2026 21:38
@roborev-ci

roborev-ci Bot commented Aug 31, 2026

Copy link
Copy Markdown

roborev: Combined Review (0556cb8)

Verdict: Changes need revision due to two high-severity identity-integrity issues and one medium-severity replacement-repository workflow issue.

High

  • internal/workspace/manager.go:1040-1070, 2135-2145 — Clone and configured worktree paths remain keyed only by mutable repository route. A replacement repository could reuse the previous repository’s checkout and fetch its content. Thread stable provider identity through workspace Git operations and validate configured bases against that identity.

  • internal/db/queries_workspace_launch_specs.go:80-95, 232-287 — Launch-spec persistence does not enforce PlatformRepoID. A route replacement between precheck and locked insert could bind a workspace to repository B while storing launch facts for repository A; same-route refreshes for different repositories are also accepted. Compare stable IDs inside the reconciliation-locked transaction and reject mismatches.

Medium

  • internal/workspace/manager.go:477-481, 568-572 — Provider-backed PR and issue creation rejects routes with historical occupants even when the route now resolves to a replacement repository, preventing the separate workspace identity promised by the change. Apply the historical-route fence only when the repository cannot be resolved.

Reviewers: 2 done | Synthesis: codex, 8s | Total: 21m18s

@wesm
wesm force-pushed the workspace-error-renamed branch from 0556cb8 to 6191c12 Compare September 1, 2026 14:55
@roborev-ci

roborev-ci Bot commented Sep 1, 2026

Copy link
Copy Markdown

roborev: Combined Review (6191c12)

Verdict: High-risk repository identity and clone/worktree isolation issues remain, plus two medium-severity race and validation flaws.

High

  • internal/workspace/manager.go:510-514, 609-613, 982-986 — Worktree paths remain keyed only by mutable provider/host/owner/name. After route reuse, different repositories can target the same filesystem path, causing setup failures or path ownership conflicts. Partition worktree destinations by stable repository identity or workspace ID, with migration for existing paths.

  • internal/workspace/manager.go:1968-1975, 2181-2190 — Identity-aware clone paths still embed mutable owner/name components. Repository renames leave worktrees linked to old clone paths, while recovery searches the renamed path and reports a mismatch. Make clone storage independent of mutable routes and migrate or explicitly adopt legacy registrations.

Medium

  • internal/workspace/launch_spec.go:131-132 — Valid launch specs are not refreshed after a repository rename. GetWorkspace projects current route fields from the stable repository ID, causing ValidateWorkspace to reject the old route and block setup or provider operations. Reconcile mutable route fields using the verified stable identity while rejecting stable-ID changes.

  • internal/workspace/manager.go:2182SetupWithOptions captures a route fence, but workspaceSetupGitDir uses unvalidated EnsureClone. A route replacement during fetch can leave repository B’s contents in repository A’s clone after the later fence check rejects setup. Use EnsureCloneValidated with the captured route-fence callback and apply equivalent validation to racing clone-creation paths.


Reviewers: 2 done | Synthesis: codex, 9s | Total: 20m45s

@wesm
wesm force-pushed the workspace-error-renamed branch from 6191c12 to f8dac58 Compare September 1, 2026 15:13
@roborev-ci

roborev-ci Bot commented Sep 1, 2026

Copy link
Copy Markdown

roborev: Combined Review (f8dac58)

Verdict: Two high-severity issues found; the change is not ready to merge.

High

  • internal/db/migrations/000055_workspace_repository_identity.down.sql:8 — Downgrade fails under SQLite because repo_id is dropped while referenced by a foreign-key constraint. Rebuild forge_workspaces without the column, copy rows, and recreate the pre-55 indexes and constraints.

  • internal/workspace/manager.go:2181-2184, 5396-5453 — Setup performs clone, branch, merge-request fetches, and repository-hook network work without route-fenced validation. A repository replacement can therefore contaminate the old stable-ID clone before setup fails. Fence every network Git operation, invalidate contaminated clones, and check the fence after the final network operation.


Reviewers: 2 done | Synthesis: codex, 9s | Total: 23m29s

@wesm
wesm force-pushed the workspace-error-renamed branch from f8dac58 to a375326 Compare September 1, 2026 15:34
@roborev-ci

This comment has been minimized.

@wesm
wesm force-pushed the workspace-error-renamed branch from a375326 to 5bda0b3 Compare September 1, 2026 19:49
@roborev-ci

roborev-ci Bot commented Sep 1, 2026

Copy link
Copy Markdown

roborev: Combined Review (5bda0b3)

Verdict: The change has one critical, two high, and one medium-severity issue requiring fixes.

Critical

  • internal/workspace/manager_test.go:5144-5145 — Test repositories inherit Git environment variables/configuration, allowing hooks, templates, or repository operations to affect surrounding checkouts. Use isolated Git test helpers with sanitized variables/configuration and fixtures outside all repositories and worktrees.

High

  • internal/workspace/manager.go:1422-1435, 2648-2711 — Route-fence validation does not cover subsequent network fetches for the base branch or merge-request head. A reused route could fetch replacement-repository objects into managed clones or local checkouts before validation detects the change. Validate before and after every fetch and clean or quarantine affected state.

  • internal/workspace/manager.go:1629-1631RefreshWorkspaceHeadRepoSnapshot resolves merge requests by mutable workspace route instead of stable RepoID, potentially persisting replacement-repository data onto the old workspace. Resolve by RepoID and add repository-ID predicates to related workspace updates.

Medium

  • internal/db/workspace_subjects.go:91-95, exposed by internal/db/queries_workspace_launch_specs.go:322-327 — Stable repository renames leave legacy workspace route keys unchanged, causing renamed workspaces without provider MR/issue rows to disappear from subject snapshots and own references. Match stable workspaces by w.repo_id = r.id, retaining route-key matching only for legacy rows.

Reviewers: 2 done | Synthesis: codex, 10s | Total: 35m16s

@wesm
wesm force-pushed the workspace-error-renamed branch from 5bda0b3 to 61f4cf8 Compare September 1, 2026 20:35
@roborev-ci

roborev-ci Bot commented Sep 1, 2026

Copy link
Copy Markdown

roborev: Combined Review (61f4cf8)

Verdict: Two high-severity and one medium-severity issue remain; the change is not clean.

High

  • internal/workspace/manager.go:1984-1987 — Migration 55 assigns stable IDs to existing workspaces without migrating their legacy managed-clone paths. Retries or recovery may classify valid legacy clones as mismatched and fail setup. Support safely validated legacy paths or migrate clones before switching namespaces.

  • internal/server/workspaceapi/routes_handlers.go:1098-1129, 1235, 1264 — Workspace refresh resolves repositories and syncs items by mutable route. Reused routes can cause inactive workspaces to refresh and write provider data for replacement repositories. Resolve stable workspaces by summary.Workspace.RepoID, reject inactive identities, and use repository-ID/fenced sync methods; retain route lookup only for legacy rows.

Medium

  • internal/server/workspaceapi/routes_handlers.go:1817-1831 — Merge-target branch lookup uses the mutable workspace route, so reused routes can make diffs use the replacement repository’s base branch. Query by stable repository ID and fail closed when the repository is inactive.

Reviewers: 2 done | Synthesis: codex, 9s | Total: 28m38s

@roborev-ci

roborev-ci Bot commented Sep 1, 2026

Copy link
Copy Markdown

roborev: Combined Review (e2032b3)

Verdict: High-severity migration and workspace-replacement regressions, plus a Medium nested GitLab routing issue.

Critical

None.

High

  • internal/workspace/manager.go:2050-2057, 2264-2273; internal/db/migrations/000055_workspace_repository_identity.up.sql:1-13 — The migration backfills repo_id but does not migrate existing worktree paths or route-keyed managed clone directories. Existing managed-clone workspaces may fail provenance checks and become unusable after upgrade. Add migration or compatibility handling for legacy locations.

  • internal/server/workspaceapi/routes_handlers.go:258-262, 339-350, 686-690 — Displaced workspaces are force-deleted before replacement creation succeeds. A failed creation can permanently remove the existing workspace, including uncommitted work. Use two-phase replacement or rollback protection.

Medium

  • internal/workspace/manager.go:152-178 — Displacement detection assumes the owner is a single path component, so nested GitLab namespaces such as group/subgroup/project are not recognized and may create duplicate workspaces. Handle nested owners and add coverage.

Reviewers: 2 done | Synthesis: codex, 8s | Total: 19m48s

@wesm
wesm force-pushed the workspace-error-renamed branch from e2032b3 to edf815e Compare September 1, 2026 21:44
@roborev-ci

roborev-ci Bot commented Sep 1, 2026

Copy link
Copy Markdown

roborev: Combined Review (edf815e)

Verdict: Two medium-severity repository-identity issues remain; one low-severity query concern is omitted.

Medium

  • internal/workspace/manager.go:1628-1630, internal/db/queries.go:5440-5456 — Head refresh resolves repositories by mutable route instead of ws.RepoID, potentially applying another repository’s MR to the wrong workspace. The update query also lacks a workspace repo_id predicate. Resolve by stable ID and require forge_workspaces.repo_id to match.

  • internal/db/queries.go:5891-5895, internal/db/workspace_subjects.go:89-96 — The launch-spec-only fallback still matches workspaces by stored route keys. Repository renames can make activity disappear when no provider replica exists. Match using w.repo_id = q.repo_id and add a rename regression test.


Reviewers: 2 done | Synthesis: codex, 7s | Total: 19m45s

@wesm
wesm force-pushed the workspace-error-renamed branch from edf815e to b44558f Compare September 1, 2026 22:24
@roborev-ci

roborev-ci Bot commented Sep 1, 2026

Copy link
Copy Markdown

roborev: Combined Review (b44558f)

Verdict: One medium-severity issue remains around repository rename recovery and cleanup.

Medium

  • internal/workspace/manager.go:1740-1761, 1951-2020 — After a repository rename, existing workspaces may still reference the old worktree and clone path. Managed-clone discovery only checks the current route, while ownership validation rejects the old clone’s origin, causing valid worktrees to be treated as unrelated and potentially breaking setup recovery and cleanup. Resolve candidates using stable repository identity plus historical routes, or persist the clone path, while retaining route-fence checks for network operations.

Reviewers: 2 done | Synthesis: codex, 7s | Total: 35m5s

@wesm
wesm force-pushed the workspace-error-renamed branch from b44558f to f0ab6b1 Compare September 2, 2026 00:37
@roborev-ci

roborev-ci Bot commented Sep 2, 2026

Copy link
Copy Markdown

roborev: Combined Review (f0ab6b1)

Verdict: One medium-severity database connection deadlock risk was identified.

  • Mediuminternal/db/queries_workspace_launch_specs.go:444-448: ListUnpreparedProviderWorkspacesAt keeps result rows open while issuing additional queries via d.scanWorkspace and GetWorkspaceLaunchSpec. With a single-connection read pool, this can block indefinitely; concurrent calls may also exhaust the pool. Materialize and close the rows before per-workspace queries, or combine the data into one statement.

Reviewers: 2 done | Synthesis: codex, 6s | Total: 20m59s

@wesm
wesm force-pushed the workspace-error-renamed branch from f0ab6b1 to b8a2b76 Compare September 2, 2026 00:58
@roborev-ci

roborev-ci Bot commented Sep 2, 2026

Copy link
Copy Markdown

roborev: Combined Review (b8a2b76)

Verdict: High-severity route-validation issue found, plus one medium-severity legacy clone-discovery issue.

High

  • internal/workspace/manager.go:2798-2804, 2839-2863 — Route validation is not passed into addWorktree or its base/MR fetches. Reused routes may import replacement-repository refs before a later fence check fails, leaving contaminated refs in the clone even after worktree rollback.
    • Fix: Thread route validation through every network fetch and clean up or restore fetched refs on validation failure. Add regression coverage for route reuse during base and merge-request-head fetching.

Medium

  • internal/workspace/manager.go:2046-2073 — Historical routes are searched only for identity-scoped clone paths. Workspaces created before identity paths existed may retain legacy route-based clones that cannot be reused or cleaned after a repository rename.
    • Fix: Safely discover legacy paths for historical routes proven to belong to the same stable repository, with coverage for pre-identity clones followed by repository rename and setup retry.

Reviewers: 2 done | Synthesis: codex, 8s | Total: 40m51s

@wesm
wesm force-pushed the workspace-error-renamed branch 7 times, most recently from 8a62a7f to e4126b1 Compare September 2, 2026 02:38
Repository routes can change after a rename or removal. Route-keyed workspace records can then reject the renamed repository or follow a later occupant of the old route.

Persist the catalog repository ID on provider workspaces so a rename keeps its workspace associations and route reuse creates a separate workspace, clone, and worktree namespace. Migration 55 backfills unambiguous routes and removes duplicate workspaces for the same repository item before adding the stable-ID constraint.

Existing route-keyed clones remain usable after backfill only when their route has a single historical owner. A replacement repository gets a separate workspace without deleting the previous checkout. Launch facts, activity projections, pull request trust facts, and network Git work remain fenced to the stable repository identity throughout setup and refresh.

- Recover identity-scoped and pre-identity managed clones across consecutive renames by searching safe routes from the repository's own history and retargeting network work to the fenced current route.
- Roll existing shared clones back after a rejected fetch while removing fresh invalid clones, so linked worktrees survive without retaining replacement-repository refs or origins.
- Retire unresolved legacy workspaces through dirty-aware deletion and atomically leave failures stable until explicit user action.
- Preserve inactive repository identity tombstones while workspaces reference them, so provider-host purge cannot null stable IDs into the legacy uniqueness lane.
- Close preparation inventory cursors before per-workspace identity and launch-fact reads so bounded SQLite read pools cannot wait on themselves.

Generated with Codex
Co-authored-by: Marius van Niekerk <marius.v.niekerk@gmail.com>
Co-authored-by: Codex <noreply@openai.com>
@wesm
wesm force-pushed the workspace-error-renamed branch from e4126b1 to 16abb33 Compare September 2, 2026 02:50
@roborev-ci

roborev-ci Bot commented Sep 2, 2026

Copy link
Copy Markdown

roborev: Combined Review (16abb33)

Summary: The changes consistently bind workspace operations, clone paths, synchronization, and projections to stable repository identity with route-fence checks.

No issues found.


Reviewers: 2 total (1 done, 1 skipped) | Synthesis: codex | Total: 18m22s

@wesm
wesm merged commit 42c7ccc into main Sep 2, 2026
17 checks passed
@wesm
wesm deleted the workspace-error-renamed branch September 2, 2026 03:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants