Skip to content

Commit 884b7ab

Browse files
committed
fix(github-webhook): /ok-to-test is not triggering CI on PRs
When an unauthorized user opens a pull request on a repository configured with Pipelines-as-Code using GitHub webhook integration, commenting /ok-to-test as an admin does not trigger the CI pipeline. This happens because the GitHub client (ghClient) is never initialized for webhook-based issue comment events — the client setup only ran for GitHub App events during payload parsing. Root cause: - In the issue_comment handler, the code checked if ghClient was nil and returned an error, but for webhook integrations the client is legitimately nil at that point since webhooks authenticate differently from GitHub Apps. - The PR number was being extracted by parsing the HTML URL string instead of reading it directly from the event object. - The webhook request payload and headers were not being preserved on the event object, which is needed for webhook signature validation. Changes: - pkg/provider/github/parse_payload.go: - Add initGitHubWebhookClient() to initialize the provider client for webhook-based events using gitclient.SetupAuthenticatedClient - Preserve request headers and payload on the event object early in ParsePayload so they are available for webhook signature validation - Reorder handleIssueCommentEvent to match the repository first, then lazily initialize the GitHub client if nil (webhook case), before fetching the pull request details - Use event.GetIssue().GetNumber() directly instead of parsing the PR number from the HTML URL string - Remove the early ghClient nil check that blocked webhook events - pkg/provider/github/github.go: - Move GitHub App token scoping logic from gitclient into SetClient, keeping provider-specific concerns within the provider package - pkg/gitclient/client_setup.go: - Remove GitHub App token scoping (moved to provider) - Add global repository lookup when globalRepo is nil, so webhook-based flows can resolve credentials from the global repository configuration - Replace github provider import with metav1 for the Get call - pkg/provider/github/parse_payload_test.go: - Remove test cases that asserted ghClient nil was an error (no longer applicable) - Remove test for invalid PR URL parsing (PR number now read from event) - Add Number field to IssueCommentEvent test fixtures - pkg/provider/github/acl_test.go: - Add html_url and number to issue comment test payload to match new handleIssueCommentEvent flow that sets URL and PR number from the event object - pkg/provider/github/github_test.go: - Add Logger, pacInfo, and repo with Settings to SetClient test to support token scoping moved into SetClient - pkg/gitclient/client_setup_test.go: - Add GlobalRepository and Namespace to test seed data to match new global repo lookup - pkg/pipelineascode/pipelineascode_test.go: - Add GlobalRepository and Kube namespace to Run.Info to match new global repo lookup in SetupAuthenticatedClient - pkg/reconciler/reconciler_test.go: - Add Logger to Provider in reconciler test to support token scoping logging in SetClient Signed-off-by: Zaki Shaikh <zashaikh@redhat.com>
1 parent 630faf5 commit 884b7ab

File tree

12 files changed

+301
-146
lines changed

12 files changed

+301
-146
lines changed

