Skip to content

fix(preview): create one preview per pull request when webhook deliveries race - #5460

Open
mikaoelitiana wants to merge 1 commit into
Dokploy:canaryfrom
mikaoelitiana:fix/preview-deployment-duplicate-race
Open

mikaoelitiana wants to merge 1 commit into
Dokploy:canaryfrom
mikaoelitiana:fix/preview-deployment-duplicate-race

Conversation

@mikaoelitiana

@mikaoelitiana mikaoelitiana commented Sep 14, 2026

Copy link
Copy Markdown

What is this PR about?

One pull request could end up with several preview deployments (#5086). GitHub sends one pull_request delivery per label on top of opened, all within the same second, and pages/api/deploy/github.ts guards creation with a non-atomic findPreviewDeploymentByApplicationIdcreatePreviewDeployment. The GitHub comment round-trip inside createPreviewDeployment makes the check→insert window several seconds wide, so every delivery sees no existing preview and creates its own container, domain and PR comment.

Two changes, both in the webhook handler:

  • Serialize the find-or-create per (applicationId, pullRequestId) with a small in-process promise chain (withPreviewLock). Concurrent deliveries for the same PR now wait for the first one to insert its row and then find it, so exactly one preview is created.
  • A labeled delivery that finds an existing preview no longer enqueues a build. A label event never moves the PR head, so there is nothing to rebuild. This also collapses the N redundant same-SHA builds the extra deliveries used to queue. labeled on a PR without a preview still creates one (the label-filter opt-in flow from feat: add support for 'labeled' action in GitHub deployment handler #3960 is unchanged), and synchronize/reopened still redeploy.

Why not a unique constraint on (applicationId, pullRequestId)? Instances hit by this bug already hold duplicate rows for open PRs (ours has three for one PR), so a migration adding the constraint would fail on upgrade, and deleting the extra rows in the migration would orphan their containers and domains. The lock fixes the self-hosted single-process case without touching the schema. It is process-local, so a horizontally scaled deployment of the webhook handler would still need the constraint once existing rows can be reconciled.

Testing

  • apps/dokploy/__test__/deploy/github-webhook-handler.test.ts gains three cases: opened + two labeled deliveries racing create one preview and one build; labeled on an existing preview enqueues nothing; labeled on a PR without a preview still creates one. The first two fail on unpatched canary (3 creates instead of 1; 1 queued build instead of 0) and pass with this change.
  • biome check clean on both files; tsc --noEmit for apps/dokploy reports no errors.
  • Reproduced the bug today on self-hosted v0.30.6 (three previews for one PR opened with two labels, rows 09:45:18 / :22 / :25 UTC), which is what the race test models. I have not run this branch against a live Dokploy instance with real GitHub deliveries; happy to build a patched image and do that if you want it before merging.

Checklist

Issues related (if applicable)

closes #5086

🤖 Generated with Claude Code

RetriggerConfidence Score: 5/5

The PR appears safe to merge, with the intended single-process race addressed and no actionable regressions identified.

Summary

  • Adds an in-process promise-chain lock around preview find/create and build dispatch.
  • Skips redundant builds when a labeled delivery finds an existing preview.
  • Adds tests covering racing deliveries, existing previews on label events, and label-based preview opt-in.

Reviews (1) · Last reviewed commit: "fix(preview): create one preview per pul..."

…ries race

GitHub sends one `pull_request` delivery per label on top of `opened`, all
within the same second. The handler guarded creation with a non-atomic
`findPreviewDeploymentByApplicationId` → `createPreviewDeployment`, and the
GitHub comment round-trip inside `createPreviewDeployment` widened the window
to several seconds, so every delivery saw no existing preview and created its
own — one container, domain and PR comment each.

Serialize the find-or-create per (application, pull request) so concurrent
deliveries wait for the first to insert and then see its row. A `labeled`
delivery that finds an existing preview now returns without enqueueing a
build: a label never moves the PR head, so there is nothing to rebuild, and
this collapses the N redundant same-SHA builds the extra deliveries used to
queue.

Closes Dokploy#5086

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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.

Multiple labels on one PR create duplicate preview deployments (race in labeled webhook handling)

1 participant