feat(runs): per-run base branch — composer picker, PRs target the starting branch, afk_base_branch #131

Open
opened 2026-07-11 11:01:56 +02:00 by dominik.polakovics · 0 comments

What to build

Record the branch a run forked from and make the whole branch lifecycle honor it. Today the base is hard-coded to repo.DefaultBranch at four points — worktree fork (internal/instance/launch.go:98), PR-create base (internal/agentapi/handlers.go:627), merge target, and the teardown merged-check (internal/gitx/worktree.go:98) — and the run row never records what it started from.

End-to-end behavior:

  • runs.base_branch column (migration), snapshotted at spawn via LaunchSpec/instance.Launch. Existing rows without a value are treated as repo.DefaultBranch.
  • Branches endpoint: GET /api/v1/repos/{id}/branches does a best-effort fetch on the bare mirror, then lists origin branches (default branch first).
  • Composer picker (web/src/routes/NewRun.tsx): base-branch selection lives in the popover, offering all remote branches (managed lab//afk/ branches included — stacking on an unmerged agent branch is supported), default branch preselected. A non-default choice is promoted to a removable chip in the composer row. The selection is never sticky: it resets on composer load and on repo switch. StartParams gains the base field; AddWorktree's fail-loud fetch of origin/<base> remains the final validator.
  • PR targets the recorded base: labctl pr create uses the run's base_branch instead of repo.DefaultBranch; merges then land on the starting branch automatically since forge and builtin both merge into the PR/CR's stored base.
  • Vanished base fallback: if origin/<base> no longer exists at PR-create or merged-check time (e.g. a stacked parent was merged with delete-on-merge), fall back to repo.DefaultBranch and note the retarget in the PR body. Merged-checks in teardown/reaper/sweep consult the recorded base with the same fallback.
  • afk_base_branch repo setting (empty = DefaultBranch), edited in RepoSettings beside the AFK branch pattern; AFK launches record it as the run's base. DefaultBranch keeps meaning "the forge's real default" — it stays the delete-safety guard and the vanished-base fallback.

Verification item (do first, document the result in this issue): confirm on the pinned Forgejo version what happens to an already-open PR whose base branch is deleted (Gitea-lineage forges retarget child PRs to the deleted branch's own base) — this decides whether the post-create vanish case needs any code beyond the PR-create fallback.

Out of scope: per-issue base overrides for AFK; pre-biasing the composer picker from afk_base_branch.

Acceptance criteria

  • runs.base_branch recorded at spawn for manual and AFK runs; absent value falls back to repo.DefaultBranch everywhere
  • GET /repos/{id}/branches lists remote branches after a best-effort fetch
  • Composer offers base selection in the popover; non-default base shows as a removable chip; selection resets per load and on repo switch
  • Worktree forks from origin/<selected base>; spawn fails loud if the branch vanished
  • labctl pr create targets the recorded base; vanished base → default branch with a note in the PR body
  • Teardown/reaper/sweep merged-checks use the recorded base with default-branch fallback (stacked run merged into its parent is torn down correctly)
  • afk_base_branch repo setting surfaced in RepoSettings; AFK runs fork from and target it
  • Forgejo base-branch-deleted behavior for open PRs verified and documented in this issue
  • Tests cover spawn-with-base, PR retarget fallback, and merged-check against a non-default base

Blocked by

None - can start immediately (interacts with #130 only at the vanished-base fallback, which is safe to build regardless)

## What to build Record the branch a run forked from and make the whole branch lifecycle honor it. Today the base is hard-coded to `repo.DefaultBranch` at four points — worktree fork (`internal/instance/launch.go:98`), PR-create base (`internal/agentapi/handlers.go:627`), merge target, and the teardown merged-check (`internal/gitx/worktree.go:98`) — and the run row never records what it started from. End-to-end behavior: - **`runs.base_branch`** column (migration), snapshotted at spawn via `LaunchSpec`/`instance.Launch`. Existing rows without a value are treated as `repo.DefaultBranch`. - **Branches endpoint**: `GET /api/v1/repos/{id}/branches` does a best-effort fetch on the bare mirror, then lists origin branches (default branch first). - **Composer picker** (`web/src/routes/NewRun.tsx`): base-branch selection lives in the `…` popover, offering **all** remote branches (managed `lab/`/`afk/` branches included — stacking on an unmerged agent branch is supported), default branch preselected. A non-default choice is promoted to a removable chip in the composer row. The selection is never sticky: it resets on composer load and on repo switch. `StartParams` gains the base field; `AddWorktree`'s fail-loud fetch of `origin/<base>` remains the final validator. - **PR targets the recorded base**: `labctl pr create` uses the run's `base_branch` instead of `repo.DefaultBranch`; merges then land on the starting branch automatically since forge and builtin both merge into the PR/CR's stored base. - **Vanished base fallback**: if `origin/<base>` no longer exists at PR-create or merged-check time (e.g. a stacked parent was merged with delete-on-merge), fall back to `repo.DefaultBranch` and note the retarget in the PR body. Merged-checks in teardown/reaper/sweep consult the recorded base with the same fallback. - **`afk_base_branch`** repo setting (empty = `DefaultBranch`), edited in RepoSettings beside the AFK branch pattern; AFK launches record it as the run's base. `DefaultBranch` keeps meaning "the forge's real default" — it stays the delete-safety guard and the vanished-base fallback. Verification item (do first, document the result in this issue): confirm on the pinned Forgejo version what happens to an already-open PR whose base branch is deleted (Gitea-lineage forges retarget child PRs to the deleted branch's own base) — this decides whether the post-create vanish case needs any code beyond the PR-create fallback. Out of scope: per-issue base overrides for AFK; pre-biasing the composer picker from `afk_base_branch`. ## Acceptance criteria - [ ] `runs.base_branch` recorded at spawn for manual and AFK runs; absent value falls back to `repo.DefaultBranch` everywhere - [ ] `GET /repos/{id}/branches` lists remote branches after a best-effort fetch - [ ] Composer offers base selection in the `…` popover; non-default base shows as a removable chip; selection resets per load and on repo switch - [ ] Worktree forks from `origin/<selected base>`; spawn fails loud if the branch vanished - [ ] `labctl pr create` targets the recorded base; vanished base → default branch with a note in the PR body - [ ] Teardown/reaper/sweep merged-checks use the recorded base with default-branch fallback (stacked run merged into its parent is torn down correctly) - [ ] `afk_base_branch` repo setting surfaced in RepoSettings; AFK runs fork from and target it - [ ] Forgejo base-branch-deleted behavior for open PRs verified and documented in this issue - [ ] Tests cover spawn-with-base, PR retarget fallback, and merged-check against a non-default base ## Blocked by None - can start immediately (interacts with #130 only at the vanished-base fallback, which is safe to build regardless)
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/coding-lab#131
No description provided.