feat(ssh): AGit PR creation in ssh-only mode - push refs/for/<base>, deterministic topic, closes #N linkage, metadata degradation #327

Closed
opened 2026-07-21 13:57:46 +00:00 by hexajon · 0 comments
hexajon commented 2026-07-21 13:57:46 +00:00 (Migrated from codeberg.org)

Follow-up from the #298 spike (decision: keep, scoped). AGit becomes the PR-creation path for ssh-only mode (#292's madtea.git-protocol = ssh-only): git push origin HEAD:refs/for/<base> with push options creates or updates the PR entirely over SSH, no token. Scope is bare create/update - merge and all richer PR metadata stay API-side, so in ssh-only they degrade per #295's typed-degradation model, not silently.

Design, from the probe evidence on #298:

  • finish's ssh-only push leg targets refs/for/<base> with -o topic=<derived deterministically from the local branch name> so every re-push of the same branch updates the SAME PR; -o title=/-o description= are passed on the FIRST push only (the server ignores them on updates - do not pretend otherwise in output).
  • Non-fast-forward pushes need -o force-push=true; detect the rejection (the server names the option in its error) and either retry with the option or surface a teaching error - decide during build, but never force silently on the first attempt.
  • The PR number/URL comes from the push's stderr remote: lines (pulls/(\d+)); parse it tolerantly ("Visit the existing pull request" appears on both create and update; * [new reference] vs <old>..<new> distinguishes them). This is text scraping - keep the parser small, pinned by fixtures of real push output, and degrade to "PR created, number unknown - check the forge" rather than failing the finish if the format shifts.
  • No head branch exists server-side (head is refs/pull/N/head, empty label) and none locally either - so branch-name issue auto-close cannot work on this path. Put closes #N in the PR body from the branch-detected issue instead, honoring the same dependency safety check as the normal path.
  • Watch out documented in the probe: UNKNOWN push options are silently ignored by the server - a typo'd option gives rc=0 and no feedback. Our own option strings are constants, so this mainly matters for tests (pin the exact options we emit).
  • After merge there is no topic branch to delete, locally or remotely - the cleanup legs skip with a note, not an error.

Tests (fixtures from the probe transcript): create, ff update same topic, non-ff rejected then force-push retry, nonexistent base rejection, no-new-commits rejection, stderr URL parse on create and update wording, closes-linkage in body.

Out of scope: any change to token-mode finish; merge over SSH (stays API/web); rich metadata over AGit (the -o vocabulary is topic/title/description/force-push only).

Refs #187, #298. Depends on #294 (the ssh push leg) landing first.

Follow-up from the #298 spike (decision: keep, scoped). AGit becomes the PR-creation path for ssh-only mode (#292's `madtea.git-protocol = ssh-only`): `git push origin HEAD:refs/for/<base>` with push options creates or updates the PR entirely over SSH, no token. Scope is bare create/update - merge and all richer PR metadata stay API-side, so in ssh-only they degrade per #295's typed-degradation model, not silently. Design, from the probe evidence on #298: - finish's ssh-only push leg targets `refs/for/<base>` with `-o topic=<derived deterministically from the local branch name>` so every re-push of the same branch updates the SAME PR; `-o title=`/`-o description=` are passed on the FIRST push only (the server ignores them on updates - do not pretend otherwise in output). - Non-fast-forward pushes need `-o force-push=true`; detect the rejection (the server names the option in its error) and either retry with the option or surface a teaching error - decide during build, but never force silently on the first attempt. - The PR number/URL comes from the push's stderr `remote:` lines (`pulls/(\d+)`); parse it tolerantly ("Visit the existing pull request" appears on both create and update; `* [new reference]` vs `<old>..<new>` distinguishes them). This is text scraping - keep the parser small, pinned by fixtures of real push output, and degrade to "PR created, number unknown - check the forge" rather than failing the finish if the format shifts. - No head branch exists server-side (head is `refs/pull/N/head`, empty label) and none locally either - so branch-name issue auto-close cannot work on this path. Put `closes #N` in the PR body from the branch-detected issue instead, honoring the same dependency safety check as the normal path. - Watch out documented in the probe: UNKNOWN push options are silently ignored by the server - a typo'd option gives rc=0 and no feedback. Our own option strings are constants, so this mainly matters for tests (pin the exact options we emit). - After merge there is no topic branch to delete, locally or remotely - the cleanup legs skip with a note, not an error. Tests (fixtures from the probe transcript): create, ff update same topic, non-ff rejected then force-push retry, nonexistent base rejection, no-new-commits rejection, stderr URL parse on create and update wording, closes-linkage in body. Out of scope: any change to token-mode finish; merge over SSH (stays API/web); rich metadata over AGit (the -o vocabulary is topic/title/description/force-push only). Refs #187, #298. Depends on #294 (the ssh push leg) landing first.
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
sixfold-space/madtea#327
No description provided.