Skip to content

Commit 9523364

Browse files
committed
feat: deduplicate GitHub webhook deliveries
Adds a check before creating a deployment to skip if one already exists for the same commit SHA + app + environment. This prevents duplicate deployments when GitHub retries webhook deliveries. Also extracts X-GitHub-Delivery header and logs it throughout the webhook handler for observability.
1 parent 6040a5b commit 9523364

File tree

5 files changed

+87
-2
lines changed

5 files changed

+87
-2
lines changed

pkg/db/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ go_library(
117117
"custom_types.go",
118118
"database.go",
119119
"deployment_delete_instances.sql_generated.go",
120+
"deployment_exists_by_sha_app_env.sql_generated.go",
120121
"deployment_find_by_id.sql_generated.go",
121122
"deployment_find_by_k8s_name.sql_generated.go",
122123
"deployment_find_latest_ready_by_app_and_env.sql_generated.go",

pkg/db/deployment_exists_by_sha_app_env.sql_generated.go

Lines changed: 41 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/db/querier_generated.go

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
-- name: DeploymentExistsByCommitShaAppAndEnv :one
2+
SELECT EXISTS(
3+
SELECT 1 FROM deployments
4+
WHERE git_commit_sha = sqlc.arg(git_commit_sha)
5+
AND app_id = sqlc.arg(app_id)
6+
AND environment_id = sqlc.arg(environment_id)
7+
) AS `exists`;

svc/ctrl/api/github_webhook.go

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,11 @@ func (s *GitHubWebhook) ServeHTTP(w http.ResponseWriter, r *http.Request) {
7575

7676
logger.Info("GitHub webhook signature verified", "event", event)
7777

78+
deliveryID := r.Header.Get("X-GitHub-Delivery")
79+
7880
switch event {
7981
case "push":
80-
s.handlePush(r.Context(), w, body)
82+
s.handlePush(r.Context(), w, body, deliveryID)
8183
case "installation":
8284
logger.Info("Installation event received")
8385
w.WriteHeader(http.StatusOK)
@@ -91,7 +93,7 @@ func (s *GitHubWebhook) ServeHTTP(w http.ResponseWriter, r *http.Request) {
9193
// handlePush processes push events by creating a deployment record and
9294
// starting the deploy workflow. Maps branches to environments: the project's
9395
// default branch deploys to production, all others to preview.
94-
func (s *GitHubWebhook) handlePush(ctx context.Context, w http.ResponseWriter, body []byte) {
96+
func (s *GitHubWebhook) handlePush(ctx context.Context, w http.ResponseWriter, body []byte, deliveryID string) {
9597
var payload pushPayload
9698
if err := json.Unmarshal(body, &payload); err != nil {
9799
logger.Error("failed to parse push payload", "error", err)
@@ -220,6 +222,29 @@ func (s *GitHubWebhook) handlePush(ctx context.Context, w http.ResponseWriter, b
220222
}
221223
}
222224

225+
// Deduplicate: skip if a deployment already exists for this commit + app + env
226+
if payload.After != "" {
227+
exists, existsErr := db.Query.DeploymentExistsByCommitShaAppAndEnv(ctx, s.db.RO(), db.DeploymentExistsByCommitShaAppAndEnvParams{
228+
GitCommitSha: sql.NullString{String: payload.After, Valid: true},
229+
AppID: app.ID,
230+
EnvironmentID: env.ID,
231+
})
232+
if existsErr != nil {
233+
logger.Error("failed to check for existing deployment", "error", existsErr, "delivery_id", deliveryID)
234+
http.Error(w, "failed to check for existing deployment", http.StatusInternalServerError)
235+
return
236+
}
237+
if exists {
238+
logger.Info("skipping duplicate deployment",
239+
"delivery_id", deliveryID,
240+
"commit_sha", payload.After,
241+
"app_id", app.ID,
242+
"environment_id", env.ID,
243+
)
244+
continue
245+
}
246+
}
247+
223248
// Create deployment record
224249
deploymentID := uid.New(uid.DeploymentPrefix)
225250
now := time.Now().UnixMilli()
@@ -259,6 +284,7 @@ func (s *GitHubWebhook) handlePush(ctx context.Context, w http.ResponseWriter, b
259284

260285
logger.Info("Created deployment record",
261286
"deployment_id", deploymentID,
287+
"delivery_id", deliveryID,
262288
"project_id", project.ID,
263289
"repository", payload.Repository.FullName,
264290
"commit_sha", payload.After,
@@ -289,6 +315,7 @@ func (s *GitHubWebhook) handlePush(ctx context.Context, w http.ResponseWriter, b
289315
logger.Info("Deployment workflow started",
290316
"invocation_id", invocation.Id,
291317
"deployment_id", deploymentID,
318+
"delivery_id", deliveryID,
292319
"project_id", project.ID,
293320
"repository", payload.Repository.FullName,
294321
"commit_sha", payload.After,

0 commit comments

Comments
 (0)