pkg/adapter/sinker.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import (
77
"net/http"
88

99
"github.com/openshift-pipelines/pipelines-as-code/pkg/apis/pipelinesascode/v1alpha1"
10-
"github.com/openshift-pipelines/pipelines-as-code/pkg/kubeinteraction"
1110
"github.com/openshift-pipelines/pipelines-as-code/pkg/gitclient"
11+
"github.com/openshift-pipelines/pipelines-as-code/pkg/kubeinteraction"
1212
"github.com/openshift-pipelines/pipelines-as-code/pkg/matcher"
1313
"github.com/openshift-pipelines/pipelines-as-code/pkg/params"
1414
"github.com/openshift-pipelines/pipelines-as-code/pkg/params/info"

pkg/adapter/sinker_test.go

Lines changed: 3 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -105,18 +105,6 @@ func TestSetupClientGitHubAppVsOther(t *testing.T) {
105105
wantErr: false,
106106
wantRepositoryIDsCount: 0, // No extra repos
107107
},
108-
{
109-
name: "GitHub App with extra repos - IDs should be populated",
110-
installationID: 12345,
111-
hasGitProvider: false,
112-
extraReposConfigured: true,
113-
extraRepoInstallIDs: map[string]int64{
114-
"another/one": 789,
115-
"andanother/two": 10112,
116-
},
117-
wantErr: false,
118-
wantRepositoryIDsCount: 2, // Should have 2 extra repo IDs
119-
},
120108
{
121109
name: "Non-GitHub App requires git_provider",
122110
installationID: 0,
@@ -162,37 +150,11 @@ func TestSetupClientGitHubAppVsOther(t *testing.T) {
162150
}
163151
}
164152

165-
// Setup extra repos if configured
166-
extraRepos := []*v1alpha1.Repository{}
167-
if tt.extraReposConfigured {
168-
repo.Spec.Settings = &v1alpha1.Settings{
169-
GithubAppTokenScopeRepos: []string{},
170-
}
171-
for repoName := range tt.extraRepoInstallIDs {
172-
repo.Spec.Settings.GithubAppTokenScopeRepos = append(
173-
repo.Spec.Settings.GithubAppTokenScopeRepos,
174-
repoName,
175-
)
176-
// Create matching repository CRs for extra repos
177-
extraRepo := testnewrepo.NewRepo(testnewrepo.RepoTestcreationOpts{
178-
Name: repoName,
179-
URL: "https://github.com/" + repoName,
180-
InstallNamespace: "default",
181-
})
182-
extraRepos = append(extraRepos, extraRepo)
183-
}
184-
}
185-
186-
// Create test data with all repositories
187-
allRepos := append([]*v1alpha1.Repository{repo}, extraRepos...)
188-
run := setupTestData(t, allRepos)
153+
run := setupTestData(t, []*v1alpha1.Repository{repo})
189154

190155
// Create a tracking provider to verify behavior
191156
trackingProvider := &trackingProviderImpl{
192-
TestProviderImp: testprovider.TestProviderImp{AllowIT: true},
193-
createTokenCalled: false,
194-
repositoryIDs: []int64{},
195-
extraRepoInstallIDs: tt.extraRepoInstallIDs,
157+
TestProviderImp: testprovider.TestProviderImp{AllowIT: true},
196158
}
197159
trackingProvider.SetLogger(log)
198160

@@ -229,52 +191,16 @@ func TestSetupClientGitHubAppVsOther(t *testing.T) {
229191
} else {
230192
assert.NilError(t, err, "unexpected error: %v", err)
231193
}
232-
233-
// For GitHub Apps with extra repos, verify CreateToken was called
234-
// and repository IDs were populated
235-
if tt.extraReposConfigured && !tt.wantErr {
236-
assert.Assert(t, trackingProvider.createTokenCalled,
237-
"CreateToken should have been called for extra repos")
238-
239-
// Verify all expected repo IDs are present
240-
for repoName, expectedID := range tt.extraRepoInstallIDs {
241-
found := false
242-
for _, id := range trackingProvider.repositoryIDs {
243-
if id == expectedID {
244-
found = true
245-
break
246-
}
247-
}
248-
assert.Assert(t, found,
249-
"Repository ID %d for %s not found in provider.RepositoryIDs: %v",
250-
expectedID, repoName, trackingProvider.repositoryIDs)
251-
}
252-
253-
assert.Equal(t, len(trackingProvider.repositoryIDs), tt.wantRepositoryIDsCount,
254-
"Expected %d repository IDs, got %d: %v",
255-
tt.wantRepositoryIDsCount, len(trackingProvider.repositoryIDs),
256-
trackingProvider.repositoryIDs)
257-
}
258194
})
259195
}
260196
}
261197

262198
// trackingProviderImpl wraps TestProviderImp to track CreateToken calls and repository IDs.
263199
type trackingProviderImpl struct {
264200
testprovider.TestProviderImp
265-
createTokenCalled bool
266-
repositoryIDs []int64
267-
extraRepoInstallIDs map[string]int64
268201
}
269202

