2
\$\begingroup\$

I came up with the logic for an app that talks to a remote GitHub repository using below. I tried to stick to Go best practices and approaches, but would like to identify bottlenecks and adopt better Go practices wherever applicable

package main
import (
 "context"
 "fmt"
 "log"
 "net/http"
 "strings"
 "time"
 "github.com/google/go-github/v61/github"
 "github.com/sethvargo/go-retry"
)
type APIResult struct {
 Data any
 Response *github.Response
 Error error
}
type GitHubAPIFunc func(ctx context.Context) APIResult
func retryWithExpBackoff(ctx context.Context, githubAPIRequest GitHubAPIFunc) APIResult {
 var result APIResult
 b := retry.NewExponential(1 * time.Second)
 b = retry.WithMaxRetries(5, b)
 b = retry.WithMaxDuration(5*time.Second, b)
 err := retry.Do(ctx, b, func(ctx context.Context) error {
 result = githubAPIRequest(ctx)
 if retryErr := retryDecision(result.Response); retryErr != nil {
 return retryErr
 }
 return result.Error
 })
 result.Error = err
 return result
}
func GetRepository(ctx context.Context, client *github.Client, owner, repo string) (*github.Repository, *github.Response, error) {
 githubAPIRequest := func(ctx context.Context) APIResult {
 repo, resp, err := client.Repositories.Get(ctx, owner, repo)
 return APIResult{Data: repo, Response: resp, Error: err}
 }
 result := retryWithExpBackoff(ctx, githubAPIRequest)
 if result.Error != nil {
 return nil, nil, result.Error
 }
 return result.Data.(*github.Repository), result.Response, nil
}
func retryDecision(resp *github.Response) error {
 // Check the response code. We retry on 500-range responses to allow
 // the server time to recover, as 500's are typically not permanent
 // errors and may relate to outages on the server side. This will catch
 // invalid response codes as well, like 0 and 999.
 if resp.StatusCode == 0 || (resp.StatusCode >= 500 && resp.StatusCode != http.StatusNotImplemented) {
 return retry.RetryableError(fmt.Errorf("unexpected HTTP status %s", resp.Status))
 }
 return nil
}
func CheckGitTag(ctx context.Context, client *github.Client, repo, tag string) error {
 r := strings.Split(repo, "/")
 request := func(ctx context.Context) APIResult {
 ref, resp, err := client.Git.GetRef(ctx, r[0], r[1], fmt.Sprintf("refs/tags/%s", tag))
 return APIResult{Data: ref, Response: resp, Error: err}
 }
 result := retryWithExpBackoff(ctx, request)
 if result.Error != nil {
 return result.Error
 }
 fmt.Printf("result=%v\n", result)
 return nil
}
// NewGitHubClient returns an instance of a GitHub client to interact with a
// remote repository with an OAuth token as an argument.
func NewGitHubClient(ctx context.Context, token, repo string) (*github.Client, error) {
 r := strings.Split(repo, "/")
 client := github.NewClient(nil).WithAuthToken(token)
 request := func(ctx context.Context) APIResult {
 repo, resp, err := client.Repositories.Get(ctx, r[0], r[1])
 return APIResult{Data: repo, Response: resp, Error: err}
 }
 result := retryWithExpBackoff(ctx, request)
 if result.Error != nil {
 return nil, result.Error
 }
 fmt.Printf("result=%v\n", result)
 return client, nil
}
func CommitGitTag(ctx context.Context, client *github.Client, repo, baseSHA, tag string) error {
 r := strings.Split(repo, "/")
 ghTag := &github.Tag{
 Tag: github.String(tag),
 Message: github.String("initial version"),
 Object: &github.GitObject{
 Type: github.String("commit"),
 SHA: github.String(baseSHA),
 },
 }
 request := func(ctx context.Context) APIResult {
 tagInfo, resp, err := client.Git.CreateTag(ctx, r[0], r[1], ghTag)
 return APIResult{Data: tagInfo, Response: resp, Error: err}
 }
 result := retryWithExpBackoff(ctx, request)
 if result.Error != nil {
 return result.Error
 }
 tagCreated, ok := result.Data.(*github.Tag)
 if !ok {
 return fmt.Errorf("unable to get github.Tag from API response")
 }
 // Create a reference to the tag
 ref := &github.Reference{
 Ref: github.String(fmt.Sprintf("refs/tags/%s", tag)),
 Object: &github.GitObject{
 SHA: tagCreated.SHA,
 },
 }
 request = func(ctx context.Context) APIResult {
 _, resp, err := client.Git.CreateRef(ctx, r[0], r[1], ref)
 return APIResult{Response: resp, Error: err}
 }
 result = retryWithExpBackoff(ctx, request)
 if result.Error != nil {
 return result.Error
 }
 return nil
}
func main() {
 ctx := context.Background()
 repo := "randomorg/randomrepo"
 sha := "somrandombaseshaofsomevaryinglength"
 tag := "v0.0.1"
 client, err := NewGitHubClient(ctx, "somerandomauthtokenthatisofnousehere", repo)
 if err != nil {
 log.Fatal(err)
 }
 if err := CheckGitTag(ctx, client, repo, tag); err == nil {
 log.Fatal(fmt.Errorf("tag %s already references an exiting commit in %s", tag, repo))
 }
 if err := CommitGitTag(ctx, client, repo, sha, tag); err != nil {
 log.Fatal(err)
 }
}
toolic
15k5 gold badges29 silver badges209 bronze badges
asked Apr 26, 2024 at 17:33
\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

Retrying

Instead of implementing your own retries, you could implement retries at the client level.

Github.NewClient takes in a optional http.Client argument. You could either implement your own http.Client that does retry or you could use something like go-retryablehttp to do it for you.

retryableClient := retryablehttp.NewClient()
ghclient := github.NewClient(retryableClient.StandardClient())

The benefit of this is that you're pushing the retry logic down the stack, and your application code itself does not need to care or implement how retries are being done.

This can also help remove the APIResult struct, which I presume is created for the sole purpose of implement retries.

General comments

  • Be consistent with naming. Some of your functions start with lowercase alphabets, while others start with uppercase. Since you're not exporting any of these functions, you should just use lowercase for all of them.

  • Be careful of bad input.

    r := strings.Split(repo, "/")
    r[0], r[1]
    

    will panic if someone passes in a repo which does not contain /.

answered Apr 27, 2024 at 20:00
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.