Skip to content

Make start-reviewing repos idempotent - #445

Merged
TheGreatAxios merged 4 commits into
mainfrom
cl-7134-start-reviewing-repos-is-not-idempotent-a-mid-loop-failure
Aug 29, 2026
Merged

TheGreatAxios merged 4 commits into
mainfrom
cl-7134-start-reviewing-repos-is-not-idempotent-a-mid-loop-failure

Conversation

@TheGreatAxios

@TheGreatAxios TheGreatAxios commented Aug 28, 2026 •

Copy link
Copy Markdown
Contributor

Fixes CL-7134 — https://linear.app/abklabs/issue/CL-7134

Problem

startReviewingRepos (packages/workflow-catalog/src/connect-github-setup.ts) loops selected repos calling ports.mintRepoGrant(repo) then ports.createWebhookTrigger(repo) unconditionally, and persists selectedRepos once after the loop. A failure on repo N leaves repos 1..N-1 with a live grant and trigger that nothing recorded. The route's generic "Try again in a moment." retries the same selection, and neither WebhookTriggerStore.create (plain INSERT, no dedupe key) nor the grant table (no unique constraint) rejects the duplicate — the retry mints a second grant and trigger per repo.

Change

  • startReviewingRepos is idempotent per repo on both steps: hasRepoGrant(repo) gates mintRepoGrant, hasWebhookTrigger(repo) gates createWebhookTrigger, checked independently so a failure between the two steps re-mints nothing on retry.
  • webhookTriggerName(repo) is the one helper both trigger creation and the lookup use, so the identity string cannot drift.
  • Both ports are threaded through ConnectGithubRoutesDeps and bound in apps/hub/src/index.ts: the grant check reads the grant store scoped to (tenant, resource "repo:<name>", action "read") — exactly what mintRepoGrant inserts — and the trigger check reads WebhookTriggerStore.list(tenantId) filtered on the code-review definition id and webhookTriggerName(repo).
  • selectedRepos stays persisted once at the end: idempotency comes from the per-repo checks, not from what settings recorded, so a retry is safe whether or not the failed attempt reached persistSelectedRepos.
  • Behavior note: nothing disables or deletes a trigger on GitHub disconnect, so re-adding a repo after a reconnect reuses its existing trigger and grant rather than minting new ones.

Tests

connect-github-setup.test.ts: (1) fail mintRepoGrant on repo 2 of 3, retry → exactly one grant and one trigger per repo; (2) fail createWebhookTrigger after the grant succeeded on repo 2, retry → the grant is not re-minted; (3) webhookTriggerName round-trips between create and lookup. Fakes are state-backed so the retry sees what the first attempt wrote. Confirmed red without the checks, green with them.

startReviewingRepos loops repos minting a grant and a webhook trigger
per repo, unconditionally. A failure partway through the loop leaves
earlier repos live but unrecorded, and a "try again" retry mints
duplicates for them. Adds a fake-ports test that fails a repo mid-loop,
retries the same selection, and expects each repo to end up with
exactly one grant and one trigger.

Fixes CL-7134.
Adds a hasWebhookTrigger port, checked per repo before minting
anything for it, backed by a read against the existing
WebhookTriggerStore.list. A retry after a mid-loop failure now only
mints for the repos the failed attempt never reached, instead of
minting a second grant and trigger for repos already set up.

selectedRepos is still persisted once at the end rather than
incrementally: idempotency now comes from the per-repo trigger check,
not from what's recorded in settings, so a retry is safe regardless of
whether the prior attempt's persistSelectedRepos call ran.

Fixes CL-7134.
The prior fix skipped a repo whose webhook trigger already exists, but
grant minting and trigger creation are two separate steps, and the
grant table carries no unique constraint. A failure between minting
the grant and creating the trigger still re-mints a second grant for
that repo on retry, since only the trigger's existence was checked.

Adds a fake-ports test that fails createWebhookTrigger (after
mintRepoGrant already succeeded) on repo 2 of 3, retries, and asserts
the grant is minted exactly once per repo. Also adds a unit test for a
webhookTriggerName helper, so the trigger-name string used to create a
trigger and the one used to look it up can't independently drift.

Fixes CL-7134.
Gates mintRepoGrant on a new hasRepoGrant port, checked independently
of hasWebhookTrigger, so a retry after a failure between the two steps
mints only whichever one the failed attempt didn't finish rather than
re-minting a grant that already exists. apps/hub's hasRepoGrant binds
to a read against the same grant table mintRepoGrant inserts into.

Also pulls the "<repo> pull-request-opened" trigger-name convention
into one exported webhookTriggerName helper, used by both the create
and the lookup in apps/hub, so they read the same string by
construction instead of by two hand-kept copies of it.

Documents, rather than changes, that a trigger is never disabled or
deleted on GitHub disconnect: re-adding a repo after a reconnect finds
its old trigger still live and is skipped, not re-created.

Fixes CL-7134.
@TheGreatAxios
TheGreatAxios force-pushed the cl-7134-start-reviewing-repos-is-not-idempotent-a-mid-loop-failure branch from e497225 to 2f3169e Compare August 29, 2026 04:46
@TheGreatAxios
TheGreatAxios merged commit ba02121 into main Aug 29, 2026
5 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