Skip to content
This repository was archived by the owner on Dec 10, 2025. It is now read-only.

Commit 617b4a7

Browse files
committed
fix(copier): only process PRs merged to branches that match the source.branch in workflow
1 parent bb047bf commit 617b4a7

File tree

2 files changed

+200
-12
lines changed

2 files changed

+200
-12
lines changed

examples-copier/services/webhook_handler_new.go

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -166,11 +166,15 @@ func HandleWebhookWithContainer(w http.ResponseWriter, r *http.Request, config *
166166
repoOwner := repo.GetOwner().GetLogin()
167167
repoName := repo.GetName()
168168

169+
// Extract the base branch (the branch the PR was merged into)
170+
baseBranch := prEvt.GetPullRequest().GetBase().GetRef()
171+
169172
LogInfoCtx(ctx, "processing merged PR", map[string]interface{}{
170-
"pr_number": prNumber,
171-
"sha": sourceCommitSHA,
172-
"repo": fmt.Sprintf("%s/%s", repoOwner, repoName),
173-
"elapsed_ms": time.Since(startTime).Milliseconds(),
173+
"pr_number": prNumber,
174+
"sha": sourceCommitSHA,
175+
"repo": fmt.Sprintf("%s/%s", repoOwner, repoName),
176+
"base_branch": baseBranch,
177+
"elapsed_ms": time.Since(startTime).Milliseconds(),
174178
})
175179

176180
// Respond immediately to avoid GitHub webhook timeout
@@ -197,11 +201,11 @@ func HandleWebhookWithContainer(w http.ResponseWriter, r *http.Request, config *
197201
// Process asynchronously in background with a new context
198202
// Don't use the request context as it will be cancelled when the request completes
199203
bgCtx := context.Background()
200-
go handleMergedPRWithContainer(bgCtx, prNumber, sourceCommitSHA, repoOwner, repoName, config, container)
204+
go handleMergedPRWithContainer(bgCtx, prNumber, sourceCommitSHA, repoOwner, repoName, baseBranch, config, container)
201205
}
202206

203207
// handleMergedPRWithContainer processes a merged PR using the new pattern matching system
204-
func handleMergedPRWithContainer(ctx context.Context, prNumber int, sourceCommitSHA string, repoOwner string, repoName string, config *configs.Config, container *ServiceContainer) {
208+
func handleMergedPRWithContainer(ctx context.Context, prNumber int, sourceCommitSHA string, repoOwner string, repoName string, baseBranch string, config *configs.Config, container *ServiceContainer) {
205209
startTime := time.Now()
206210

207211
// Configure GitHub permissions
@@ -227,18 +231,20 @@ func handleMergedPRWithContainer(ctx context.Context, prNumber int, sourceCommit
227231
return
228232
}
229233

230-
// Find workflows matching this source repo
234+
// Find workflows matching this source repo and branch
231235
webhookRepo := fmt.Sprintf("%s/%s", repoOwner, repoName)
232-
matchingWorkflows := []types.Workflow{}
236+
var matchingWorkflows []types.Workflow
233237
for _, workflow := range yamlConfig.Workflows {
234-
if workflow.Source.Repo == webhookRepo {
238+
// Match both repository and branch
239+
if workflow.Source.Repo == webhookRepo && workflow.Source.Branch == baseBranch {
235240
matchingWorkflows = append(matchingWorkflows, workflow)
236241
}
237242
}
238243

239244
if len(matchingWorkflows) == 0 {
240-
LogWarningCtx(ctx, "no workflows configured for source repository", map[string]interface{}{
245+
LogWarningCtx(ctx, "no workflows configured for source repository and branch", map[string]interface{}{
241246
"webhook_repo": webhookRepo,
247+
"base_branch": baseBranch,
242248
"workflow_count": len(yamlConfig.Workflows),
243249
})
244250
container.MetricsCollector.RecordWebhookFailed()
@@ -247,6 +253,7 @@ func handleMergedPRWithContainer(ctx context.Context, prNumber int, sourceCommit
247253

248254
LogInfoCtx(ctx, "found matching workflows", map[string]interface{}{
249255
"webhook_repo": webhookRepo,
256+
"base_branch": baseBranch,
250257
"matching_count": len(matchingWorkflows),
251258
})
252259

@@ -361,5 +368,3 @@ func processFilesWithWorkflows(ctx context.Context, prNumber int, sourceCommitSH
361368
"workflow_count": len(yamlConfig.Workflows),
362369
})
363370
}
364-
365-

examples-copier/services/webhook_handler_new_test.go

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,9 @@ func TestHandleWebhookWithContainer_MergedPR(t *testing.T) {
301301
Number: github.Int(123),
302302
Merged: github.Bool(true),
303303
MergeCommitSHA: github.String("abc123"),
304+
Base: &github.PullRequestBranch{
305+
Ref: github.String("main"),
306+
},
304307
},
305308
Repo: &github.Repository{
306309
Name: github.String("test-repo"),
@@ -334,6 +337,186 @@ func TestHandleWebhookWithContainer_MergedPR(t *testing.T) {
334337
// when trying to access GitHub APIs. This is expected and doesn't affect the test result.
335338
}
336339

340+
func TestHandleWebhookWithContainer_MergedPRToDevelopmentBranch(t *testing.T) {
341+
// This test verifies that PRs merged to non-main branches (like development)
342+
// are accepted by the webhook handler but won't match any workflows
343+
// (assuming workflows are configured for main branch only)
344+
345+
// Set up environment variables
346+
os.Setenv(configs.AppId, "123456")
347+
os.Setenv(configs.InstallationId, "789012")
348+
os.Setenv(configs.ConfigRepoOwner, "test-owner")
349+
os.Setenv(configs.ConfigRepoName, "test-repo")
350+
os.Setenv("SKIP_SECRET_MANAGER", "true")
351+
352+
// Generate a valid RSA private key for testing
353+
key, _ := rsa.GenerateKey(rand.Reader, 1024)
354+
der := x509.MarshalPKCS1PrivateKey(key)
355+
pemBytes := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: der})
356+
os.Setenv("GITHUB_APP_PRIVATE_KEY", string(pemBytes))
357+
os.Setenv("GITHUB_APP_PRIVATE_KEY_B64", base64.StdEncoding.EncodeToString(pemBytes))
358+
359+
InstallationAccessToken = "test-token"
360+
361+
config := &configs.Config{
362+
ConfigRepoOwner: "test-owner",
363+
ConfigRepoName: "test-repo",
364+
ConfigFile: "nonexistent-config.yaml",
365+
AuditEnabled: false,
366+
}
367+
368+
container, err := NewServiceContainer(config)
369+
if err != nil {
370+
t.Fatalf("NewServiceContainer() error = %v", err)
371+
}
372+
373+
// Create a merged PR event to development branch
374+
prEvent := &github.PullRequestEvent{
375+
Action: github.String("closed"),
376+
PullRequest: &github.PullRequest{
377+
Number: github.Int(456),
378+
Merged: github.Bool(true),
379+
MergeCommitSHA: github.String("def456"),
380+
Base: &github.PullRequestBranch{
381+
Ref: github.String("development"),
382+
},
383+
},
384+
Repo: &github.Repository{
385+
Name: github.String("test-repo"),
386+
Owner: &github.User{
387+
Login: github.String("test-owner"),
388+
},
389+
},
390+
}
391+
payload, _ := json.Marshal(prEvent)
392+
393+
req := httptest.NewRequest("POST", "/webhook", bytes.NewReader(payload))
394+
req.Header.Set("X-GitHub-Event", "pull_request")
395+
396+
w := httptest.NewRecorder()
397+
398+
HandleWebhookWithContainer(w, req, config, container)
399+
400+
// Should still return 202 Accepted (webhook accepts the event)
401+
if w.Code != http.StatusAccepted {
402+
t.Errorf("Status code = %d, want %d", w.Code, http.StatusAccepted)
403+
}
404+
405+
// Check response body
406+
var response map[string]string
407+
json.Unmarshal(w.Body.Bytes(), &response)
408+
if response["status"] != "accepted" {
409+
t.Errorf("Response status = %v, want accepted", response["status"])
410+
}
411+
412+
// Note: The background goroutine will fail to find matching workflows
413+
// because the workflow config specifies main branch, not development.
414+
// This is the expected behavior - the webhook accepts the event but
415+
// no workflows will be processed.
416+
}
417+
418+
func TestHandleWebhookWithContainer_MergedPRWithDifferentBranches(t *testing.T) {
419+
// This test verifies that the base branch is correctly extracted
420+
// from different PR events
421+
422+
testCases := []struct {
423+
name string
424+
baseBranch string
425+
prNumber int
426+
}{
427+
{
428+
name: "main branch",
429+
baseBranch: "main",
430+
prNumber: 100,
431+
},
432+
{
433+
name: "development branch",
434+
baseBranch: "development",
435+
prNumber: 101,
436+
},
437+
{
438+
name: "feature branch",
439+
baseBranch: "feature/new-feature",
440+
prNumber: 102,
441+
},
442+
{
443+
name: "release branch",
444+
baseBranch: "release/v1.0",
445+
prNumber: 103,
446+
},
447+
}
448+
449+
// Set up environment variables
450+
os.Setenv(configs.AppId, "123456")
451+
os.Setenv(configs.InstallationId, "789012")
452+
os.Setenv(configs.ConfigRepoOwner, "test-owner")
453+
os.Setenv(configs.ConfigRepoName, "test-repo")
454+
os.Setenv("SKIP_SECRET_MANAGER", "true")
455+
456+
key, _ := rsa.GenerateKey(rand.Reader, 1024)
457+
der := x509.MarshalPKCS1PrivateKey(key)
458+
pemBytes := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: der})
459+
os.Setenv("GITHUB_APP_PRIVATE_KEY", string(pemBytes))
460+
os.Setenv("GITHUB_APP_PRIVATE_KEY_B64", base64.StdEncoding.EncodeToString(pemBytes))
461+
462+
InstallationAccessToken = "test-token"
463+
464+
for _, tc := range testCases {
465+
t.Run(tc.name, func(t *testing.T) {
466+
config := &configs.Config{
467+
ConfigRepoOwner: "test-owner",
468+
ConfigRepoName: "test-repo",
469+
ConfigFile: "nonexistent-config.yaml",
470+
AuditEnabled: false,
471+
}
472+
473+
container, err := NewServiceContainer(config)
474+
if err != nil {
475+
t.Fatalf("NewServiceContainer() error = %v", err)
476+
}
477+
478+
// Create a merged PR event with specific base branch
479+
prEvent := &github.PullRequestEvent{
480+
Action: github.String("closed"),
481+
PullRequest: &github.PullRequest{
482+
Number: github.Int(tc.prNumber),
483+
Merged: github.Bool(true),
484+
MergeCommitSHA: github.String("abc123"),
485+
Base: &github.PullRequestBranch{
486+
Ref: github.String(tc.baseBranch),
487+
},
488+
},
489+
Repo: &github.Repository{
490+
Name: github.String("test-repo"),
491+
Owner: &github.User{
492+
Login: github.String("test-owner"),
493+
},
494+
},
495+
}
496+
payload, _ := json.Marshal(prEvent)
497+
498+
req := httptest.NewRequest("POST", "/webhook", bytes.NewReader(payload))
499+
req.Header.Set("X-GitHub-Event", "pull_request")
500+
501+
w := httptest.NewRecorder()
502+
503+
HandleWebhookWithContainer(w, req, config, container)
504+
505+
// Should return 202 Accepted for all merged PRs
506+
if w.Code != http.StatusAccepted {
507+
t.Errorf("Status code = %d, want %d", w.Code, http.StatusAccepted)
508+
}
509+
510+
// Check response body
511+
var response map[string]string
512+
json.Unmarshal(w.Body.Bytes(), &response)
513+
if response["status"] != "accepted" {
514+
t.Errorf("Response status = %v, want accepted", response["status"])
515+
}
516+
})
517+
}
518+
}
519+
337520
func TestRetrieveFileContentsWithConfigAndBranch(t *testing.T) {
338521
// This test would require mocking the GitHub client
339522
// For now, we document the expected behavior

0 commit comments

Comments
 (0)