feat: pull request mirroring for GitLab, Bitbucket, Azure DevOps, and CodeCommit - #46
Merged
Conversation
Bitbucket Cloud has no reopen endpoint and CodeCommit forbids the CLOSED to OPEN transition, so reopening cannot be part of the writer contract every destination satisfies. PullRequestWriter now closes only, and reopening is an optional PullRequestReopener a destination may implement. A destination without it records the divergence and leaves the pull request closed rather than opening a second one for the same work. GitLab requires base, head, and start SHAs to anchor an inline comment, and CodeCommit requires a before and after commit id, so ReviewComment carries BaseSHA alongside CommitSHA. GitHub and Bitbucket ignore it.
docs: what each provider cannot represent
A destination that cannot reopen, which is Bitbucket and CodeCommit, fell through to UpdatePullRequest on a pull request it had just decided to leave closed. Both providers reject an update to a closed pull request, so the call errored before the divergence was ever recorded: the warning fired every run, a failure was appended every run, and the mirror never settled. The same path ran for a merged source whose destination was already closed whenever new activity advanced its timestamp. An update now runs only while the destination is open, and an unfixable divergence records itself and returns. A recorded divergence is also re-checked each run rather than trusted, so a destination somebody reopened by hand is picked back up; previously the marker made statesAgree return true forever and the pull request could never be closed again. A source that reports no update timestamp, which Azure DevOps does not, is no longer watermarked. Skipping on a zero time mirrored such a pull request once and then froze it: no title, body, comment, or verdict change ever reached the destination again.
CodeCommit's pull request cache outlived the run that filled it. A provider is built once and reused for every cron tick, so a CodeCommit source served the first run's snapshot forever: later pull requests were never mirrored, closed ones never closed, and the repository was skipped as unchanged because the same stale set fed the fingerprint. The listing now re-enumerates and the cache only serves the finds that follow it. CodeCommit's FindPullRequest searched open pull requests only, so a destination pull request somebody had closed was invisible and a lost state file produced a second one for the same branch. A missed lookup now enumerates the closed ones too. CodeCommit treated every failure to post a located comment as a rejected anchor, so throttling or an expired credential permanently downgraded the comment to a discussion comment. Only genuine location errors downgrade. CodeCommit's ListReviews answered "no reviews" when the underlying call failed, and the caller deletes mirrored comments it no longer sees at the source, so one throttled call deleted every mirrored verdict. It returns the error now. Azure DevOps stood the creation date in for a missing update timestamp, which froze the engine's watermark and mirrored each pull request exactly once. It reports no timestamp instead, and the engine reconciles every run. Azure DevOps added labels and never removed them, re-POSTing every label on every run and failing the pull request when one already existed. Labels are reconciled against the source, and truncation counts characters rather than bytes so a non-ASCII description is neither cut to a third of the limit nor split through a rune. GitLab decided a comment was on the left side by testing an empty path, but GitLab populates new_path on both sides of a text diff, so every deleted line comment anchored to line 0 on the right. It tests the line instead. GitLab's transient locked state is no longer read as closed, which used to close the destination and post "closed without merging" moments before the source became merged. Bitbucket omitted draft on update, so a pull request that left draft at the source stayed a draft at the destination, and its lookup missed superseded pull requests. GitHub never reported a base SHA, so every inline comment mirrored to GitLab or CodeCommit, which both require one, silently downgraded to a discussion comment. Turning comments or reviews off deleted everything previously mirrored in that category, because nothing in it appeared in the seen set. Deletion is now scoped to categories the run actually listed.
Merged
clouddrove-ci
added a commit
that referenced
this pull request
Aug 19, 2026
* chore(release): 0.2.1 Cuts the Unreleased section carrying destination pull request mirroring, the four remaining provider implementations, and the fixes from the review of #46, and repoints the Action examples at the new tag. Also corrects the 0.2.0 section, which claimed destination pull request mirroring. That entry was written under Unreleased on the #44 branch, and by the time it merged the release PR had already renamed that heading to 0.2.0, so a clean auto-merge filed a feature under a tag that never contained it. The 0.2.0 section now matches what v0.2.0 actually shipped. * fix(azuredevops): send a username with the PAT on the git transport pat mode presented an empty username with the PAT as the password. SyncerD hands git its credential through a helper that prints "username=<value>", and git reads an empty value as no username at all: it then tries to ask, terminal prompts are disabled, and every clone and push failed with "could not read Username", which names nothing about the provider or the config that produced it. Azure DevOps ignores the username on this path and authenticates on the token alone, so the value is free. The organisation name is sent because it makes the credential legible in a trace without being a secret. A basic credential carrying a password with no username is now rejected by the runner as well, so a future provider with the same gap fails with a sentence that names the cause rather than a bare git error. A bearer credential still needs no username, and an empty credential, which is what a local path uses, stays valid. The REST API path was never affected: it carries the token in an Authorization header, where an empty username is what Azure DevOps documents. --------- Co-authored-by: Anmol Nagpal <ianmolnagpal@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Final phase of the pull request replication program. Every provider type can now mirror pull requests, in any direction.
Scope correction
CodeCommit was excluded when this program was planned, on my claim that it had no pull request API worth mirroring. That was wrong on both counts. It has a full one, already in the pinned SDK, and AWS returned the service to general availability on 2025-11-24 after closing it to new customers in 2024. It is included here, so the answer to "are all the other providers fixed" is yes rather than four out of five.
Two contradictions with P2, resolved rather than papered over
The providers disagree about what a pull request has, and two of the disagreements broke rules P2 had already implemented.
Reopen is not universal. P2 reopens a destination pull request that was closed by hand while its source is open, because the source is the authority. Bitbucket has no reopen endpoint at all, and CodeCommit documents that the only valid status transition is
OPENtoCLOSED. SoPullRequestWriternow closes only, and reopening is an optionalPullRequestReopener. A destination without it warns once, records the divergence in state, and leaves the pull request closed. It does not open a second pull request for the same work, and it does not retry on every run: the recorded divergence satisfies the skip check, which otherwise would have cost a pointless write per run forever.The anchor model is not GitHub's. GitLab refuses to anchor an inline comment without base, head, and start SHAs; CodeCommit needs a before and after commit id.
ReviewCommentgainedBaseSHAalongsideCommitSHA. Where a source cannot supply them, the comment downgrades to a discussion comment through the path P2 already built.What each provider cannot represent
Documented in the runbook as a table, because an operator choosing a destination deserves to know before they choose it:
Draft:title prefixNothing here fails a run; a missing capability is logged and skipped, never faked.
Two provider quirks worth knowing:
Draft:. This is the one place SyncerD does not copy a title verbatim, and the prefix is stripped when the source leaves draft.ListPullRequestsreturns ids only, so a listing costs oneGetPullRequestper id, and there is no branch filter at all, soFindPullRequestenumerates and matches client side. The enumeration is cached per repository per run so the listing and every find share one pass.Testing
Per provider
httptestsuites, plus the CodeCommit fakeapifactory (no network, no credentials). A build-time matrix exercises all 20 source-to-destination pairs, and a separate test asserts the reopen capability is present on exactly GitHub, GitLab, and Azure DevOps and absent on Bitbucket and CodeCommit, since the engine branches on that type assertion. Each provider's tests also assert no merge endpoint is ever reached and no token leaks into an error.verify-syncerdfull tier passes: static and build, helm, live Docker Hub to local registry sync with state skip, Slack failure notification, Docker image build, clean teardown.