package github import ( "context" "encoding/base64" "log/slog" "net/http" "strconv" "forge.cadoles.com/wpetit/clearcase/internal/core/model" "forge.cadoles.com/wpetit/clearcase/internal/core/port" "forge.cadoles.com/wpetit/clearcase/internal/core/service" "github.com/google/go-github/v69/github" "github.com/pkg/errors" ) type Forge struct { client *github.Client } // CreateIssue implements port.Forge. func (f *Forge) CreateIssue(ctx context.Context, projectID string, title string, body string) (string, error) { repo, err := f.getRepository(ctx, projectID) if err != nil { return "", errors.WithStack(err) } issue, res, err := f.client.Issues.Create(ctx, *repo.Owner.Login, *repo.Name, &github.IssueRequest{ Title: &title, Body: &body, }) if res.StatusCode == http.StatusForbidden || res.StatusCode == http.StatusUnauthorized { slog.ErrorContext(ctx, "could not create issue", slog.Any("error", errors.WithStack(err))) return "", errors.WithStack(service.ErrForgeNotAvailable) } return *issue.HTMLURL, nil } // GetAllProjects implements port.Forge. func (f *Forge) GetAllProjects(ctx context.Context) ([]*model.Project, error) { projects := make([]*model.Project, 0) page := 1 for { repos, res, err := f.client.Repositories.ListByAuthenticatedUser(ctx, &github.RepositoryListByAuthenticatedUserOptions{ Type: "all", ListOptions: github.ListOptions{ Page: page, PerPage: 100, }, }) if err != nil { if res.StatusCode == http.StatusForbidden || res.StatusCode == http.StatusUnauthorized { slog.ErrorContext(ctx, "could not retrieve user repositories", slog.Any("error", errors.WithStack(err))) return nil, errors.WithStack(service.ErrForgeNotAvailable) } return nil, errors.WithStack(err) } for _, r := range repos { if r.ID == nil || r.FullName == nil { continue } var projectDescription string if r.Description != nil { projectDescription = *r.Description } projects = append(projects, &model.Project{ ID: strconv.FormatInt(*r.ID, 10), Name: *r.FullName, Description: projectDescription, }) } if res.NextPage == 0 { return projects, nil } page = res.NextPage } } // GetFile implements port.Forge. func (f *Forge) GetFile(ctx context.Context, rawProjectID string, path string) ([]byte, error) { repo, err := f.getRepository(ctx, rawProjectID) if err != nil { return nil, errors.WithStack(err) } fileContent, _, res, err := f.client.Repositories.GetContents(ctx, *repo.Owner.Login, *repo.Name, path, nil) if err != nil { if res.StatusCode == http.StatusForbidden || res.StatusCode == http.StatusUnauthorized { slog.ErrorContext(ctx, "could not retrieve file content", slog.Any("error", errors.WithStack(err))) return nil, errors.WithStack(service.ErrForgeNotAvailable) } if res.StatusCode == http.StatusNotFound { return nil, errors.WithStack(port.ErrFileNotFound) } return nil, errors.WithStack(err) } decoded, err := base64.StdEncoding.DecodeString(*fileContent.Content) if err != nil { return nil, errors.WithStack(err) } return decoded, nil } // GetIssueTemplate implements port.Forge. func (f *Forge) GetIssueTemplate(ctx context.Context, projectID string) (string, error) { data, err := f.GetFile(ctx, projectID, ".github/ISSUE_TEMPLATE/bug_report.md") if err != nil { return "", errors.WithStack(err) } return string(data), nil } // GetIssues implements port.Forge. func (f *Forge) GetIssues(ctx context.Context, projectID string, issueIDs ...string) ([]*model.Issue, error) { repo, err := f.getRepository(ctx, projectID) if err != nil { return nil, errors.WithStack(err) } issues := make([]*model.Issue, 0) for _, rawIssueID := range issueIDs { issueID, err := strconv.ParseInt(rawIssueID, 10, 64) if err != nil { slog.ErrorContext(ctx, "could not parse issue id", slog.Any("error", errors.WithStack(err))) issues = append(issues, nil) continue } issue, _, err := f.client.Issues.Get(ctx, *repo.Owner.Login, *repo.Name, int(issueID)) if err != nil { slog.ErrorContext(ctx, "could not parse retrieve issue", slog.Int("issueID", int(issueID)), slog.String("repository", *repo.FullName), slog.Any("error", errors.WithStack(err))) issues = append(issues, nil) continue } issues = append(issues, &model.Issue{ ID: strconv.FormatInt(*issue.ID, 10), Title: *issue.Title, Body: *issue.Body, }) } return issues, nil } // GetProject implements port.Forge. func (f *Forge) GetProject(ctx context.Context, rawProjectID string) (*model.Project, error) { repo, err := f.getRepository(ctx, rawProjectID) if err != nil { return nil, errors.WithStack(err) } var projectDescription string if repo.Description != nil { projectDescription = *repo.Description } return &model.Project{ ID: strconv.FormatInt(*repo.ID, 10), Name: *repo.Name, Description: projectDescription, }, nil } // GetProjectLanguages implements port.Forge. func (f *Forge) GetProjectLanguages(ctx context.Context, rawProjectID string) ([]string, error) { repo, err := f.getRepository(ctx, rawProjectID) if err != nil { return nil, errors.WithStack(err) } mappedLanguages, res, err := f.client.Repositories.ListLanguages(ctx, *repo.Owner.Login, *repo.Name) if err != nil { if res.StatusCode == http.StatusForbidden || res.StatusCode == http.StatusUnauthorized { slog.ErrorContext(ctx, "could not retrieve repository languages", slog.Any("error", errors.WithStack(err))) return nil, errors.WithStack(service.ErrForgeNotAvailable) } return nil, errors.WithStack(err) } languages := make([]string, 0, len(mappedLanguages)) for l := range mappedLanguages { languages = append(languages, l) } return languages, nil } func (f *Forge) getRepository(ctx context.Context, rawProjectID string) (*github.Repository, error) { projectID, err := strconv.ParseInt(rawProjectID, 10, 64) if err != nil { return nil, errors.WithStack(err) } repo, res, err := f.client.Repositories.GetByID(ctx, projectID) if err != nil { if res.StatusCode == http.StatusForbidden || res.StatusCode == http.StatusUnauthorized { slog.ErrorContext(ctx, "could not retrieve repository", slog.Any("error", errors.WithStack(err))) return nil, errors.WithStack(service.ErrForgeNotAvailable) } return nil, errors.WithStack(err) } return repo, nil } func NewForge(client *github.Client) *Forge { return &Forge{client: client} } var _ port.Forge = &Forge{}