Multiple labels on one PR create duplicate preview deployments (race in labeled webhook handling)
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 37.4k
- Forks
- 3k
- Avg merge
- 1d 3h
- Merged PRs (30d)
- 73
Description
To Reproduce
- Create an application with preview deployments enabled and a preview label filter configured (default
preview). - Open a PR carrying several labels at once, e.g.
gh pr create --label bug --label frontend --label preview(or add 3 labels in one action in the GitHub UI). - Watch the PR conversation.
Current vs. Expected behavior
Current: one preview deployment is created per webhook delivery. GitHub sends one pull_request delivery per label (labeled × 3) plus opened, so the PR above gets 4 separate "Dokploy Preview Deployment" comments and 4 preview deployments for the same PR/commit. Observed on a PR where 3 labels were attached in the same second: 4 bot comments, ~8–9s apart.
Expected: one preview deployment per PR, regardless of how many labels arrive or how many deliveries GitHub sends.
Root cause (from reading canary @ d542967a)
apps/dokploy/pages/api/deploy/github.ts includes labeled in shouldCreateDeployment (L364–375) and guards creation only with a non-atomic read-then-write:
const previewDeploymentResult =
await findPreviewDeploymentByApplicationId(app.applicationId, prId);
...
if (!previewDeploymentResult && shouldCreateDeployment) {
const previewDeployment = await createPreviewDeployment({ ... });
Three things make the race window seconds wide, so concurrent deliveries all pass the !previewDeploymentResult check:
- No unique constraint on
(applicationId, pullRequestId)inpackages/server/src/db/schema/preview-deployments.ts— the only unique column isappName, which gets a random suffix and never collides. Nothing at the DB layer rejects the duplicate row. createPreviewDeploymentposts the GitHub comment before inserting the row (packages/server/src/services/preview-deployment.tsL163–178) — the check→insert window spans a network round-trip.- Each delivery also does a
checkUserRepositoryPermissionsGitHub round-trip before the dedup lookup (github.ts L417–424), staggering the deliveries further.
This looks like a reintroduction of #3469: #3642 fixed it by excluding labeled from deployment creation, #3956 showed that broke label-filter workflows, and #3960 re-added labeled — leaving the racy findFirst as the only duplicate guard.
Suggested fix
- Add a composite unique constraint on
previewDeployments (applicationId, pullRequestId)and havecreatePreviewDeploymentinsert first (onConflictDoNothing), posting the PR comment only when the insert wins. A losing concurrent delivery then never comments. - Note even with the constraint, deliveries that find the existing row still each enqueue a redeploy (github.ts L509–535) — one
labeleddelivery per label means N queued builds for the same head SHA; a per-(prId, head.sha)short-circuit would collapse those too.
Provide environment information
- Dokploy version: v0.29.14 (self-hosted, Docker, Ubuntu VPS)
- Integration: GitHub App webhooks
- Affected since v0.28.6 (first release containing #3960); code present on current
canary(d542967a, 2026-08-14).
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with apps/dokploy/pages/api/deploy/github.ts, especially shouldCreateDeployment, the permission check, and the redeploy enqueue path; then read packages/server/src/db/schema/preview-deployments.ts and packages/server/src/services/preview-deployment.ts. Reproduce concurrent labeled deliveries and verify that one PR/commit produces one preview deployment and comment, without duplicate queued builds.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- github, typescript
- Domain
- backend, databases, devops
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100