@@ -4,10 +4,11 @@ import (
4
4
"context"
5
5
"fmt"
6
6
"net/http"
7
+ "net/url"
8
+ "strings"
7
9
"time"
8
10
9
11
"github.com/golang-jwt/jwt/v4"
10
- gt "github.com/google/go-github/v71/github"
11
12
"github.com/openshift-pipelines/pipelines-as-code/pkg/apis/pipelinesascode/v1alpha1"
12
13
"github.com/openshift-pipelines/pipelines-as-code/pkg/params"
13
14
"github.com/openshift-pipelines/pipelines-as-code/pkg/provider/github"
@@ -20,8 +21,6 @@ type Install struct {
20
21
repo * v1alpha1.Repository
21
22
ghClient * github.Provider
22
23
namespace string
23
-
24
- repoList []string
25
24
}
26
25
27
26
func NewInstallation (req * http.Request , run * params.Run , repo * v1alpha1.Repository , gh * github.Provider , namespace string ) * Install {
@@ -38,91 +37,70 @@ func NewInstallation(req *http.Request, run *params.Run, repo *v1alpha1.Reposito
38
37
}
39
38
40
39
// 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 .
40
+ // It generates a JWT token, and directly fetches the installation for the
41
+ // repository.
43
42
func (ip * Install ) GetAndUpdateInstallationID (ctx context.Context ) (string , string , int64 , error ) {
44
- var (
45
- enterpriseHost , token string
46
- installationID int64
47
- )
43
+ logger := logging .FromContext (ctx )
48
44
49
45
// Generate a JWT token for authentication
50
46
jwtToken , err := ip .GenerateJWT (ctx )
51
47
if err != nil {
52
48
return "" , "" , 0 , err
53
49
}
54
50
51
+ // Get owner and repo from the repository URL
52
+ repoURL , err := url .Parse (ip .repo .Spec .URL )
53
+ if err != nil {
54
+ return "" , "" , 0 , fmt .Errorf ("failed to parse repository URL: %w" , err )
55
+ }
56
+ pathParts := strings .Split (strings .Trim (repoURL .Path , "/" ), "/" )
57
+ if len (pathParts ) != 2 {
58
+ return "" , "" , 0 , fmt .Errorf ("invalid repository URL path: %s" , repoURL .Path )
59
+ }
60
+ owner := pathParts [0 ]
61
+ repoName := pathParts [1 ]
62
+ if owner == "" || repoName == "" {
63
+ return "" , "" , 0 , fmt .Errorf ("invalid repository URL: owner or repo name is empty" )
64
+ }
65
+
66
+ if ip .ghClient .APIURL == nil {
67
+ return "" , "" , 0 , fmt .Errorf ("github client APIURL is nil" )
68
+ }
55
69
apiURL := * ip .ghClient .APIURL
56
- enterpriseHost = ip .request .Header .Get ("X-GitHub-Enterprise-Host" )
70
+ enterpriseHost : = ip .request .Header .Get ("X-GitHub-Enterprise-Host" )
57
71
if enterpriseHost != "" {
58
- // NOTE: Hopefully this works even when the GHE URL is on another host than the API URL
59
- apiURL = "https://" + enterpriseHost + "/api/v3"
72
+ apiURL = fmt .Sprintf ("https://%s/api/v3" , strings .TrimSuffix (enterpriseHost , "/" ))
60
73
}
61
74
62
- logger := logging .FromContext (ctx )
63
- opt := & gt.ListOptions {PerPage : ip .ghClient .PaginedNumber }
64
75
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 )
76
+ // Directly get the installation for the repository
77
+ installation , _ , err := client . Apps . FindRepositoryInstallation ( ctx , owner , repoName )
78
+ if err != nil {
79
+ // Fallback to finding organization installation if repository installation is not found
80
+ installation , _ , err = client .Apps .FindOrganizationInstallation (ctx , owner )
70
81
if err != nil {
71
- return "" , "" , 0 , err
72
- }
73
- installationData = append (installationData , installationSet ... )
74
- if resp .NextPage == 0 {
75
- break
76
- }
77
- opt .Page = resp .NextPage
78
- }
79
-
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 "".
82
+ // Fallback to finding user installation if organization installation is not found
83
+ installation , _ , err = client .Apps .FindUserInstallation (ctx , owner )
93
84
if err != nil {
94
- logger .Warn (err )
95
- continue
85
+ return "" , "" , 0 , fmt .Errorf ("could not find repository, organization or user installation for %s/%s: %w" , owner , repoName , err )
96
86
}
97
87
}
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
- }
106
88
}
107
- return enterpriseHost , token , installationID , nil
108
- }
109
89
110
- // matchRepos matches GitHub repositories to their installation IDs.
111
- // It lists all repositories accessible to the app installation and checks if
112
- // any match the repository URL in the spec.
113
- func (ip * Install ) matchRepos (ctx context.Context ) (bool , error ) {
114
- installationRepoList , err := github .ListRepos (ctx , ip .ghClient )
115
- if err != nil {
116
- return false , err
90
+ if installation .ID == nil {
91
+ return "" , "" , 0 , fmt .Errorf ("github App installation found but contained no ID. This is likely a bug" )
117
92
}
118
- 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
- }
93
+
94
+ installationID := * installation .ID
95
+ token , err := ip .ghClient .GetAppToken (ctx , ip .run .Clients .Kube , enterpriseHost , installationID , ip .namespace )
96
+ if err != nil {
97
+ logger .Warnf ("Could not get a token for installation ID %d: %v" , installationID , err )
98
+ // Return with the installation ID even if token generation fails,
99
+ // as some operations might only need the ID.
100
+ return enterpriseHost , "" , installationID , nil
124
101
}
125
- return false , nil
102
+
103
+ return enterpriseHost , token , installationID , nil
126
104
}
127
105
128
106
// JWTClaim represents the JWT claims for the GitHub App.
0 commit comments