Skip to content

Move repository bindings when a credential is disconnected - #2122

Merged
ppXD merged 1 commit into
mainfrom
fix/move-bindings-when-a-credential-is-disconnected
Oct 18, 2026
Merged

ppXD merged 1 commit into
mainfrom
fix/move-bindings-when-a-credential-is-disconnected

Conversation

@ppXD

@ppXD ppXD commented Oct 18, 2026

Copy link
Copy Markdown
Owner

Summary

  • Disconnecting a credential now moves the repositories and group hooks bound through it in the same unit of work. RevokeCredentialCommand takes an optional MoveBindingsToCredentialId. Left out, the bindings follow the owner's only other live connection, but only when the owner is the one disconnecting a credential that is still live. An Admin cutting off a teammate's token, a retry of a disconnect already made, a team token, and a choice between two or more connections all move nothing, as before. Connect-then-disconnect now ends where disconnect-then-connect always did. Unlinking a provider identity ends its credential under the same rule. A refused target fails before the provider is asked to revoke anything.
  • GET /credentials/{id}/usage previews which outcome a Disconnect will have (willMoveRepositoryCount, willMoveToDisplayName; activeRepositoryCount is the number that would break), and the Disconnect confirm dialog quotes it. The old fields keep their meaning, and an API that predates the fields is read as "nothing moves".
  • New RelinkCredentialBindingsCommand (POST /credentials/{id}/relink-bindings) moves everything a dead credential left, a departed member's retired token included, in one action. It needs repos.manage and credentials.manage, accepts only the caller's own connection or a team token on the same provider instance, and asks the target to read every repository first. A repository the target cannot read (401/403/404, insufficient scope, a refused OAuth grant) refuses the whole move and names it. A provider that does not answer (outage, rate limit) is a retryable refusal that blames no one. One relink verifies at most 100 repositories.
  • GetCredentialDependentsQuery lists the repositories (with projects), group hooks, act-as identities and workflow triggers that lean on a credential. GetRelinkSuggestionsQuery ranks the caller's own live connections first, then team tokens. For a credential that is still connected it offers exactly the targets a Disconnect accepts (the same owner's other connections).
  • The departed-token retirement and the stranded-binding repair run through one shared pass. It reads its backlog in pages behind an id cursor, so candidates that can never succeed no longer hold the whole batch and starve the rest. It is bounded in attempts and candidates examined, and a candidate that fails three passes in a row is set aside for an hour instead of being attempted and warned about every tick.
  • The disconnected-repository error and CredentialDisconnectedException name only the remedies a person can perform today. TeamStanding and IRequireCredentialManagement semantics are unchanged.
  • Non-breaking: no migration, existing credentials, bindings and runs are untouched, new response fields are additive and optional, a body-less revoke still works, and a rolling deploy or rollback leaves the repair sweep as the fallback.

Test plan

  • Integration (real Postgres, real mediator pipeline): connect-then-disconnect, named and implied targets, refused targets, owner-only and live-only implied move, identity unlink, usage preview equals the Disconnect outcome, relink-all with the authorization matrix (including the service-side credentials.manage check), suggestions equal what a Disconnect accepts, dependents filters, probe failure classification, the 100-repository bound, and poison-pill fairness for both sweeps with a real Postgres trigger.
  • Unit: sweep pass (cursor, bounds, back-off, cancellation), back-off ledger (threshold, window, forgetting), suggestion ranking, remedy messages.
  • E2E over HTTP: disconnect with move then a run clones through the new credential, usage preview vs body-less revoke, Admin default moves nothing, live suggestions, owner leaves then relink-all and hooks revive.
  • Frontend (Node 22): lint, tsc --noEmit -p tsconfig.app.json, vitest (273 files), build. The dialog wording tests are red against the old modal.
  • Mutation checks on the new guards and filters, each restored by copy.
  • After deploy: watch credential-binding-repair and departed-credential-retirement warnings; a candidate should log once, then go quiet for an hour.

@ppXD
ppXD force-pushed the fix/keep-older-clients-and-plain-http-deployments-working branch from 2d598a0 to 0f96f74 Compare October 18, 2026 11:21
Connecting a new credential and then disconnecting the old one stranded
every repository bound through the old one: the connect found nothing to
carry forward (the owner's other credential was already live), and the
repair sweep only follows a credential connected after the revocation.
The repository stayed Error on a revoked row for good, and the only way
back was a per-repository Re-link that never touched group hooks.

Disconnect now takes an optional MoveBindingsToCredentialId and moves
the credential's repositories and group hooks in the same unit of work.
Left out, the bindings follow the owner's only other live connection,
but only when the owner is the one disconnecting a credential that is
still live: never for an Admin cutting off a teammate's token, never on a
retry of a disconnect already made, never for a team token or when two
connections compete. Connect-then-disconnect thus ends where
disconnect-then-connect always did, and no one else's grant is chosen for
them. A refused target fails before the provider is asked to revoke
anything. Unlinking a provider identity ends its credential the same way
and follows the same rule. GET /credentials/{id}/usage previews which of
the two outcomes a Disconnect will be, and the confirm dialog quotes it.

RelinkCredentialBindingsCommand moves everything a dead credential left,
a departed member's retired token included, in one action. It needs
repos.manage and credentials.manage, takes only the caller's own
connection or a team token, and asks the target to read every repository
first, so one it cannot read refuses the whole move and names it. Only an
answer about the target counts (401, 403, 404, insufficient scope, a
refused OAuth grant); an outage or rate limit is a retryable refusal that
blames no one, and one relink verifies at most 100 repositories.
GetCredentialDependentsQuery and GetRelinkSuggestionsQuery say what is
waiting and where it could go; for a credential that is still connected
the suggestions are exactly the targets a Disconnect accepts.

The departed-token retirement and the repair of stranded bindings read
their backlog through one shared pass: in pages behind an id cursor, so
candidates that can never succeed no longer hold the whole batch and
starve the rest; bounded in attempts and candidates examined as well as
in results; and a candidate that fails several passes in a row is set
aside for an hour rather than attempted and warned about every tick.

The disconnected-repository error and CredentialDisconnectedException
name only the remedies a person can perform today.
@ppXD
ppXD changed the base branch from fix/keep-older-clients-and-plain-http-deployments-working to main October 18, 2026 11:22
@ppXD
ppXD force-pushed the fix/move-bindings-when-a-credential-is-disconnected branch from 18ae07b to 519bfee Compare October 18, 2026 11:22
@ppXD
ppXD merged commit c0f3b76 into main Oct 18, 2026
8 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.

1 participant