package githubclient import ( "context" "fmt" "sync" "time" "github.com/google/go-github/v47/github" ) type Client struct { *github.Client cfg Config owner string repo string } // just for testing remove when in review func (c *Client) ToString() string { return fmt.Sprintf("org %s repo %s", c.owner, c.repo) } func (c *Client) Config() Config { return c.cfg } // NewClient creates a new github.Client with the provided token func NewClient(cfg Config) (*Client, error) { err := cfg.Validate() if err != nil { return nil, err } if cfg.AccessToken != "" { hc, err := cfg.HTTPClient() if err != nil { return nil, err } client := github.NewClient(hc) owner := cfg.Owner() repo := cfg.Repo() return &Client{client, cfg, owner, repo}, nil } if cfg.InstallationID == 0 { cfg.InstallationID, err = getInstallationID(cfg) if err != nil { return nil, err } } fmt.Printf("cfg %+v \n", cfg.InstallationID) hc, err := cfg.HTTPClient() if err != nil { return nil, err } client := github.NewClient(hc) owner := cfg.Owner() repo := cfg.Repo() return &Client{client, cfg, owner, repo}, nil } // installationIDCache speeds up the github client, reduces API calls, and lives as a singleton for a few reasons: // - since the InstallationID shouldn't change, the cache can live until Chariot restarts. // - Github clients are created/destroyed often so an instance variable wouldn't perform well. // // Getting the InstallationID is called often enough that bypassing a 304 request is worth creating // a bespoke cache like this. var installationIDCache = struct { sync.Mutex c map[string]int64 // Map[RepositoryURL]BranchName }{ c: make(map[string]int64), } func setInstallationIDCache(repository string, id int64) { installationIDCache.Lock() installationIDCache.c[repository] = id installationIDCache.Unlock() } func getInstallationIDCache(repository string) (id int64, found bool) { installationIDCache.Lock() id, found = installationIDCache.c[repository] installationIDCache.Unlock() return } // Finds the installation id using the `cfg` provided // // This function requires constructing an http client `cfg.HTTPClient()` that works with the Apps service. // The constructed client can find the InstallationID but can't be reused for other Github API services. // This function was created to encapsulate the reuse logic since it's not obvious why we create two http // clients when calling `NewClient`. // // This function also caches the InstallationID in memory since they shouldn't change. func getInstallationID(cfg Config) (int64, error) { if cfg.InstallationID != 0 { return 0, fmt.Errorf("InstallationID not zero. Got: %d", cfg.InstallationID) } // check the local cache if cached, found := getInstallationIDCache(cfg.Repository); found { return cached, nil } hc, err := cfg.HTTPClient() if err != nil { return 0, err } ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() apps := github.NewClient(hc).Apps installation, _, err := apps.FindRepositoryInstallation(ctx, cfg.Owner(), cfg.Repo()) if err != nil { return 0, err } id := installation.GetID() if id <= 0 { return 0, fmt.Errorf("InstallationID less than or equal to 0 means installation does not exist for owner/repo. Got %d", id) } setInstallationIDCache(cfg.Repository, id) return id, nil }