From 912da08c643471783d10d62433debf403a730430 Mon Sep 17 00:00:00 2001 From: Andrew Henry Date: Tue, 31 Oct 2023 13:18:56 -0400 Subject: [PATCH 01/12] feat: overhaul the repo collector --- .github/workflows/graphql.yml | 2 +- .../github/fetchers/contribution_info.go | 40 ++++++-- .../collectors/github/fetchers/org_info.go | 48 +++++++--- .../github/fetchers/repos_info_fetcher.go | 58 ++++++++++-- .../business/core/collectors/github/github.go | 93 +++++++++++++++---- backend/business/core/core.go | 2 +- backend/business/helpers/helpers.go | 4 + backend/cmd/main.go | 38 +++++++- who-metrics-ui/next.config.js | 9 +- who-metrics-ui/src/data/data.json | 30 ++++++ who-metrics-ui/src/data/types.d.ts | 23 +++++ 11 files changed, 288 insertions(+), 59 deletions(-) create mode 100644 who-metrics-ui/src/data/data.json create mode 100644 who-metrics-ui/src/data/types.d.ts diff --git a/.github/workflows/graphql.yml b/.github/workflows/graphql.yml index 3180269..fdf8b99 100644 --- a/.github/workflows/graphql.yml +++ b/.github/workflows/graphql.yml @@ -5,7 +5,7 @@ # - saves the output to a file # - saves the file in the repository -name: Fecth metrics +name: Fetch metrics on: # manually trigger the workflow diff --git a/backend/business/core/collectors/github/fetchers/contribution_info.go b/backend/business/core/collectors/github/fetchers/contribution_info.go index 459aab8..7a668b4 100644 --- a/backend/business/core/collectors/github/fetchers/contribution_info.go +++ b/backend/business/core/collectors/github/fetchers/contribution_info.go @@ -12,6 +12,10 @@ type ContributionInfoFetcher struct { orgName string } +type ContributionInfoResult struct { + RepositoryName string `json:"repositoryName"` +} + func NewContributionInfoFetcher(client *githubv4.Client, orgName string) *ContributionInfoFetcher { return &ContributionInfoFetcher{client: client, orgName: orgName} } @@ -19,6 +23,7 @@ func NewContributionInfoFetcher(client *githubv4.Client, orgName string) *Contri type contributionInfo struct { Node struct { NameWithOwner githubv4.String + Name githubv4.String } } @@ -34,7 +39,7 @@ type contributionInfoQuery struct { } `graphql:"organization(login:$organizationLogin)"` } -func (c *ContributionInfoFetcher) Fetch(ctx context.Context) (string, error) { +func (c *ContributionInfoFetcher) Fetch(ctx context.Context) (*map[string]ContributionInfoResult, error) { variables := map[string]interface{}{ "organizationLogin": githubv4.String(c.orgName), "reposCursor": (*githubv4.String)(nil), // Null after argument to get first page. @@ -47,7 +52,7 @@ func (c *ContributionInfoFetcher) Fetch(ctx context.Context) (string, error) { for { err := c.client.Query(ctx, &q, variables) if err != nil { - return "", err + return nil, err } allRepos = append(allRepos, q.Organization.Repositories.Edges...) if !q.Organization.Repositories.PageInfo.HasNextPage { @@ -56,14 +61,12 @@ func (c *ContributionInfoFetcher) Fetch(ctx context.Context) (string, error) { variables["reposCursor"] = githubv4.NewString(q.Organization.Repositories.PageInfo.EndCursor) } - csvString, err := c.formatCSV(allRepos) - if err != nil { - return "", err - } - return csvString, nil + result := c.buildResult(allRepos) + return result, nil } -func (f *ContributionInfoFetcher) formatCSV(data []contributionInfo) (string, error) { +// FormatCSV formats the given data as CSV +func (f *ContributionInfoFetcher) FormatCSV(data []contributionInfo) (string, error) { csvString := "repo_name,issues_opened\n" for _, edge := range data { csvString += fmt.Sprintf("%s\n", @@ -76,3 +79,24 @@ func (f *ContributionInfoFetcher) formatCSV(data []contributionInfo) (string, er } return csvString, nil } + +func (f *ContributionInfoFetcher) buildResult(data []contributionInfo) *map[string]ContributionInfoResult { + result := make(map[string]ContributionInfoResult) + for _, edge := range data { + result[string(edge.Node.NameWithOwner)] = ContributionInfoResult{ + RepositoryName: string(edge.Node.Name), + // CollaboratorsCount: edge.Node.Collaborators.TotalCount, + // ProjectsCount: edge.Node.Projects.TotalCount + edge.Node.ProjectsV2.TotalCount, + // DiscussionsCount: edge.Node.Discussions.TotalCount, + // ForksCount: edge.Node.Forks.TotalCount, + // IssuesCount: edge.Node.Issues.TotalCount, + // OpenIssuesCount: edge.Node.OpenIssues.TotalCount, + // ClosedIssuesCount: edge.Node.ClosedIssues.TotalCount, + // OpenPRsCount: edge.Node.OpenPullRequests.TotalCount, + // MergedPRsCount: edge.Node.MergedPullRequests.TotalCount, + // LicenseName: edge.Node.LicenseInfo.Name, + // WatchersCount: edge.Node.Watchers.TotalCount, + } + } + return &result +} diff --git a/backend/business/core/collectors/github/fetchers/org_info.go b/backend/business/core/collectors/github/fetchers/org_info.go index b57d6c2..26a5a91 100644 --- a/backend/business/core/collectors/github/fetchers/org_info.go +++ b/backend/business/core/collectors/github/fetchers/org_info.go @@ -7,11 +7,23 @@ import ( "github.com/shurcooL/githubv4" ) -type OrgInfo struct { +type OrgInfoFetcher struct { client *githubv4.Client orgName string } +type OrgInfoResult struct { + Login string `json:"login"` + Name string `json:"name"` + Description string `json:"description"` + CreatedAt string `json:"createdAt"` + MembersWithRoleCount int `json:"membersWithRoleCount"` + ProjectsCount int `json:"projectsCount"` + ProjectsV2Count int `json:"projectsV2Count"` + RepositoriesCount int `json:"repositoriesCount"` + TeamsCount int `json:"teamsCount"` +} + type orgInfo struct { Login githubv4.String Name githubv4.String @@ -38,31 +50,31 @@ type orgInfoQuery struct { Organization orgInfo `graphql:"organization(login:$organizationLogin)"` } -func NewOrgInfo(client *githubv4.Client, orgName string) *OrgInfo { - return &OrgInfo{client: client, orgName: orgName} +func NewOrgInfo(client *githubv4.Client, orgName string) *OrgInfoFetcher { + return &OrgInfoFetcher{client: client, orgName: orgName} } -func (o *OrgInfo) Fetch(ctx context.Context) (string, error) { +func (oif *OrgInfoFetcher) Fetch(ctx context.Context) (*OrgInfoResult, error) { variables := map[string]interface{}{ - "organizationLogin": githubv4.String(o.orgName), + "organizationLogin": githubv4.String(oif.orgName), } var q orgInfoQuery - err := o.client.Query(ctx, &q, variables) + err := oif.client.Query(ctx, &q, variables) if err != nil { - return "", err + return nil, err } - csvString, err := o.formatCSV(q.Organization) + result := oif.buildResult(q.Organization) if err != nil { - return "", err + return result, err } - return csvString, nil + return result, nil } -func (f *OrgInfo) formatCSV(data orgInfo) (string, error) { +func (f *OrgInfoFetcher) formatCSV(data orgInfo) (string, error) { csvString := "login,name,description,createdAt,totalMembers,totalProjects,totalRepositories,totalTeams\n" csvString += fmt.Sprintf("%s,%s,%s,%s,%d,%d,%d,%d\n", data.Login, @@ -76,3 +88,17 @@ func (f *OrgInfo) formatCSV(data orgInfo) (string, error) { ) return csvString, nil } + +func (f *OrgInfoFetcher) buildResult(data orgInfo) *OrgInfoResult { + return &OrgInfoResult{ + Login: string(data.Login), + Name: string(data.Name), + Description: string(data.Description), + CreatedAt: data.CreatedAt.Format("2006-01-02T15:04:05-0700"), + MembersWithRoleCount: int(data.MembersWithRole.TotalCount), + ProjectsCount: int(data.Projects.TotalCount), + ProjectsV2Count: int(data.ProjectsV2.TotalCount), + RepositoriesCount: int(data.Repositories.TotalCount), + TeamsCount: int(data.Teams.TotalCount), + } +} diff --git a/backend/business/core/collectors/github/fetchers/repos_info_fetcher.go b/backend/business/core/collectors/github/fetchers/repos_info_fetcher.go index ac42fbf..c6c2354 100644 --- a/backend/business/core/collectors/github/fetchers/repos_info_fetcher.go +++ b/backend/business/core/collectors/github/fetchers/repos_info_fetcher.go @@ -7,6 +7,21 @@ import ( "github.com/shurcooL/githubv4" ) +type RepoInfoResult struct { + RepoName string `json:"repoName"` + CollaboratorsCount int `json:"collaboratorsCount"` + ProjectsCount int `json:"projectsCount"` + DiscussionsCount int `json:"discussionsCount"` + ForksCount int `json:"forksCount"` + IssuesCount int `json:"issuesCount"` + OpenIssuesCount int `json:"openIssuesCount"` + ClosedIssuesCount int `json:"closedIssuesCount"` + OpenPullRequestsCount int `json:"openPullRequestsCount"` + MergedPullRequestsCount int `json:"mergedPullRequestsCount"` + LicenseName string `json:"licenseName"` + WatchersCount int `json:"watchersCount"` +} + type ReposInfoFetcher struct { client *githubv4.Client orgName string @@ -37,7 +52,7 @@ type repoInfo struct { Issues struct { TotalCount githubv4.Int } - OpenIssues struct { + OpenIssues struct { TotalCount githubv4.Int } `graphql:"openIssues: issues(states: OPEN)"` ClosedIssues struct { @@ -66,11 +81,12 @@ type reposInfoQuery struct { EndCursor githubv4.String HasNextPage bool } - } `graphql:"repositories(first: 100, privacy: PUBLIC, after: $reposCursor)"` + } `graphql:"repositories(first: 20, privacy: PUBLIC, after: $reposCursor)"` } `graphql:"organization(login:$organizationLogin)"` } -func (f *ReposInfoFetcher) Fetch(ctx context.Context) (string, error) { +// Fetch fetches the repos info for the given organization +func (f *ReposInfoFetcher) Fetch(ctx context.Context) (*map[string]RepoInfoResult, error) { variables := map[string]interface{}{ "organizationLogin": githubv4.String(f.orgName), "reposCursor": (*githubv4.String)(nil), // Null after argument to get first page. @@ -82,8 +98,10 @@ func (f *ReposInfoFetcher) Fetch(ctx context.Context) (string, error) { for { err := f.client.Query(ctx, &q, variables) + fmt.Println(err) + if err != nil { - return "", err + return nil, fmt.Errorf("failed to fetch repos info for %s: %s", f.orgName, err.Error()) } allRepos = append(allRepos, q.Organization.Repositories.Edges...) if !q.Organization.Repositories.PageInfo.HasNextPage { @@ -92,14 +110,12 @@ func (f *ReposInfoFetcher) Fetch(ctx context.Context) (string, error) { variables["reposCursor"] = githubv4.NewString(q.Organization.Repositories.PageInfo.EndCursor) } - csvString, err := f.formatCSV(allRepos) - if err != nil { - return "", err - } - return csvString, nil + result := f.buildJSON(allRepos) + return result, nil } -func (f *ReposInfoFetcher) formatCSV(data []repoInfo) (string, error) { +// FormatCSV formats the given data as CSV file +func (f *ReposInfoFetcher) FormatCSV(data []repoInfo) (string, error) { csvString := "repo_name,collaborators_count,projects_count,discussions_count,forks_count,issues_count,open_issues_count,closed_issues_count,open_pull_requests_count,merged_pull_requests_count,license_name,watchers_count\n" for _, edge := range data { csvString += fmt.Sprintf("%s,%d,%d,%d,%d,%d,%d,%d,%d,%d,%s,%d\n", @@ -119,3 +135,25 @@ func (f *ReposInfoFetcher) formatCSV(data []repoInfo) (string, error) { } return csvString, nil } + +func (f *ReposInfoFetcher) buildJSON(data []repoInfo) *map[string]RepoInfoResult { + holder := make(map[string]RepoInfoResult) + for _, edge := range data { + holder[string(edge.Node.NameWithOwner)] = RepoInfoResult{ + RepoName: string(edge.Node.NameWithOwner), + CollaboratorsCount: int(edge.Node.Collaborators.TotalCount), + ProjectsCount: int(edge.Node.Projects.TotalCount + edge.Node.ProjectsV2.TotalCount), + DiscussionsCount: int(edge.Node.Discussions.TotalCount), + ForksCount: int(edge.Node.Forks.TotalCount), + IssuesCount: int(edge.Node.Issues.TotalCount), + OpenIssuesCount: int(edge.Node.OpenIssues.TotalCount), + ClosedIssuesCount: int(edge.Node.ClosedIssues.TotalCount), + OpenPullRequestsCount: int(edge.Node.OpenPullRequests.TotalCount), + MergedPullRequestsCount: int(edge.Node.MergedPullRequests.TotalCount), + LicenseName: string(edge.Node.LicenseInfo.Name), + WatchersCount: int(edge.Node.Watchers.TotalCount), + } + } + + return &holder +} diff --git a/backend/business/core/collectors/github/github.go b/backend/business/core/collectors/github/github.go index 5a3e3af..d9cec9d 100644 --- a/backend/business/core/collectors/github/github.go +++ b/backend/business/core/collectors/github/github.go @@ -2,37 +2,88 @@ package github import ( "context" + "fmt" "github.com/shurcooL/githubv4" "github.com/who-metrics/business/core/collectors/github/fetchers" ) -type Fetcher interface { - Fetch(ctx context.Context) (string, error) +type GitHubCollector struct { + fetchers Fetchers + // format string } -type GitHubCollector struct { - fetchers []Fetcher +func NewGitHubCollector(client *githubv4.Client, organizationName string) *GitHubCollector { + return &GitHubCollector{fetchers: buildFetchers(client, organizationName)} } -func NewGitHubCollector(client *githubv4.Client) *GitHubCollector { - return &GitHubCollector{fetchers: buildFetchers(client)} +type RepositoryInfoResult struct { + fetchers.ContributionInfoResult + fetchers.RepoInfoResult +} + +type ResultOutput struct { + OrgInfo *fetchers.OrgInfoResult `json:"orgInfo"` + Repositories map[string]RepositoryInfoResult `json:"repositories"` } func (c *GitHubCollector) Collect(ctx context.Context) ([]string, []error) { - // TODO: process with goroutines - results, errors := make([]string, len(c.fetchers)), make([]error, len(c.fetchers)) - for _, fetcher := range c.fetchers { - result, err := fetcher.Fetch(ctx) - if err != nil { - errors = append(errors, err) + errors := make([]error, 0) + orgInfo, err := c.fetchers.OrgInfo.Fetch(ctx) + if err != nil { + errors = append(errors, err) + } + reposInfo, err := c.fetchers.ReposInfo.Fetch(ctx) + if err != nil { + errors = append(errors, err) + } + contributionInfo, err := c.fetchers.ContributionInfo.Fetch(ctx) + if err != nil { + errors = append(errors, err) + } + + // Build the final result + result := ResultOutput{ + OrgInfo: orgInfo, + Repositories: make(map[string]RepositoryInfoResult), + } + + for _, repo := range *reposInfo { + result.Repositories[repo.RepoName] = RepositoryInfoResult{ + RepoInfoResult: repo, + ContributionInfoResult: (*contributionInfo)[repo.RepoName], } - results = append(results, result) } - return results, errors + + fmt.Println(result) + + return []string{}, errors + // // Handle csv format + // if c.format == "csv" { + // // TODO: process with goroutines + // results, errors := make([]string, len(c.fetchers)), make([]error, 0) + + // for _, fetcher := range c.fetchers { + // result, err := fetcher.Fetch(ctx) + // if err != nil { + // errors = append(errors, err) + // } + // results = append(results, result) + // } + // return results, errors + + // } + + // Handle json format } -func buildFetchers(client *githubv4.Client) []Fetcher { +type Fetchers struct { + OrgInfo *fetchers.OrgInfoFetcher + ReposInfo *fetchers.ReposInfoFetcher + ContributionInfo *fetchers.ContributionInfoFetcher +} + +func buildFetchers(client *githubv4.Client, organizationName string) Fetchers { // TODO: // load all variables from env or somewhere else // it should look something like this: @@ -42,10 +93,12 @@ func buildFetchers(client *githubv4.Client) []Fetcher { &fetchers.OrgInfo{client: client, orgName: vars.orgName}, } */ - // TODO: map the fetchers to keys so we can easily access them - return []Fetcher{ - // fetchers.NewOrgInfo(client, "WorldHealthOrganization"), - fetchers.NewReposInfoFetcher(client, "WorldHealthOrganization"), - // fetchers.NewContributionInfoFetcher(client, "WorldHealthOrganization"), + fetchers := Fetchers{ + OrgInfo: fetchers.NewOrgInfo(client, organizationName), + ReposInfo: fetchers.NewReposInfoFetcher(client, organizationName), + ContributionInfo: fetchers.NewContributionInfoFetcher(client, organizationName), } + + // TODO: map the fetchers to keys so we can easily access them + return fetchers } diff --git a/backend/business/core/core.go b/backend/business/core/core.go index c0d7feb..dbc7d01 100644 --- a/backend/business/core/core.go +++ b/backend/business/core/core.go @@ -23,7 +23,7 @@ Its because Collect is a method of the Collector interface. And "Amass" is a synonym for "collect" (and is much cooler). */ func (c *Core) Amass(ctx context.Context) ([]string, []error) { - results, errors := make([]string, len(c.collectors)), make([]error, len(c.collectors)) + results, errors := make([]string, len(c.collectors)), make([]error, 0) for _, collector := range c.collectors { r, errs := collector.Collect(ctx) results = append(results, r...) diff --git a/backend/business/helpers/helpers.go b/backend/business/helpers/helpers.go index 7906a89..93eeaa9 100644 --- a/backend/business/helpers/helpers.go +++ b/backend/business/helpers/helpers.go @@ -23,6 +23,10 @@ func FormatJSON(data interface{}) (string, error) { } func NewGHGraphQLClient() *githubv4.Client { + if os.Getenv("GRAPHQL_TOKEN") == "" { + panic("GRAPHQL_TOKEN environment variable is not set") + } + src := oauth2.StaticTokenSource( &oauth2.Token{AccessToken: os.Getenv("GRAPHQL_TOKEN")}, ) diff --git a/backend/cmd/main.go b/backend/cmd/main.go index 23b086e..486ba24 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "flag" "fmt" "log" @@ -11,20 +12,47 @@ import ( ) func main() { - err := run() + formatPtr := flag.String("format", "csv", "Format of the output file: csv or json") + + if (*formatPtr != "csv") && (*formatPtr != "json") { + log.Fatal("Invalid format flag") + } + + err := run(*formatPtr) if err != nil { log.Println(err) } } -func run() error { +func run(format string) error { // GitHub GraphQL API v4 token. c := helpers.NewGHGraphQLClient() - githubCollector := github.NewGitHubCollector(c) + githubCollector := github.NewGitHubCollector(c, "sbv-world-health-org-metrics") core := core.New(githubCollector) results, errors := core.Amass(context.Background()) - fmt.Println(results) - fmt.Println(errors) + + // We should really retry on error, and preserve the data we've already collected + // TODO: maybe fix this? + if len(errors) > 0 { + // panic(errors) + } + + if (format == "csv") && (len(results) > 0) { + fmt.Println(results) + fmt.Println(errors) + return nil + } + + if (format == "json") && (len(results) > 0) { + output, err := helpers.FormatJSON(results) + if err != nil { + return err + } + + fmt.Println(output) + return nil + } + return nil } diff --git a/who-metrics-ui/next.config.js b/who-metrics-ui/next.config.js index a2872ba..7d79223 100644 --- a/who-metrics-ui/next.config.js +++ b/who-metrics-ui/next.config.js @@ -2,10 +2,13 @@ const nextConfig = { reactStrictMode: true, productionBrowserSourceMaps: true, - basePath: '/sbv-world-health-org-metrics', + basePath: + process.env.NODE_ENV === "development" + ? "" + : "/sbv-world-health-org-metrics", images: { unoptimized: true, }, -} +}; -module.exports = nextConfig +module.exports = nextConfig; diff --git a/who-metrics-ui/src/data/data.json b/who-metrics-ui/src/data/data.json new file mode 100644 index 0000000..3d92151 --- /dev/null +++ b/who-metrics-ui/src/data/data.json @@ -0,0 +1,30 @@ +{ + "github/github": { + "repoName": "github", + "collaboratorsCount": 10, + "projectsCount": 100, + "discussionsCount": 100, + "forksCount": 100, + "issuesCount": 100, + "openIssuesCount": 100, + "closedIssuesCount": 100, + "openPullRequestsCount": 100, + "mergedPullRequestsCount": 100, + "licenseName": "MIT", + "watchersCount": 100 + }, + "github/ospo": { + "repoName": "github", + "collaboratorsCount": 10, + "projectsCount": 100, + "discussionsCount": 100, + "forksCount": 100, + "issuesCount": 100, + "openIssuesCount": 100, + "closedIssuesCount": 100, + "openPullRequestsCount": 100, + "mergedPullRequestsCount": 100, + "licenseName": "MIT", + "watchersCount": 100 + } +} diff --git a/who-metrics-ui/src/data/types.d.ts b/who-metrics-ui/src/data/types.d.ts new file mode 100644 index 0000000..251eb3e --- /dev/null +++ b/who-metrics-ui/src/data/types.d.ts @@ -0,0 +1,23 @@ +declare module "data.json" { + interface RepoData { + repoName: string; + collaboratorsCount: number; + projectsCount: number; + discussionsCount: number; + forksCount: number; + issuesCount: number; + openIssuesCount: number; + closedIssuesCount: number; + openPullRequestsCount: number; + mergedPullRequestsCount: number; + licenseName: string; + watchersCount: number; + } + + interface Data { + [key: string]: RepoData; + } + + const value: Data; + export default value; +} From a03f750c391324a23d1a585670e9e64ccfbbe59c Mon Sep 17 00:00:00 2001 From: Ian Candy Date: Tue, 31 Oct 2023 18:32:10 +0000 Subject: [PATCH 02/12] Allow fetchers to export JSON files directly Instead of having each fetcher return a CSV string, this update allows each fetcher to create it's own data files directly. This should make things more open for extension in the future, because a fetcher doesn't need to be tied to a specific return value. Instead, it can format files directly however it likes (or append to multiple files if needed). Co-authored-by: Andrew Henry --- .../github/fetchers/repos_info_fetcher.go | 1 - .../business/core/collectors/github/github.go | 55 +++++++++---------- backend/business/core/core.go | 11 ++-- backend/cmd/main.go | 23 ++------ 4 files changed, 36 insertions(+), 54 deletions(-) diff --git a/backend/business/core/collectors/github/fetchers/repos_info_fetcher.go b/backend/business/core/collectors/github/fetchers/repos_info_fetcher.go index c6c2354..1facd66 100644 --- a/backend/business/core/collectors/github/fetchers/repos_info_fetcher.go +++ b/backend/business/core/collectors/github/fetchers/repos_info_fetcher.go @@ -98,7 +98,6 @@ func (f *ReposInfoFetcher) Fetch(ctx context.Context) (*map[string]RepoInfoResul for { err := f.client.Query(ctx, &q, variables) - fmt.Println(err) if err != nil { return nil, fmt.Errorf("failed to fetch repos info for %s: %s", f.orgName, err.Error()) diff --git a/backend/business/core/collectors/github/github.go b/backend/business/core/collectors/github/github.go index d9cec9d..87a4ed8 100644 --- a/backend/business/core/collectors/github/github.go +++ b/backend/business/core/collectors/github/github.go @@ -2,7 +2,9 @@ package github import ( "context" + "encoding/json" "fmt" + "os" "github.com/shurcooL/githubv4" "github.com/who-metrics/business/core/collectors/github/fetchers" @@ -10,7 +12,6 @@ import ( type GitHubCollector struct { fetchers Fetchers - // format string } func NewGitHubCollector(client *githubv4.Client, organizationName string) *GitHubCollector { @@ -27,7 +28,7 @@ type ResultOutput struct { Repositories map[string]RepositoryInfoResult `json:"repositories"` } -func (c *GitHubCollector) Collect(ctx context.Context) ([]string, []error) { +func (c *GitHubCollector) Collect(ctx context.Context) []error { errors := make([]error, 0) orgInfo, err := c.fetchers.OrgInfo.Fetch(ctx) if err != nil { @@ -42,12 +43,16 @@ func (c *GitHubCollector) Collect(ctx context.Context) ([]string, []error) { errors = append(errors, err) } - // Build the final result result := ResultOutput{ OrgInfo: orgInfo, Repositories: make(map[string]RepositoryInfoResult), } + if reposInfo == nil { + errors = append(errors, fmt.Errorf("no repository information found")) + return errors + } + for _, repo := range *reposInfo { result.Repositories[repo.RepoName] = RepositoryInfoResult{ RepoInfoResult: repo, @@ -55,26 +60,28 @@ func (c *GitHubCollector) Collect(ctx context.Context) ([]string, []error) { } } - fmt.Println(result) + jsonData, err := json.Marshal(result) + if err != nil { + errors = append(errors, err) + return errors + } - return []string{}, errors - // // Handle csv format - // if c.format == "csv" { - // // TODO: process with goroutines - // results, errors := make([]string, len(c.fetchers)), make([]error, 0) + file, err := os.Create("who-metrics-ui/src/data/data.json") + if err != nil { + errors = append(errors, err) + return errors + } + defer file.Close() - // for _, fetcher := range c.fetchers { - // result, err := fetcher.Fetch(ctx) - // if err != nil { - // errors = append(errors, err) - // } - // results = append(results, result) - // } - // return results, errors + _, err = file.Write(jsonData) + if err != nil { + errors = append(errors, err) + return errors + } - // } + fmt.Println("JSON data written to ", file.Name()) - // Handle json format + return errors } type Fetchers struct { @@ -84,21 +91,11 @@ type Fetchers struct { } func buildFetchers(client *githubv4.Client, organizationName string) Fetchers { - // TODO: - // load all variables from env or somewhere else - // it should look something like this: - /* - vars := GetVars() - return []Fetcher{ - &fetchers.OrgInfo{client: client, orgName: vars.orgName}, - } - */ fetchers := Fetchers{ OrgInfo: fetchers.NewOrgInfo(client, organizationName), ReposInfo: fetchers.NewReposInfoFetcher(client, organizationName), ContributionInfo: fetchers.NewContributionInfoFetcher(client, organizationName), } - // TODO: map the fetchers to keys so we can easily access them return fetchers } diff --git a/backend/business/core/core.go b/backend/business/core/core.go index dbc7d01..1e9210b 100644 --- a/backend/business/core/core.go +++ b/backend/business/core/core.go @@ -5,7 +5,7 @@ import ( ) type Collector interface { - Collect(ctx context.Context) ([]string, []error) + Collect(ctx context.Context) ([]error) } type Core struct { @@ -22,13 +22,12 @@ You would wonder why it's not called Collect. Its because Collect is a method of the Collector interface. And "Amass" is a synonym for "collect" (and is much cooler). */ -func (c *Core) Amass(ctx context.Context) ([]string, []error) { - results, errors := make([]string, len(c.collectors)), make([]error, 0) +func (c *Core) Amass(ctx context.Context) ([]error) { + errors := make([]error, 0) for _, collector := range c.collectors { - r, errs := collector.Collect(ctx) - results = append(results, r...) + errs := collector.Collect(ctx) errors = append(errors, errs...) } - return results, errors + return errors } diff --git a/backend/cmd/main.go b/backend/cmd/main.go index 486ba24..ef500e5 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -3,7 +3,6 @@ package main import ( "context" "flag" - "fmt" "log" "github.com/who-metrics/business/core" @@ -30,28 +29,16 @@ func run(format string) error { githubCollector := github.NewGitHubCollector(c, "sbv-world-health-org-metrics") core := core.New(githubCollector) - results, errors := core.Amass(context.Background()) + errors := core.Amass(context.Background()) // We should really retry on error, and preserve the data we've already collected // TODO: maybe fix this? if len(errors) > 0 { - // panic(errors) - } - - if (format == "csv") && (len(results) > 0) { - fmt.Println(results) - fmt.Println(errors) - return nil - } - - if (format == "json") && (len(results) > 0) { - output, err := helpers.FormatJSON(results) - if err != nil { - return err + // print every error in the errors array + for _, err := range errors { + log.Println(err) } - - fmt.Println(output) - return nil + panic(errors) } return nil From 1ad2bb7dd99d723bebe025f4b25772a11622e2f0 Mon Sep 17 00:00:00 2001 From: Ian Candy Date: Tue, 31 Oct 2023 18:33:49 +0000 Subject: [PATCH 03/12] Update nextjs workflow to build JSON files When we build the next site, we can automatically include the static data so that we don't need to check the data into the source code. This updates the workflow to build the JSON file before the rest of the site so it's included in the frontend build. Co-authored-by: Andrew Henry --- .github/workflows/nextjs.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.github/workflows/nextjs.yml b/.github/workflows/nextjs.yml index 8bfea1e..d314cd7 100644 --- a/.github/workflows/nextjs.yml +++ b/.github/workflows/nextjs.yml @@ -31,6 +31,26 @@ jobs: steps: - name: Checkout uses: actions/checkout@v3 + - name: Set up Go 1.x + uses: actions/setup-go@v4 + with: + go-version: ^1.19 + - name: Get dependencies + run: | + cd backend + go get -v -t -d ./... + if [ -f Gopkg.toml ]; then + curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh + dep ensure + fi + - name: Build Go + run: make build + # - name: Test + # run: make test + - name: Run Go program and save output + id: run + run: | + $PWD/backend/bin/metrics - name: Detect package manager id: detect-package-manager run: | From b19144bfe268188af0348669e089fe3a8ab5213d Mon Sep 17 00:00:00 2001 From: Ian Candy Date: Tue, 31 Oct 2023 18:36:55 +0000 Subject: [PATCH 04/12] Remove graphql workflow Because the build step now includes the data files, we don't need a separate action to do those. We can instead remove this and depend on the nextjs workflow to automatically load the data it needs. If we need CSV functionality, we can either 1) write an action/workflow to download a CSV for you or 2) add a CSV download button to the UI. Co-authored-by: Andrew Henry --- .github/workflows/graphql.yml | 79 ----------------------------------- README.md | 5 ++- 2 files changed, 3 insertions(+), 81 deletions(-) delete mode 100644 .github/workflows/graphql.yml diff --git a/.github/workflows/graphql.yml b/.github/workflows/graphql.yml deleted file mode 100644 index fdf8b99..0000000 --- a/.github/workflows/graphql.yml +++ /dev/null @@ -1,79 +0,0 @@ -# This workflow will do a: -# - clean build of a Go project -# - run the tests -# - execute the generated binary -# - saves the output to a file -# - saves the file in the repository - -name: Fetch metrics - -on: - # manually trigger the workflow - workflow_dispatch: - -# Sets permissions of the GITHUB_TOKEN to allow pushing to the repository -permissions: - contents: write - pull-requests: write - -env: - GH_TOKEN: ${{ github.token }} - BRANCH: output-file-${{ github.run_number }} - GRAPHQL_TOKEN: ${{ secrets.GRAPHQL_TOKEN }} - -jobs: - build: - name: Build - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - name: Set up Go 1.x - uses: actions/setup-go@v4 - with: - go-version: ^1.16 - - name: Get dependencies - run: | - cd backend - go get -v -t -d ./... - if [ -f Gopkg.toml ]; then - curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh - dep ensure - fi - - name: Build Go - run: make build - # - name: Test - # run: make test - - name: Run Go program and save output - id: run - run: | - $PWD/backend/bin/metrics > output.txt - echo "::set-output name=output::$(cat output.txt)" - - name: Upload output - # upload the output to the repository - uses: actions/upload-artifact@v3 - with: - name: output - path: output.txt - - - name: Save output to file - run: echo "${{ steps.run.outputs.output }}" > output.txt - - - name: Configure Git - run: | - git config user.name "Hassan Hawache" - git config user.email "hasan-dot@github.com" - git config credential.helper store - git config --global user.username "hasan-dot" - - name: Commit output file - run: | - git checkout -b $BRANCH - git add output.txt - git -c commit.gpgsign=false commit -m "Add output file" - git push origin $BRANCH - - name: Create Pull Request - run: | - gh pr create --base main --head $BRANCH --title "Output file PR" --body "Please review the output file changes." - - name: Merge Pull Request - run: | - gh pr merge --auto --delete-branch diff --git a/README.md b/README.md index c72a21f..ec71a57 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,8 @@ Issue Project [here](https://github.com/github/SI-skills-based-volunteering/iss ### Backend -Run the following command to run the action locally +Run the following command from the root of the repository ``` -gh act -W .github/workflows/graphql.yml --artifact-server-path ./tmp/ --env-file dev.vscode.env +make build +./backend/bin/metrics ``` From 071f36a898f3efdc283d4c8129928baa8a69f5af Mon Sep 17 00:00:00 2001 From: Ian Candy Date: Thu, 2 Nov 2023 00:34:28 +0000 Subject: [PATCH 05/12] Make org name dynamic Allows us to set the organization name for fetching data via an environemnt variable. Left a fallback in place so that this can be optional for now during development. Also removed the csv/json flag as we ended up not using that pattern. --- backend/cmd/main.go | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/backend/cmd/main.go b/backend/cmd/main.go index ef500e5..04a7c38 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -2,8 +2,8 @@ package main import ( "context" - "flag" "log" + "os" "github.com/who-metrics/business/core" "github.com/who-metrics/business/core/collectors/github" @@ -11,22 +11,20 @@ import ( ) func main() { - formatPtr := flag.String("format", "csv", "Format of the output file: csv or json") - - if (*formatPtr != "csv") && (*formatPtr != "json") { - log.Fatal("Invalid format flag") - } - - err := run(*formatPtr) + err := run() if err != nil { log.Println(err) } } -func run(format string) error { +func run() error { // GitHub GraphQL API v4 token. c := helpers.NewGHGraphQLClient() - githubCollector := github.NewGitHubCollector(c, "sbv-world-health-org-metrics") + org := os.Getenv("ORGANIZATION_NAME") + if org == "" { + org = "sbv-world-health-org-metrics" + } + githubCollector := github.NewGitHubCollector(c, org) core := core.New(githubCollector) errors := core.Amass(context.Background()) From 9aeb8c0d3eb9a247c0f921669fb3b472e5168fe5 Mon Sep 17 00:00:00 2001 From: Ian Candy Date: Thu, 2 Nov 2023 14:12:11 +0000 Subject: [PATCH 06/12] Clarify README instructions --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ec71a57..d685862 100644 --- a/README.md +++ b/README.md @@ -67,8 +67,14 @@ Issue Project [here](https://github.com/github/SI-skills-based-volunteering/iss ### Backend -Run the following command from the root of the repository + +To update the repository data. + +1. Generate a [new GitHub Token](https://github.com/settings/tokens) with the ability to read repo and projects. +2. Run the following command from the root of the repository ``` make build ./backend/bin/metrics ``` + +This will generate a new `data.json` file in the UI directory which can be imported directly as part of the static build. \ No newline at end of file From 5cf6b8482707fe39679accf154c6ec630a30795c Mon Sep 17 00:00:00 2001 From: Ian Candy Date: Thu, 2 Nov 2023 17:45:18 +0000 Subject: [PATCH 07/12] Clarify instructions around using the token --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e2eb3c7..ae09a59 100644 --- a/README.md +++ b/README.md @@ -76,8 +76,9 @@ Issue Project [here](https://github.com/github/SI-skills-based-volunteering/iss To update the repository data. -1. Generate a [new GitHub Token](https://github.com/settings/tokens) with the ability to read repo and projects. -2. Run the following command from the root of the repository +1. Generate a [new GitHub Token](https://github.com/settings/tokens) with the ability to read repo, read org, and read projects scopes. +1. Set the `GRAPHQL_TOKEN` environment variable to be the value of your newly created token. +1. Run the following command from the root of the repository: ``` make build ./backend/bin/metrics From 1bd8bc5eb947c402a5c2fe080e73008a487b0a53 Mon Sep 17 00:00:00 2001 From: Ian Candy Date: Thu, 2 Nov 2023 17:52:51 +0000 Subject: [PATCH 08/12] Update result functions - Remove formatCSV functions, which are now unused - Rename on instnace of `buildJSON` to `buildResult` for consistency Co-authored-by: David Gardiner --- .../github/fetchers/contribution_info.go | 16 ------------ .../collectors/github/fetchers/org_info.go | 16 ------------ .../github/fetchers/repos_info_fetcher.go | 26 ++----------------- 3 files changed, 2 insertions(+), 56 deletions(-) diff --git a/backend/business/core/collectors/github/fetchers/contribution_info.go b/backend/business/core/collectors/github/fetchers/contribution_info.go index 7a668b4..9464176 100644 --- a/backend/business/core/collectors/github/fetchers/contribution_info.go +++ b/backend/business/core/collectors/github/fetchers/contribution_info.go @@ -2,7 +2,6 @@ package fetchers import ( "context" - "fmt" "github.com/shurcooL/githubv4" ) @@ -65,21 +64,6 @@ func (c *ContributionInfoFetcher) Fetch(ctx context.Context) (*map[string]Contri return result, nil } -// FormatCSV formats the given data as CSV -func (f *ContributionInfoFetcher) FormatCSV(data []contributionInfo) (string, error) { - csvString := "repo_name,issues_opened\n" - for _, edge := range data { - csvString += fmt.Sprintf("%s\n", - edge.Node.NameWithOwner, - // edge.Node.OpenIssues.TotalCount, - // edge.Node.ClosedIssues.TotalCount, - // edge.Node.OpenPullRequests.TotalCount, - // edge.Node.MergedPullRequests.TotalCount, - ) - } - return csvString, nil -} - func (f *ContributionInfoFetcher) buildResult(data []contributionInfo) *map[string]ContributionInfoResult { result := make(map[string]ContributionInfoResult) for _, edge := range data { diff --git a/backend/business/core/collectors/github/fetchers/org_info.go b/backend/business/core/collectors/github/fetchers/org_info.go index 26a5a91..d7b74c5 100644 --- a/backend/business/core/collectors/github/fetchers/org_info.go +++ b/backend/business/core/collectors/github/fetchers/org_info.go @@ -2,7 +2,6 @@ package fetchers import ( "context" - "fmt" "github.com/shurcooL/githubv4" ) @@ -74,21 +73,6 @@ func (oif *OrgInfoFetcher) Fetch(ctx context.Context) (*OrgInfoResult, error) { return result, nil } -func (f *OrgInfoFetcher) formatCSV(data orgInfo) (string, error) { - csvString := "login,name,description,createdAt,totalMembers,totalProjects,totalRepositories,totalTeams\n" - csvString += fmt.Sprintf("%s,%s,%s,%s,%d,%d,%d,%d\n", - data.Login, - data.Name, - data.Description, - data.CreatedAt, - data.MembersWithRole.TotalCount, - data.Projects.TotalCount+data.ProjectsV2.TotalCount, - data.Repositories.TotalCount, - data.Teams.TotalCount, - ) - return csvString, nil -} - func (f *OrgInfoFetcher) buildResult(data orgInfo) *OrgInfoResult { return &OrgInfoResult{ Login: string(data.Login), diff --git a/backend/business/core/collectors/github/fetchers/repos_info_fetcher.go b/backend/business/core/collectors/github/fetchers/repos_info_fetcher.go index 1facd66..db57134 100644 --- a/backend/business/core/collectors/github/fetchers/repos_info_fetcher.go +++ b/backend/business/core/collectors/github/fetchers/repos_info_fetcher.go @@ -109,33 +109,11 @@ func (f *ReposInfoFetcher) Fetch(ctx context.Context) (*map[string]RepoInfoResul variables["reposCursor"] = githubv4.NewString(q.Organization.Repositories.PageInfo.EndCursor) } - result := f.buildJSON(allRepos) + result := f.buildResult(allRepos) return result, nil } -// FormatCSV formats the given data as CSV file -func (f *ReposInfoFetcher) FormatCSV(data []repoInfo) (string, error) { - csvString := "repo_name,collaborators_count,projects_count,discussions_count,forks_count,issues_count,open_issues_count,closed_issues_count,open_pull_requests_count,merged_pull_requests_count,license_name,watchers_count\n" - for _, edge := range data { - csvString += fmt.Sprintf("%s,%d,%d,%d,%d,%d,%d,%d,%d,%d,%s,%d\n", - edge.Node.NameWithOwner, - edge.Node.Collaborators.TotalCount, - edge.Node.Projects.TotalCount+edge.Node.ProjectsV2.TotalCount, - edge.Node.Discussions.TotalCount, - edge.Node.Forks.TotalCount, - edge.Node.Issues.TotalCount, - edge.Node.OpenIssues.TotalCount, - edge.Node.ClosedIssues.TotalCount, - edge.Node.OpenPullRequests.TotalCount, - edge.Node.MergedPullRequests.TotalCount, - edge.Node.LicenseInfo.Name, - edge.Node.Watchers.TotalCount, - ) - } - return csvString, nil -} - -func (f *ReposInfoFetcher) buildJSON(data []repoInfo) *map[string]RepoInfoResult { +func (f *ReposInfoFetcher) buildResult(data []repoInfo) *map[string]RepoInfoResult { holder := make(map[string]RepoInfoResult) for _, edge := range data { holder[string(edge.Node.NameWithOwner)] = RepoInfoResult{ From 8a7875d42eb35a7a6660ba61db75987ac51ee0a6 Mon Sep 17 00:00:00 2001 From: Ian Candy Date: Thu, 2 Nov 2023 18:05:06 +0000 Subject: [PATCH 09/12] Add data.json to .gitignore file --- who-metrics-ui/.gitignore | 3 +++ who-metrics-ui/src/data/data.json | 30 ------------------------------ 2 files changed, 3 insertions(+), 30 deletions(-) delete mode 100644 who-metrics-ui/src/data/data.json diff --git a/who-metrics-ui/.gitignore b/who-metrics-ui/.gitignore index c87c9b3..95cd774 100644 --- a/who-metrics-ui/.gitignore +++ b/who-metrics-ui/.gitignore @@ -34,3 +34,6 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# Data files - these get generated at buildtime. The README includes instructions for generating these. +/src/data/data.json \ No newline at end of file diff --git a/who-metrics-ui/src/data/data.json b/who-metrics-ui/src/data/data.json deleted file mode 100644 index 3d92151..0000000 --- a/who-metrics-ui/src/data/data.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "github/github": { - "repoName": "github", - "collaboratorsCount": 10, - "projectsCount": 100, - "discussionsCount": 100, - "forksCount": 100, - "issuesCount": 100, - "openIssuesCount": 100, - "closedIssuesCount": 100, - "openPullRequestsCount": 100, - "mergedPullRequestsCount": 100, - "licenseName": "MIT", - "watchersCount": 100 - }, - "github/ospo": { - "repoName": "github", - "collaboratorsCount": 10, - "projectsCount": 100, - "discussionsCount": 100, - "forksCount": 100, - "issuesCount": 100, - "openIssuesCount": 100, - "closedIssuesCount": 100, - "openPullRequestsCount": 100, - "mergedPullRequestsCount": 100, - "licenseName": "MIT", - "watchersCount": 100 - } -} From c37f6296517953532a8a807b9d530159eef0d140 Mon Sep 17 00:00:00 2001 From: Ian Candy Date: Thu, 2 Nov 2023 19:12:32 +0000 Subject: [PATCH 10/12] Remove TODO comment in favor of an issue https://github.com/sbv-world-health-org-metrics/sbv-world-health-org-metrics/issues/66 contains the relevant information for adding this feature. --- backend/cmd/main.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/backend/cmd/main.go b/backend/cmd/main.go index 04a7c38..86f3472 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -29,8 +29,6 @@ func run() error { errors := core.Amass(context.Background()) - // We should really retry on error, and preserve the data we've already collected - // TODO: maybe fix this? if len(errors) > 0 { // print every error in the errors array for _, err := range errors { From 1609b0dd5d19846d4a8a64747ed9c7a9bbbcc8fc Mon Sep 17 00:00:00 2001 From: Ian Candy Date: Tue, 7 Nov 2023 15:24:45 +0000 Subject: [PATCH 11/12] Add ENV variable to workflow --- .github/workflows/nextjs.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/nextjs.yml b/.github/workflows/nextjs.yml index 78d75a4..2f85cf0 100644 --- a/.github/workflows/nextjs.yml +++ b/.github/workflows/nextjs.yml @@ -18,6 +18,9 @@ permissions: pages: write id-token: write +env: + GRAPHQL_TOKEN: ${{ secrets.GRAPHQL_TOKEN }} + # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. concurrency: From 99ad01fe0481dd0af41affeb1d9c035dc39b7597 Mon Sep 17 00:00:00 2001 From: Ian Candy Date: Tue, 7 Nov 2023 15:27:50 +0000 Subject: [PATCH 12/12] Add OrganizationName ENV variable to workflow --- .github/workflows/nextjs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/nextjs.yml b/.github/workflows/nextjs.yml index 2f85cf0..10c0384 100644 --- a/.github/workflows/nextjs.yml +++ b/.github/workflows/nextjs.yml @@ -20,7 +20,7 @@ permissions: env: GRAPHQL_TOKEN: ${{ secrets.GRAPHQL_TOKEN }} - + ORGANIZATION_NAME: WorldHealthOrganization # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. concurrency: