Skip to content

Commit e8fb002

Browse files
committed
refactor: Improve GitHub App installation ID retrieval
- Removed listing of all installations. - Fetched the installation directly using the repository URL. - Added fallback to find the organization installation if the repository installation is not found. - Enhanced error handling for token retrieval. - Updated tests to reflect the changes. Signed-off-by: Chmouel Boudjnah <[email protected]>
1 parent f3a73b4 commit e8fb002

File tree

2 files changed

+48
-55
lines changed

2 files changed

+48
-55
lines changed

pkg/provider/github/app/token.go

Lines changed: 39 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ import (
44
"context"
55
"fmt"
66
"net/http"
7+
"net/url"
8+
"slices"
9+
"strings"
710
"time"
811

912
"github.com/golang-jwt/jwt/v4"
10-
gt "github.com/google/go-github/v71/github"
1113
"github.com/openshift-pipelines/pipelines-as-code/pkg/apis/pipelinesascode/v1alpha1"
1214
"github.com/openshift-pipelines/pipelines-as-code/pkg/params"
1315
"github.com/openshift-pipelines/pipelines-as-code/pkg/provider/github"
@@ -38,72 +40,59 @@ func NewInstallation(req *http.Request, run *params.Run, repo *v1alpha1.Reposito
3840
}
3941

4042
// GetAndUpdateInstallationID retrieves and updates the installation ID for the GitHub App.
41-
// It generates a JWT token, lists all installations, and matches repositories to their installation IDs.
42-
// If a matching repository is found, it returns the enterprise host, token, and installation ID.
43+
// It generates a JWT token, and directly fetches the installation for the
44+
// repository.
4345
func (ip *Install) GetAndUpdateInstallationID(ctx context.Context) (string, string, int64, error) {
44-
var (
45-
enterpriseHost, token string
46-
installationID int64
47-
)
46+
logger := logging.FromContext(ctx)
4847

4948
// Generate a JWT token for authentication
5049
jwtToken, err := ip.GenerateJWT(ctx)
5150
if err != nil {
5251
return "", "", 0, err
5352
}
5453

54+
// Get owner and repo from the repository URL
55+
repoURL, err := url.Parse(ip.repo.Spec.URL)
56+
if err != nil {
57+
return "", "", 0, fmt.Errorf("failed to parse repository URL: %w", err)
58+
}
59+
pathParts := strings.Split(strings.Trim(repoURL.Path, "/"), "/")
60+
if len(pathParts) < 2 {
61+
return "", "", 0, fmt.Errorf("invalid repository URL path: %s", repoURL.Path)
62+
}
63+
owner := pathParts[0]
64+
repoName := pathParts[1]
65+
5566
apiURL := *ip.ghClient.APIURL
56-
enterpriseHost = ip.request.Header.Get("X-GitHub-Enterprise-Host")
67+
enterpriseHost := ip.request.Header.Get("X-GitHub-Enterprise-Host")
5768
if enterpriseHost != "" {
58-
// NOTE: Hopefully this works even when the GHE URL is on another host than the API URL
5969
apiURL = "https://" + enterpriseHost + "/api/v3"
6070
}
6171

62-
logger := logging.FromContext(ctx)
63-
opt := &gt.ListOptions{PerPage: ip.ghClient.PaginedNumber}
6472
client, _, _ := github.MakeClient(ctx, apiURL, jwtToken)
65-
installationData := []*gt.Installation{}
66-
67-
// List all installations
68-
for {
69-
installationSet, resp, err := client.Apps.ListInstallations(ctx, opt)
73+
// Directly get the installation for the repository
74+
installation, _, err := client.Apps.FindRepositoryInstallation(ctx, owner, repoName)
75+
if err != nil {
76+
// Fallback to finding organization installation if repository installation is not found
77+
installation, _, err = client.Apps.FindOrganizationInstallation(ctx, owner)
7078
if err != nil {
71-
return "", "", 0, err
79+
return "", "", 0, fmt.Errorf("could not find repository or organization installation for %s/%s: %w", owner, repoName, err)
7280
}
73-
installationData = append(installationData, installationSet...)
74-
if resp.NextPage == 0 {
75-
break
76-
}
77-
opt.Page = resp.NextPage
7881
}
7982

80-
// Iterate through each installation to find a matching repository
81-
for i := range installationData {
82-
if installationData[i].ID == nil {
83-
return "", "", 0, fmt.Errorf("installation ID is nil")
84-
}
85-
if *installationData[i].ID != 0 {
86-
token, err = ip.ghClient.GetAppToken(ctx, ip.run.Clients.Kube, enterpriseHost, *installationData[i].ID, ip.namespace)
87-
// While looping on the list of installations, there could be cases where we can't
88-
// obtain a token for installation. In a test I did for GitHub App with ~400
89-
// installations, there were 3 failing consistently with:
90-
// "could not refresh installation id XXX's token: received non 2xx response status "403 Forbidden".
91-
// If there is a matching installation after the failure, we miss it. So instead of
92-
// failing, we just log the error and continue. Token is "".
93-
if err != nil {
94-
logger.Warn(err)
95-
continue
96-
}
97-
}
98-
exist, err := ip.matchRepos(ctx)
99-
if err != nil {
100-
return "", "", 0, err
101-
}
102-
if exist {
103-
installationID = *installationData[i].ID
104-
break
105-
}
83+
if installation.ID == nil {
84+
return "", "", 0, fmt.Errorf("installation ID is nil")
10685
}
86+
87+
installationID := *installation.ID
88+
token, err := ip.ghClient.GetAppToken(ctx, ip.run.Clients.Kube, enterpriseHost, installationID, ip.namespace)
89+
if err != nil {
90+
logger.Warnf("Could not get a token for installation ID %d: %v", installationID, err)
91+
// Return with the installation ID even if token generation fails,
92+
// as some operations might only need the ID.
93+
return enterpriseHost, "", installationID, nil
94+
}
95+
10796
return enterpriseHost, token, installationID, nil
10897
}
10998

@@ -116,11 +105,8 @@ func (ip *Install) matchRepos(ctx context.Context) (bool, error) {
116105
return false, err
117106
}
118107
ip.repoList = append(ip.repoList, installationRepoList...)
119-
for i := range installationRepoList {
120-
// If URL matches with repo spec URL then we can break the loop
121-
if installationRepoList[i] == ip.repo.Spec.URL {
122-
return true, nil
123-
}
108+
if slices.Contains(installationRepoList, ip.repo.Spec.URL) {
109+
return true, nil
124110
}
125111
return false, nil
126112
}

pkg/provider/github/app/token_test.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ func Test_GetAndUpdateInstallationID(t *testing.T) {
170170
badToken := "BADTOKEN"
171171
badID := 666
172172
missingID := 111
173+
orgName := "org"
173174

174175
fakeghclient, mux, serverURL, teardown := ghtesthelper.SetupGH()
175176
defer teardown()
@@ -214,7 +215,7 @@ func Test_GetAndUpdateInstallationID(t *testing.T) {
214215
Name: "repo",
215216
},
216217
Spec: v1alpha1.RepositorySpec{
217-
URL: "https://matched/by/incoming",
218+
URL: fmt.Sprintf("https://matched/%s/incoming", orgName),
218219
Incomings: &[]v1alpha1.Incoming{
219220
{
220221
Targets: []string{"main"},
@@ -235,6 +236,10 @@ func Test_GetAndUpdateInstallationID(t *testing.T) {
235236
_, _ = fmt.Fprintf(w, `{"token": "%s"}`, wantToken)
236237
})
237238

239+
mux.HandleFunc(fmt.Sprintf("/orgs/%s/installation", orgName), func(w http.ResponseWriter, _ *http.Request) {
240+
_, _ = fmt.Fprintf(w, `{"id": %d}`, wantID)
241+
})
242+
238243
mux.HandleFunc(fmt.Sprintf("/app/installations/%d/access_tokens", badID), func(w http.ResponseWriter, r *http.Request) {
239244
testMethod(t, r, "POST")
240245
w.Header().Set("Authorization", "Bearer "+jwtToken)
@@ -247,7 +252,9 @@ func Test_GetAndUpdateInstallationID(t *testing.T) {
247252
mux.HandleFunc("/installation/repositories", func(w http.ResponseWriter, _ *http.Request) {
248253
w.Header().Set("Authorization", "Bearer 12345")
249254
w.Header().Set("Accept", "application/vnd.github+json")
250-
_, _ = fmt.Fprint(w, `{"total_count": 2,"repositories": [{"id":1,"html_url": "https://matched/by/incoming"},{"id":2,"html_url": "https://anotherrepo/that/would/failit"}]}`)
255+
_, _ = fmt.Fprintf(w,
256+
`{"total_count": 2,"repositories": [{"id":1,"html_url": "https://matched/%s/incoming"},{"id":2,"html_url": "https://anotherrepo/that/would/failit"}]}`,
257+
orgName)
251258
})
252259
ip = NewInstallation(req, run, repo, gprovider, testNamespace.GetName())
253260
_, token, installationID, err := ip.GetAndUpdateInstallationID(ctx)

0 commit comments

Comments
 (0)