270-
func (t *trackingProviderImpl) CreateToken(_ context.Context, repositories []string, _ *info.Event) (string, error) {
271-
t.createTokenCalled = true
272-
// Simulate adding repository IDs like the real CreateToken does
273-
for _, repo := range repositories {
274-
if id, ok := t.extraRepoInstallIDs[repo]; ok {
275-
t.repositoryIDs = append(t.repositoryIDs, id)
276-
}
277-
}
203+
func (t *trackingProviderImpl) CreateToken(_ context.Context, _ []string, _ *info.Event) (string, error) {
278204
return "fake-token", nil
279205
}
280206

pkg/gitclient/client_setup.go

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ import (
1212
"github.com/openshift-pipelines/pipelines-as-code/pkg/params/info"
1313
"github.com/openshift-pipelines/pipelines-as-code/pkg/provider"
1414
"github.com/openshift-pipelines/pipelines-as-code/pkg/secrets"
15-
"github.com/openshift-pipelines/pipelines-as-code/pkg/provider/github"
1615
"go.uber.org/zap"
16+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1717
)
1818

1919
// SetupAuthenticatedClient sets up the authenticated VCS client with proper token scoping.
@@ -31,6 +31,19 @@ func SetupAuthenticatedClient(
3131
pacInfo *info.PacOpts,
3232
logger *zap.SugaredLogger,
3333
) error {
34+
if globalRepo == nil &&
35+
run != nil &&
36+
run.Info.Controller != nil &&
37+
run.Info.Kube != nil &&
38+
run.Info.Kube.Namespace != "" &&
39+
run.Info.Controller.GlobalRepository != "" {
40+
var err error
41+
if globalRepo, err = run.Clients.PipelineAsCode.PipelinesascodeV1alpha1().Repositories(run.Info.Kube.Namespace).Get(
42+
ctx, run.Info.Controller.GlobalRepository, metav1.GetOptions{},
43+
); err != nil {
44+
logger.Errorf("cannot get global repository: %v", err)
45+
}
46+
}
3447
// Determine secret namespace BEFORE merging repos
3548
// This preserves the ability to detect when credentials come from global repo
3649
secretNS := repo.GetNamespace()
@@ -89,18 +102,5 @@ is that what you want? make sure you use -n when generating the secret, eg: echo
89102
}
90103
logger.Debugf("setupAuthenticatedClient: provider client initialized")
91104

92-
// Handle GitHub App token scoping for both global and repo-level configuration
93-
if event.InstallationID > 0 {
94-
logger.Debugf("setupAuthenticatedClient: scoping github app token")
95-
token, err := github.ScopeTokenToListOfRepos(ctx, vcx, pacInfo, repo, run, event, eventEmitter, logger)
96-
if err != nil {
97-
return fmt.Errorf("failed to scope token: %w", err)
98-
}
99-
// If Global and Repo level configurations are not provided then lets not override the provider token.
100-
if token != "" {
101-
event.Provider.Token = token
102-
}
103-
}
104-
105105
return nil
106106
}

pkg/gitclient/client_setup_test.go

Lines changed: 154 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,11 @@ func seedTestData(ctx context.Context, t *testing.T, repos []*v1alpha1.Repositor
113113
},
114114
Info: info.Info{
115115
Controller: &info.ControllerInfo{
116-
Secret: "pipelines-as-code-secret",
116+
Secret: "pipelines-as-code-secret",
117+
GlobalRepository: "global-repo",
118+
},
119+
Kube: &info.KubeOpts{
120+
Namespace: "default",
117121
},
118122
},
119123
}
@@ -339,6 +343,155 @@ func TestSetupAuthenticatedClientRepositoryConfig(t *testing.T) {
339343
}
340344
}
341345

346+
// TestSetupAuthenticatedClientGlobalRepoAutoFetch tests the auto-fetch behavior
347+
// when globalRepo is nil.
348+
func TestSetupAuthenticatedClientGlobalRepoAutoFetch(t *testing.T) {
349+
t.Parallel()
350+
351+
tests := []struct {
352+
name string
353+
globalRepoExist bool
354+
passGlobalRepo bool
355+
wantErr bool
356+
description string
357+
}{
358+
{
359+
name: "globalRepo nil and exists in API is fetched and merged",
360+
globalRepoExist: true,
361+
passGlobalRepo: false,
362+
wantErr: false,
363+
description: "When globalRepo is nil and exists in the API, it should be fetched automatically",
364+
},
365+
{
366+
name: "globalRepo nil and does not exist in API continues without error",
367+
globalRepoExist: false,
368+
passGlobalRepo: false,
369+
wantErr: false,
370+
description: "When globalRepo is nil and does not exist in the API, function should continue without error",
371+
},
372+
{
373+
name: "globalRepo provided skips API fetch",
374+
globalRepoExist: false,
375+
passGlobalRepo: true,
376+
wantErr: false,
377+
description: "When globalRepo is provided, no API fetch should happen",
378+
},
379+
}
380+
381+
for _, tt := range tests {
382+
t.Run(tt.name, func(t *testing.T) {
383+
t.Parallel()
384+
385+
ctx, log := setupTestContext(t)
386+
387+
repo := createTestRepository(true)
388+
repos := []*v1alpha1.Repository{repo}
389+
390+
// If global repo should exist in the API, seed it
391+
var globalRepo *v1alpha1.Repository
392+
if tt.globalRepoExist {
393+
globalRepo = testnewrepo.NewRepo(testnewrepo.RepoTestcreationOpts{
394+
Name: "global-repo",
395+
URL: "https://github.com/global/repo",
396+
InstallNamespace: "default",
397+
})
398+
globalRepo.Spec.GitProvider = &v1alpha1.GitProvider{
399+
URL: "https://github.com",
400+
Secret: &v1alpha1.Secret{
401+
Name: "global-secret",
402+
Key: "token",
403+
},
404+
}
405+
repos = append(repos, globalRepo)
406+
}
407+
408+
run := seedTestData(ctx, t, repos)
409+
testProvider := createTestProvider(log)
410+
411+
kint := createTestKintMock(map[string]string{
412+
"test-secret": "test-token",
413+
"global-secret": "global-token",
414+
})
415+
416+
event := createTestEvent("pull_request", 0, "")
417+
pacInfo := createTestPacInfo()
418+
419+
// Determine what to pass as globalRepo parameter
420+
var globalRepoParam *v1alpha1.Repository
421+
if tt.passGlobalRepo {
422+
globalRepoParam = &v1alpha1.Repository{
423+
ObjectMeta: metav1.ObjectMeta{
424+
Name: "provided-global-repo",
425+
Namespace: "default",
426+
},
427+
}
428+
}
429+
430+
err := SetupAuthenticatedClient(ctx, testProvider, kint, run, event, repo, globalRepoParam, pacInfo, log)
431+
432+
if tt.wantErr {
433+
assert.Assert(t, err != nil, "%s: expected error but got nil", tt.description)
434+
} else {
435+
assert.NilError(t, err, "%s: %v", tt.description, err)
436+
}
437+
})
438+
}
439+
}
440+
441+
// TestSetupAuthenticatedClientGlobalRepoMergesSettings verifies that when globalRepo
442+
// is auto-fetched, its settings are properly merged into the local repo.
443+
func TestSetupAuthenticatedClientGlobalRepoMergesSettings(t *testing.T) {
444+
t.Parallel()
445+
446+
ctx, log := setupTestContext(t)
447+
448+
// Local repo with git_provider URL but no secret (secret comes from global)
449+
repo := testnewrepo.NewRepo(testnewrepo.RepoTestcreationOpts{
450+
Name: "test-repo",
451+
URL: "https://github.com/owner/repo",
452+
InstallNamespace: "default",
453+
})
454+
repo.Spec.GitProvider = &v1alpha1.GitProvider{
455+
URL: "https://github.com",
456+
}
457+
458+
// Global repo with git_provider credentials
459+
globalRepo := testnewrepo.NewRepo(testnewrepo.RepoTestcreationOpts{
460+
Name: "global-repo",
461+
URL: "https://github.com/global/repo",
462+
InstallNamespace: "default",
463+
})
464+
globalRepo.Spec.GitProvider = &v1alpha1.GitProvider{
465+
URL: "https://github.com",
466+
Secret: &v1alpha1.Secret{
467+
Name: "global-secret",
468+
Key: "token",
469+
},
470+
WebhookSecret: &v1alpha1.Secret{
471+
Name: "global-secret",
472+
Key: "webhook.secret",
473+
},
474+
}
475+
476+
run := seedTestData(ctx, t, []*v1alpha1.Repository{repo, globalRepo})
477+
testProvider := createTestProvider(log)
478+
479+
kint := createTestKintMock(map[string]string{
480+
"global-secret": "global-token-value",
481+
})
482+
483+
event := createTestEvent("pull_request", 0, "")
484+
pacInfo := createTestPacInfo()
485+
486+
// Pass nil globalRepo so it gets auto-fetched
487+
err := SetupAuthenticatedClient(ctx, testProvider, kint, run, event, repo, nil, pacInfo, log)
488+
489+
assert.NilError(t, err, "should succeed with auto-fetched global repo providing credentials")
490+
// After merge, the repo should have secret from global repo
491+
assert.Assert(t, repo.Spec.GitProvider.Secret != nil, "secret should be merged from global repo")
492+
assert.Equal(t, "global-token-value", event.Provider.Token, "token should come from global repo secret")
493+
}
494+
342495
// TestSetupAuthenticatedClient_WebhookValidation tests webhook secret validation.
343496
func TestSetupAuthenticatedClientWebhookValidation(t *testing.T) {
344497
t.Parallel()

pkg/pipelineascode/pipelineascode_test.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -610,7 +610,11 @@ func TestRun(t *testing.T) {
610610
},
611611
},
612612
Controller: &info.ControllerInfo{
613-
Secret: info.DefaultPipelinesAscodeSecretName,
613+
Secret: info.DefaultPipelinesAscodeSecretName,
614+
GlobalRepository: "global-repo",
615+
},
616+
Kube: &info.KubeOpts{
617+
Namespace: repo.InstallNamespace,
614618
},
615619
},
616620
}

pkg/provider/github/acl_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -539,7 +539,7 @@ func TestOkToTestCommentSHA(t *testing.T) {
539539
pacInfo: pacopts,
540540
}
541541

542-
payload := fmt.Sprintf(`{"action": "created", "repository": {"name": "repo", "owner": {"login": "owner"}}, "sender": {"login": %q}, "issue": {"pull_request": {"html_url": "https://github.com/owner/repo/pull/1"}}, "comment": {"body": %q}}`,
542+
payload := fmt.Sprintf(`{"action": "created", "repository": {"name": "repo", "owner": {"login": "owner"}, "html_url": "http://url.com/owner/repo/1"}, "sender": {"login": %q}, "issue": {"number": 1, "pull_request": {"html_url": "https://github.com/owner/repo/pull/1"}}, "comment": {"body": %q}}`,
543543
tt.runevent.Sender, tt.commentBody)
544544
_, err := gprovider.ParsePayload(ctx, gprovider.Run, &http.Request{Header: http.Header{"X-Github-Event": []string{"issue_comment"}}}, payload)
545545
if (err != nil) != tt.wantErr {

pkg/provider/github/github.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,19 @@ func (v *Provider) SetClient(ctx context.Context, run *params.Run, event *info.E
343343
}
344344
}
345345

346+
// Handle GitHub App token scoping for both global and repo-level configuration
347+
if event.InstallationID > 0 {
348+
v.Logger.Debugf("setupAuthenticatedClient: scoping github app token")
349+
token, err := ScopeTokenToListOfRepos(ctx, v, v.pacInfo, repo, run, event, v.eventEmitter, v.Logger)
350+
if err != nil {
351+
return fmt.Errorf("failed to scope token: %w", err)
352+
}
353+
// If Global and Repo level configurations are not provided then lets not override the provider token.
354+
if token != "" {
355+
event.Provider.Token = token
356+
}
357+
}
358+
346359
return nil
347360
}
348361

0 commit comments

Comments
 (0)