Forgejo: workspace index caches failed origin lookups as "not connected" #59
Labels
No labels
Agent
Ignore
Agent
In Progress
Agent
Planned
Agent
Planning
Agent
Ready
Agent
Triage
Compat
Breaking
Kind
Bug
Kind
Documentation
Kind
Enhancement
Kind
Feature
Kind
Security
Kind
Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
lucas/omp-deck#59
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Implementation: #60
Problem
On 2026-10-22, a comment on PR #56 (delivery
959dc166-9682-448b-91ce-7c53e39af6b8, 15:23:59Z) was dropped with "repository is not connected to a workspace". The bot postedNOT_CONNECTED, even though/workspace/omp-deckis registered and its origin isssh://git@git.sclu1034.dev/lucas/omp-deck.git. A PR #58 trigger at 15:22:08Z had matched the same workspace. The process that dropped the comment is gone. In the current environment,resolveRepoFromCwd("/workspace/omp-deck", "https://git.sclu1034.dev")returnslucas/omp-deck.Cause:
apps/server/src/forgejo/workspace-index.tsstores thenullfromrepoKeyin the index whether the origin really is somewhere else orgit remote get-url originfailed.readOriginUrl(forgejo/repo.ts) turns every nonzero exit intonulland throws away git's error.matchWorkspacesonly resolves workspaces that have no entry yet, so after one failed git call (e.g. when the directory is not mounted yet at container start) the workspace stays "not connected" until the next restart or until it is registered again. Nothing is logged.runGittells the cases apart:error: No such remote 'origin'): the repo has no origin. This is a real answer.fatal: not a git repository): this is also what an unmounted directory looks like.Decisions:
matchWorkspacescall until a lookup gives an answer. There is no backoff, because it is one local git call per failed workspace.log.warn, giving the workspace path and git's error text.Plan
Context
Make the workspace index cache only real answers about a workspace's origin, retry failed lookups on the next match, and log failures. Then a transient git failure can no longer make a registered repository "not connected" until restart.
Approach
Step 1 builds on its own and keeps existing tests green. Step 2 depends on step 1.
1. Report failures separately from "no origin" (
apps/server/src/forgejo/repo.ts)readOriginUrl: Implementation:runGit(cwd, ["remote", "get-url", "origin"], { readOnly: true, signal }).code === 0→{ ok: true, url: res.stdout.trim() || null }.code === 2→{ ok: true, url: null }.{ ok: false, error: gitErrorText(res, "git remote get-url origin failed") }. ImportgitErrorTextfrom../bridge/git-tools/run.ts;runGitis already imported there.readOriginUrlon top of it:const r = await lookupOrigin(cwd, signal); return r.ok ? r.url : null;. Keep its signature and doc comment. Its callers (checkout.ts,resolveRepoFromCwd) andresolveRepoFromCwd's callers (routes-forgejo.ts,tools/index.ts) stay unchanged. They don't cache anything.2. Cache only answers in the index (
apps/server/src/forgejo/workspace-index.ts)import { logger } from "../log.ts";andconst log = logger("forgejo:workspace-index");, following the same pattern ascheckout.ts.repoKey(path, instanceUrl)to returnPromise<string | null | undefined>.undefinedmeans the lookup failed and must not be cached. It callslookupOrigin(path)instead ofresolveRepoFromCwd:!r.ok→log.warn(\could not read origin of workspace ${path}: ${r.error}`), then returnundefined`.r.url === null→null.const ref = matchRemoteToInstance(r.url, instanceUrl); return ref ? \${ref.owner}/${ref.repo}`.toLowerCase() : null;`resolveRepoFromCwdimport withlookupOrigin, matchRemoteToInstance.ensureBuilt: only callnext.set(path, key)whenkey !== undefined.matchWorkspaces: in theunindexed.forEach, only set entries whose key is notundefined. A workspace without an entry is already inunindexedon every lookup, so this alone gives the retry on every lookup. Don't add any other retry state.noteWorkspaceAdded: only calltarget.set(ws.path, key)in the.thenwhenkey !== undefined. Leave the!cfgbranch (index.set(ws.path, null)) as is: no Forgejo config is an answer.indexto:/** Workspace path → lowercased \owner/repo`, or null when its origin is not on the configured instance or absent. Workspaces whose origin could not be read have no entry. */`. In the module doc comment, replace the sentence "Every lookup also resolves registered workspaces the index has no entry for yet (registered while the build ran, or whose resolution is still in flight), so a fresh registration is never missed." with "Every lookup also resolves registered workspaces the index has no entry for (registered while the build ran, resolution still in flight, or a previous attempt failed with a git error), so a fresh registration or a transient failure is never stuck."Critical files & anchors
apps/server/src/forgejo/workspace-index.ts—repoKey,ensureBuilt,matchWorkspaces,noteWorkspaceAdded.apps/server/src/forgejo/repo.ts—readOriginUrl(~147), newlookupOrigin.apps/server/src/forgejo/workspace-index.test.ts— theboot()andcheckout()helpers to reuse.Verification
Add these to
apps/server/src/forgejo/workspace-index.test.ts, reusingboot(),tmp()andcheckout(). Spawn git withBun.spawnSync(["git", ...], { cwd })ascheckout()does.boot().const dir = tmp("omp-deck-ws-index-repo-"). This is a plain directory (not a repo, so git exits 128), the way an unmounted directory looks.const ws = createWorkspace({ path: dir, name: "late" }).expect(await matchWorkspaces("o/late")).toEqual([]). Then rungit init -qandgit remote add origin https://git.example.com/o/late.gitindir.expect((await matchWorkspaces("o/late")).map((w) => w.id)).toEqual([ws.id]). This test fails on the current code.boot(). Make a dir withgit init -qonly andcreateWorkspaceit.expect(await matchWorkspaces("o/x")).toEqual([]). Thengit remote add origin https://git.example.com/o/x.git.expect(await matchWorkspaces("o/x")).toEqual([]). This is the documented behaviour: a changed origin is only picked up after a restart or registering again.apps/server/src/forgejo/repo.test.ts, add one test forlookupOrigin:{ ok: true, url };tmpRepo()with no origin →{ ok: true, url: null };ok: falsewitherrorcontainingnot a git repository;ok: false.Run from the repo root:
bun test apps/server/src/forgejo, thenbun run typecheck. Both must pass, and the existingresolveRepoFromCwdnull cases inrepo.test.ts(~162–172) must stay green.