Skip to content

feat(gitsync): mirror pull request heads as destination branches - #43

Merged
clouddrove-ci merged 7 commits into
masterfrom
feat/pr-sync-p1
Aug 19, 2026
Merged

feat(gitsync): mirror pull request heads as destination branches#43
clouddrove-ci merged 7 commits into
masterfrom
feat/pr-sync-p1

Conversation

@clouddrove-ci

Copy link
Copy Markdown
Member

Stacked on #42. Review that one first; the base retargets to master once it merges.

What

Phase 1 of four in the pull request replication program. The commits behind an open pull request now reach the destination as an ordinary branch, and the provider independent model the later phases build on is defined.

A pull request opened from a fork has no branch in the source repository, so today its commits exist nowhere in the mirror. With pull_requests.enabled on a mirror, the head is pushed as refs/heads/syncerd/pr/<number>.

The pull request object itself is not recreated at the destination. That is P2.

Two vendor facts that shaped this

Checked against vendor documentation on 2026-08-19, not assumed:

  • GitLab deletes refs/merge-requests/<iid>/head 14 days after the merge request closes.
  • Atlassian classifies refs/pull-requests/*/from as an internal implementation detail and states that any integration using it "is going to be racy and unreliable because that ref is not eagerly updated".
  • Azure DevOps has no head ref at all, only refs/pull/<id>/merge, which is absent whenever the pull request has conflicts.

So a ref-based strategy cannot be the only one. The head is fetched from the fork's own clone URL and head branch, which every provider's pull request API reports.

The load-bearing design decision

The head lands in the clone cache as an ordinary branch, not in a private namespace. Everything downstream then works unchanged:

  • the existing push refspecs carry it to the destination, no new refspec
  • mirror mode's --prune deletes the destination branch once the pull request closes and SyncerD stops fetching its head, so cleanup needs no bookkeeping
  • additive and fast-forward behave for these branches exactly as they do for any other

The cache's own fetch --prune does not cover these branches, since they are not tracked by the origin refspec, so PrunePRBranches removes the ones that are no longer wanted.

Behaviour worth calling out

  • Only fork heads get a branch. A head living in the source repository already mirrors under its own name and gets no second copy.
  • Pull requests are listed before the skip decision, and their head SHAs join the ref fingerprint. A new fork pull request changes no branch and no tag at the source, so without this the repository would be skipped and the pull request would never appear. Cost is one API call per repository per run, only for mirrors that enable this.
  • A source branch under the reserved prefix fails the repository with an error naming the branch, rather than the two silently overwriting each other every run.
  • One unreachable fork head is a warning, not a failure. A fork can be deleted or made private while its pull request stays open.
  • A failed listing fails the repository at a new pull-requests stage.

Security: read before enabling

Off by default, deliberately. A fork pull request head is code written by anyone who can open a pull request against the source. This pushes it to a destination branch, where it arrives as an ordinary push from a trusted mirror rather than as a fork pull request, so none of the source's fork-PR protections apply. If destination CI builds on branch push, it runs that code with whatever credentials that CI holds.

The runbook says this next to the setting and recommends excluding the branch_prefix namespace from destination CI. The prefix is one contiguous namespace so a single glob covers it.

Scope

GitHub sources only. A mirror enabling pull_requests with any other source type is rejected at startup with a message naming the phase. GitLab, Bitbucket, and Azure DevOps arrive in P4 behind the same interfaces.

Testing

Provider tests cover pagination, fork versus same-repository heads, deleted forks, merged-state mapping, and no token leakage on error. Engine tests use real local git repositories and prove the destination branch appears, that a same-repository head gets no copy, that a new pull request defeats the unchanged skip, and that the branch is pruned from the destination after the pull request closes. verify-syncerd full tier passes.

@clouddrove-ci
clouddrove-ci changed the base branch from fix/private-repo-discovery to master August 19, 2026 14:01
@clouddrove-ci
clouddrove-ci merged commit 371bc5c into master Aug 19, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants