diff --git a/.gitignore b/.gitignore index 2a47455..1696cb5 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,7 @@ .vscode/ # Build artifact -./main +main + +# Test coverage artifact +coverage.out \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 3083ed7..6f11212 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -ARG GOLANG_VERSION=1.25.3 +ARG GOLANG_VERSION=1.25.7 FROM --platform=$BUILDPLATFORM golang:${GOLANG_VERSION} AS build LABEL maintainer="githubexporter" diff --git a/Makefile b/Makefile index 0fd54f6..62a0217 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,18 @@ -.PHONY: install test build +.PHONY: install test build test-coverage coverage-report coverage install: @go mod download +build: + @go build -o main + test: @go test -v -race ./... -build: - @go build -o main +test-coverage: + @go test -v -race -coverprofile=coverage.out -coverpkg=./... ./... + +coverage-report: + @go tool cover -html=coverage.out + +coverage: test-coverage coverage-report \ No newline at end of file diff --git a/README.md b/README.md index 4e2d9d4..b1f72a3 100644 --- a/README.md +++ b/README.md @@ -6,23 +6,29 @@ Exposes basic metrics for your repositories from the GitHub API, to a Prometheus This exporter is configured via environment variables. All variables are optional unless otherwise stated. Below is a list of supported configuration values: -| Variable | Description | Default | -|------------------------------|------------------------------------------------------------------------------------|--------------------------| -| `ORGS` | Comma-separated list of GitHub organizations to monitor (e.g. `org1,org2`). | | -| `REPOS` | Comma-separated list of repositories to monitor (e.g. `user/repo1,user/repo2`). | | -| `USERS` | Comma-separated list of GitHub users to monitor (e.g. `user1,user2`). | | -| `GITHUB_TOKEN` | GitHub personal access token for API authentication. | | -| `GITHUB_TOKEN_FILE` | Path to a file containing a GitHub personal access token. | | -| `GITHUB_APP` | Set to `true` to authenticate as a GitHub App. | `false` | -| `GITHUB_APP_ID` | The App ID of the GitHub App. Required if `GITHUB_APP` is `true`. | | -| `GITHUB_APP_INSTALLATION_ID` | The Installation ID of the GitHub App. Required if `GITHUB_APP` is `true`. | | -| `GITHUB_APP_KEY_PATH` | Path to the GitHub App private key file. Required if `GITHUB_APP` is `true`. | | -| `GITHUB_RATE_LIMIT_ENABLED` | Whether to fetch GitHub API rate limit metrics (`true` or `false`). | `true` | -| `GITHUB_RESULTS_PER_PAGE` | Number of results to request per page from the GitHub API (max 100). | `100` | -| `API_URL` | GitHub API URL. You should not need to change this unless using GitHub Enterprise. | `https://api.github.com` | -| `LISTEN_PORT` | The port the exporter will listen on. | `9171` | -| `METRICS_PATH` | The HTTP path to expose Prometheus metrics. | `/metrics` | -| `LOG_LEVEL` | Logging level (`debug`, `info`, `warn`, `error`). | `info` | +| Variable | Description | Default | +|-------------------------------|------------------------------------------------------------------------------------|--------------------------| +| `ORGS` | Comma-separated list of GitHub organizations to monitor (e.g. `org1,org2`). | | +| `REPOS` | Comma-separated list of repositories to monitor (e.g. `user/repo1,user/repo2`). | | +| `USERS` | Comma-separated list of GitHub users to monitor (e.g. `user1,user2`). | | +| `GITHUB_TOKEN` | GitHub personal access token for API authentication. | | +| `GITHUB_TOKEN_FILE` | Path to a file containing a GitHub personal access token. | | +| `GITHUB_APP` | Set to `true` to authenticate as a GitHub App. | `false` | +| `GITHUB_APP_ID` | The App ID of the GitHub App. Required if `GITHUB_APP` is `true`. | | +| `GITHUB_APP_INSTALLATION_ID` | The Installation ID of the GitHub App. Required if `GITHUB_APP` is `true`. | | +| `GITHUB_APP_KEY_PATH` | Path to the GitHub App private key file. Required if `GITHUB_APP` is `true`. | | +| `GITHUB_RATE_LIMIT_ENABLED` | Whether to fetch GitHub API rate limit metrics (`true` or `false`). | `true` | +| `GITHUB_RATE_LIMIT` | Core API quota threshold for proactive GitHub App token refresh. When the remaining `core` requests drop below this value, a new installation token is requested. `0` disables this behaviour. | `0` | +| `GITHUB_RESULTS_PER_PAGE` | Number of results to request per page from the GitHub API (max 100). | `100` | +| `FETCH_REPO_RELEASES_ENABLED` | Whether to fetch repository release metrics (`true` or `false`). | `true` | +| `FETCH_ORGS_CONCURRENCY` | Number of concurrent requests to make when fetching organization data. | `1` | +| `FETCH_ORG_REPOS_CONCURRENCY` | Number of concurrent requests to make when fetching organization repository data. | `1` | +| `FETCH_USERS_CONCURRENCY` | Number of concurrent requests to make when fetching user data. | `1` | +| `FETCH_USERS_CONCURRENCY` | Number of concurrent requests to make when fetching repository data. | `1` | +| `API_URL` | GitHub API URL. You should not need to change this unless using GitHub Enterprise. | `https://api.github.com` | +| `LISTEN_PORT` | The port the exporter will listen on. | `9171` | +| `METRICS_PATH` | The HTTP path to expose Prometheus metrics. | `/metrics` | +| `LOG_LEVEL` | Logging level (`debug`, `info`, `warn`, `error`). | `info` | ### Credential Precedence diff --git a/VERSION b/VERSION index 81ef58f..a6316f0 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v2.0.1 \ No newline at end of file +v2.3.0 \ No newline at end of file diff --git a/config/config.go b/config/config.go index 08050d2..b218910 100644 --- a/config/config.go +++ b/config/config.go @@ -16,19 +16,25 @@ import ( // Config struct holds runtime configuration required for the application type Config struct { - MetricsPath string `envconfig:"METRICS_PATH" required:"false" default:"/metrics"` - ListenPort string `envconfig:"LISTEN_PORT" required:"false" default:"9171"` - LogLevel string `envconfig:"LOG_LEVEL" required:"false" default:"INFO"` - ApiUrl *url.URL `envconfig:"API_URL" required:"false" default:"https://api.github.com"` - Repositories []string `envconfig:"REPOS" required:"false"` - Organisations []string `envconfig:"ORGS" required:"false"` - Users []string `envconfig:"USERS" required:"false"` - GitHubResultsPerPage int `envconfig:"GITHUB_RESULTS_PER_PAGE" required:"false" default:"100"` - GithubToken string `envconfig:"GITHUB_TOKEN" required:"false"` - GithubTokenFile string `envconfig:"GITHUB_TOKEN_FILE" required:"false"` - GitHubApp bool `envconfig:"GITHUB_APP" required:"false" default:"false"` - GitHubRateLimitEnabled bool `envconfig:"GITHUB_RATE_LIMIT_ENABLED" required:"false" default:"true"` - *GitHubAppConfig `ignored:"true"` + MetricsPath string `envconfig:"METRICS_PATH" required:"false" default:"/metrics"` + ListenPort string `envconfig:"LISTEN_PORT" required:"false" default:"9171"` + LogLevel string `envconfig:"LOG_LEVEL" required:"false" default:"INFO"` + ApiUrl *url.URL `envconfig:"API_URL" required:"false" default:"https://api.github.com"` + Repositories []string `envconfig:"REPOS" required:"false"` + Organisations []string `envconfig:"ORGS" required:"false"` + Users []string `envconfig:"USERS" required:"false"` + GitHubResultsPerPage int `envconfig:"GITHUB_RESULTS_PER_PAGE" required:"false" default:"100"` + GithubToken string `envconfig:"GITHUB_TOKEN" required:"false"` + GithubTokenFile string `envconfig:"GITHUB_TOKEN_FILE" required:"false"` + GitHubApp bool `envconfig:"GITHUB_APP" required:"false" default:"false"` + GitHubRateLimitEnabled bool `envconfig:"GITHUB_RATE_LIMIT_ENABLED" required:"false" default:"true"` + GitHubRateLimit float64 `envconfig:"GITHUB_RATE_LIMIT" required:"false" default:"0"` + FetchRepoReleasesEnabled bool `envconfig:"FETCH_REPO_RELEASES_ENABLED" required:"false" default:"true"` + FetchOrgsConcurrency int `envconfig:"FETCH_ORGS_CONCURRENCY" required:"false" default:"1"` + FetchOrgReposConcurrency int `envconfig:"FETCH_ORG_REPOS_CONCURRENCY" required:"false" default:"1"` + FetchReposConcurrency int `envconfig:"FETCH_REPOS_CONCURRENCY" required:"false" default:"1"` + FetchUsersConcurrency int `envconfig:"FETCH_USERS_CONCURRENCY" required:"false" default:"1"` + *GitHubAppConfig `ignored:"true"` } type GitHubAppConfig struct { @@ -83,7 +89,7 @@ func (c *Config) GetClient() (*github.Client, error) { // Add custom transport for GitHub App authentication if enabled if c.GitHubApp { - itr, err := ghinstallation.NewKeyFromFile(http.DefaultTransport, c.GitHubAppId, c.GitHubAppInstallationId, c.GitHubAppKeyPath) + itr, err := ghinstallation.NewKeyFromFile(transport, c.GitHubAppId, c.GitHubAppInstallationId, c.GitHubAppKeyPath) if err != nil { return nil, fmt.Errorf("creating GitHub App installation transport: %v", err) } @@ -96,6 +102,11 @@ func (c *Config) GetClient() (*github.Client, error) { client := github.NewClient(paginator) + if c.ApiUrl != nil && c.ApiUrl.String() != "https://api.github.com" { + // Don't need to validate error as it's checked in envconfig + client, _ = client.WithEnterpriseURLs(c.ApiUrl.String(), c.ApiUrl.String()) + } + if c.GithubToken != "" { client = client.WithAuthToken(c.GithubToken) } diff --git a/config/config_test.go b/config/config_test.go index 3072798..20a2aae 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -25,31 +25,42 @@ func TestConfig(t *testing.T) { Scheme: "https", Host: "api.github.com", }, - Repositories: []string{}, - Organisations: []string{}, - Users: []string{}, - GitHubResultsPerPage: 100, - GithubToken: "", - GithubTokenFile: "", - GitHubApp: false, - GitHubAppConfig: nil, - GitHubRateLimitEnabled: true, + Repositories: []string{}, + Organisations: []string{}, + Users: []string{}, + GitHubResultsPerPage: 100, + GithubToken: "", + GithubTokenFile: "", + GitHubApp: false, + GitHubAppConfig: nil, + GitHubRateLimitEnabled: true, + GitHubRateLimit: 0, + FetchRepoReleasesEnabled: true, + FetchOrgsConcurrency: 1, + FetchOrgReposConcurrency: 1, + FetchReposConcurrency: 1, + FetchUsersConcurrency: 1, }, expectedErr: nil, }, { name: "non-default config", envVars: map[string]string{ - "METRICS_PATH": "/otherendpoint", - "LISTEN_PORT": "1111", - "LOG_LEVEL": "DEBUG", - "API_URL": "https://example.com", - "REPOS": "repo1, repo2", - "ORGS": "org1,org2 ", - "USERS": " user1, user2 ", - "GITHUB_RESULTS_PER_PAGE": "50", - "GITHUB_TOKEN": "token", - "GITHUB_RATE_LIMIT_ENABLED": "false", + "METRICS_PATH": "/otherendpoint", + "LISTEN_PORT": "1111", + "LOG_LEVEL": "DEBUG", + "API_URL": "https://example.com", + "REPOS": "repo1, repo2", + "ORGS": "org1,org2 ", + "USERS": " user1, user2 ", + "GITHUB_RESULTS_PER_PAGE": "50", + "GITHUB_TOKEN": "token", + "GITHUB_RATE_LIMIT_ENABLED": "false", + "FETCH_REPO_RELEASES_ENABLED": "false", + "FETCH_ORGS_CONCURRENCY": "2", + "FETCH_ORG_REPOS_CONCURRENCY": "3", + "FETCH_REPOS_CONCURRENCY": "4", + "FETCH_USERS_CONCURRENCY": "5", }, expectedCfg: &Config{ MetricsPath: "/otherendpoint", @@ -71,12 +82,49 @@ func TestConfig(t *testing.T) { "user1", "user2", }, - GitHubResultsPerPage: 50, - GithubToken: "token", - GithubTokenFile: "", - GitHubApp: false, - GitHubAppConfig: nil, - GitHubRateLimitEnabled: false, + GitHubResultsPerPage: 50, + GithubToken: "token", + GithubTokenFile: "", + GitHubApp: false, + GitHubAppConfig: nil, + GitHubRateLimitEnabled: false, + GitHubRateLimit: 0, + FetchRepoReleasesEnabled: false, + FetchOrgsConcurrency: 2, + FetchOrgReposConcurrency: 3, + FetchReposConcurrency: 4, + FetchUsersConcurrency: 5, + }, + expectedErr: nil, + }, + { + name: "github rate limit threshold config", + envVars: map[string]string{ + "GITHUB_RATE_LIMIT": "15000", + }, + expectedCfg: &Config{ + MetricsPath: "/metrics", + ListenPort: "9171", + LogLevel: "INFO", + ApiUrl: &url.URL{ + Scheme: "https", + Host: "api.github.com", + }, + Repositories: []string{}, + Organisations: []string{}, + Users: []string{}, + GitHubResultsPerPage: 100, + GithubToken: "", + GithubTokenFile: "", + GitHubApp: false, + GitHubAppConfig: nil, + GitHubRateLimitEnabled: true, + GitHubRateLimit: 15000, + FetchRepoReleasesEnabled: true, + FetchOrgsConcurrency: 1, + FetchOrgReposConcurrency: 1, + FetchReposConcurrency: 1, + FetchUsersConcurrency: 1, }, expectedErr: nil, }, @@ -127,3 +175,56 @@ func TestConfig(t *testing.T) { }) } } + +func TestGetClient(t *testing.T) { + tests := []struct { + name string + envVars map[string]string + expectedHost string + expectedPath string + expectedErr error + }{ + { + name: "default config", + expectedHost: "api.github.com", + expectedPath: "/", + expectedErr: nil, + }, + { + name: "non-default config", + envVars: map[string]string{ + "METRICS_PATH": "/otherendpoint", + "LISTEN_PORT": "1111", + "LOG_LEVEL": "DEBUG", + "API_URL": "https://example.com", + "REPOS": "repo1, repo2", + "ORGS": "org1,org2 ", + "USERS": " user1, user2 ", + "GITHUB_RESULTS_PER_PAGE": "50", + "GITHUB_TOKEN": "token", + "GITHUB_RATE_LIMIT_ENABLED": "false", + "FETCH_REPO_RELEASES_ENABLED": "false", + "FETCH_ORGS_CONCURRENCY": "2", + "FETCH_ORG_REPOS_CONCURRENCY": "3", + "FETCH_REPOS_CONCURRENCY": "4", + "FETCH_USERS_CONCURRENCY": "5", + }, + expectedHost: "example.com", + expectedPath: "/api/v3/", + expectedErr: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + for k, v := range tt.envVars { + t.Setenv(k, v) + } + + cfg, _ := Init() + client, _ := cfg.GetClient() + + assert.Equal(t, client.BaseURL.Host, tt.expectedHost) + assert.Equal(t, client.BaseURL.Path, tt.expectedPath) + }) + } +} diff --git a/exporter/metrics.go b/exporter/metrics.go index a886d46..590df9d 100644 --- a/exporter/metrics.go +++ b/exporter/metrics.go @@ -87,9 +87,16 @@ func AddMetrics(cfg *config.Config) map[string]*prometheus.Desc { // processMetrics - processes the response data and sets the metrics using it as a source func (e *Exporter) processMetrics(data []*Datum, rates *[]RateLimit, ch chan<- prometheus.Metric) error { + processed := make(map[string]struct{}) - // APIMetrics - range through the data slice for _, x := range data { + // Check if we've already processed this repo + repo := fmt.Sprintf("%s/%s", x.Owner.Login, x.Name) + if _, ok := processed[repo]; ok { + continue + } + processed[repo] = struct{}{} + ch <- prometheus.MustNewConstMetric(e.APIMetrics["Stars"], prometheus.GaugeValue, float64(x.Stars), x.Name, x.Owner.Login, strconv.FormatBool(x.Private), strconv.FormatBool(x.Fork), strconv.FormatBool(x.Archived), x.License.Key, x.Language) ch <- prometheus.MustNewConstMetric(e.APIMetrics["Forks"], prometheus.GaugeValue, float64(x.Forks), x.Name, x.Owner.Login, strconv.FormatBool(x.Private), strconv.FormatBool(x.Fork), strconv.FormatBool(x.Archived), x.License.Key, x.Language) ch <- prometheus.MustNewConstMetric(e.APIMetrics["Watchers"], prometheus.GaugeValue, float64(x.Watchers), x.Name, x.Owner.Login, strconv.FormatBool(x.Private), strconv.FormatBool(x.Fork), strconv.FormatBool(x.Archived), x.License.Key, x.Language) diff --git a/exporter/prometheus.go b/exporter/prometheus.go index a124775..b322a1a 100644 --- a/exporter/prometheus.go +++ b/exporter/prometheus.go @@ -9,6 +9,7 @@ import ( "github.com/google/go-github/v76/github" "github.com/prometheus/client_golang/prometheus" log "github.com/sirupsen/logrus" + "golang.org/x/sync/errgroup" ) // Describe - loops through the API metrics and passes them to prometheus.Describe @@ -56,6 +57,9 @@ func (e *Exporter) Collect(ch chan<- prometheus.Metric) { return } + // Refresh client if rate limit threshold enabled + e.rateLimitRefresh(r) + // Set prometheus gauge metrics using the data gathered err = e.processMetrics(data, r, ch) if err != nil { @@ -68,6 +72,7 @@ func (e *Exporter) getRateLimits(ctx context.Context) (*[]RateLimit, error) { return nil, nil } + log.Debug("fetching rate limits") rates, _, err := e.Client.RateLimit.Get(ctx) if err != nil { return nil, fmt.Errorf("fetching rate limits: %w", err) @@ -105,30 +110,66 @@ func (e *Exporter) getRateLimits(ctx context.Context) (*[]RateLimit, error) { return &rls, nil } +func (e *Exporter) rateLimitRefresh(rates *[]RateLimit) { + if e.GitHubRateLimit <= 0 || rates == nil { + return + } + + for _, rl := range *rates { + if rl.Resource == "core" && rl.Remaining <= e.GitHubRateLimit { + log.Infof("GitHub API core rate limit is low (%.0f remaining, threshold: %.0f)", rl.Remaining, e.GitHubRateLimit) + if !e.GitHubApp { + log.Warn("GITHUB_RATE_LIMIT threshold breached but GitHub App auth is not configured; cannot refresh token automatically") + return + } + + log.Info("Refreshing GitHub App installation token due to low rate limit") + newClient, err := e.GetClient() + if err != nil { + log.Errorf("refreshing GitHub App client after low rate limit: %v", err) + return + } + + e.Client = newClient + return + } + } +} + // getRepoMetrics fetches metrics for the configured repositories func (e *Exporter) getRepoMetrics(ctx context.Context) ([]*Datum, error) { var data []*Datum + + gRepos, rCtx := errgroup.WithContext(ctx) + gRepos.SetLimit(e.FetchReposConcurrency) + for _, m := range e.Repositories { - // Split the repository string into owner and name - parts := strings.Split(m, "/") - if len(parts) != 2 { - log.Errorf("Invalid repository format: %s", m) - continue - } + gRepos.Go(func() error { + // Split the repository string into owner and name + parts := strings.Split(m, "/") + if len(parts) != 2 { + return fmt.Errorf("invalid repository format: %s", m) + } - repo, _, err := e.Client.Repositories.Get(ctx, parts[0], parts[1]) - if err != nil { - log.Errorf("Error fetching repository data: %v", err) - continue - } + log.Debugf("Fetching data for repository: %s", m) + repo, _, err := e.Client.Repositories.Get(rCtx, parts[0], parts[1]) + if err != nil { + return fmt.Errorf("fetching repository data: %v", err) + } - d, err := e.parseRepo(ctx, *repo) - if err != nil { - log.Errorf("Error parsing repository data: %v", err) - continue - } + d, err := e.parseRepo(ctx, *repo) + if err != nil { + return fmt.Errorf("parsing repository data: %v", err) + } - data = append(data, d) + data = append(data, d) + return nil + }) + } + + err := gRepos.Wait() + if err != nil { + return nil, fmt.Errorf("processing repositories: %v", err) } return data, nil @@ -137,86 +178,124 @@ func (e *Exporter) getRepoMetrics(ctx context.Context) ([]*Datum, error) { // getUserMetrics fetches metrics for the configured users func (e *Exporter) getUserMetrics(ctx context.Context) ([]*Datum, error) { var data []*Datum - for _, m := range e.Users { - repos, _, err := e.Client.Repositories.ListByUser(ctx, m, nil) - if err != nil { - log.Errorf("Error fetching user data: %v", err) - continue - } - for _, repo := range repos { - d, err := e.parseRepo(ctx, *repo) + gUsers, uCtx := errgroup.WithContext(ctx) + gUsers.SetLimit(e.FetchUsersConcurrency) + + for _, m := range e.Users { + gUsers.Go(func() error { + log.Debugf("Fetching data for user: %s", m) + repos, _, err := e.Client.Repositories.ListByUser(uCtx, m, nil) if err != nil { - log.Errorf("Error parsing user repository data: %v", err) - continue + return fmt.Errorf("fetching user data: %v", err) } - data = append(data, d) - } + for _, repo := range repos { + d, err := e.parseRepo(ctx, *repo) + if err != nil { + return fmt.Errorf("parsing user repository data: %v", err) + } + data = append(data, d) + } + return nil + }) } + + err := gUsers.Wait() + if err != nil { + return nil, fmt.Errorf("processing users: %v", err) + } + return data, nil } // getOrgMetrics fetches metrics for the configured organisations func (e *Exporter) getOrgMetrics(ctx context.Context) ([]*Datum, error) { var data []*Datum + + gOrgs, oCtx := errgroup.WithContext(ctx) + gOrgs.SetLimit(e.FetchOrgsConcurrency) + for _, m := range e.Organisations { - repos, _, err := e.Client.Repositories.ListByOrg(ctx, m, nil) - if err != nil { - log.Errorf("Error fetching organisation data: %v", err) - continue - } + gOrgs.Go(func() error { + log.Debugf("Fetching data for organisation: %s", m) + repos, _, err := e.Client.Repositories.ListByOrg(oCtx, m, nil) + if err != nil { + return fmt.Errorf("fetching organisation data: %v", err) + } - for _, repo := range repos { - d, err := e.parseRepo(ctx, *repo) + gRepos, rCtx := errgroup.WithContext(ctx) + gRepos.SetLimit(e.FetchOrgReposConcurrency) + + for _, repo := range repos { + gRepos.Go(func() error { + d, err := e.parseRepo(rCtx, *repo) + if err != nil { + return fmt.Errorf("parsing organisation repository data: %w", err) + } + + data = append(data, d) + return nil + }) + } + + err = gRepos.Wait() if err != nil { - log.Errorf("Error parsing organisation repository data: %v", err) - continue + return fmt.Errorf("processing organisation repositories: %v", err) } + return nil + }) + } - data = append(data, d) - } + err := gOrgs.Wait() + if err != nil { + return nil, fmt.Errorf("processing organisations: %v", err) } return data, nil } func (e *Exporter) parseRepo(ctx context.Context, repo github.Repository) (*Datum, error) { + var releases []Release + var pulls []Pull + repoOwner := repo.GetOwner().GetLogin() repoName := repo.GetName() - rel, _, err := e.Client.Repositories.ListReleases(ctx, repoOwner, repoName, nil) - if err != nil { - return nil, fmt.Errorf("listing releases: %w", err) - } + if e.FetchRepoReleasesEnabled { + log.Debugf("Fetching releases for repository: %s/%s", repoOwner, repoName) + rel, _, err := e.Client.Repositories.ListReleases(ctx, repoOwner, repoName, nil) + if err != nil { + return nil, fmt.Errorf("listing releases: %w", err) + } - var releases []Release - for _, release := range rel { - var assets []Asset - for _, asset := range release.Assets { - a := Asset{ - Name: asset.GetName(), - Size: asset.GetSize(), - Downloads: asset.GetDownloadCount(), - CreatedAt: asset.GetCreatedAt().Format(time.RFC3339), + for _, release := range rel { + var assets []Asset + for _, asset := range release.Assets { + a := Asset{ + Name: asset.GetName(), + Size: asset.GetSize(), + Downloads: asset.GetDownloadCount(), + CreatedAt: asset.GetCreatedAt().Format(time.RFC3339), + } + assets = append(assets, a) } - assets = append(assets, a) - } - r := Release{ - Name: release.GetName(), - Tag: release.GetTagName(), - Assets: assets, + r := Release{ + Name: release.GetName(), + Tag: release.GetTagName(), + Assets: assets, + } + releases = append(releases, r) } - releases = append(releases, r) } + log.Debugf("Fetching pull requests for repository: %s/%s", repoOwner, repoName) pullRequests, _, err := e.Client.PullRequests.List(ctx, repoOwner, repoName, nil) if err != nil { return nil, fmt.Errorf("fetching pull requests: %w", err) } - var pulls []Pull for _, pr := range pullRequests { p := Pull{ Url: pr.GetURL(), diff --git a/exporter/prometheus_test.go b/exporter/prometheus_test.go new file mode 100644 index 0000000..dc00b03 --- /dev/null +++ b/exporter/prometheus_test.go @@ -0,0 +1,193 @@ +package exporter + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "os" + "testing" + + "github.com/githubexporter/github-exporter/config" + "github.com/google/go-github/v76/github" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// writeTempRSAKey generates a 2048-bit RSA private key and writes it as a +// PKCS#1 PEM file to a temp path. The file is removed automatically when the +// test finishes. +func writeTempRSAKey(t *testing.T) string { + t.Helper() + key, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + + f, err := os.CreateTemp("", "test-github-app-key-*.pem") + require.NoError(t, err) + defer f.Close() + + err = pem.Encode(f, &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(key), + }) + require.NoError(t, err) + + t.Cleanup(func() { os.Remove(f.Name()) }) + + return f.Name() +} + +func newTestExporter(cfg config.Config) *Exporter { + return &Exporter{ + Client: github.NewClient(nil), + Config: cfg, + } +} + +func TestRateLimitRefresh(t *testing.T) { + // Shared rate-limit slices used across sub-tests. + aboveThreshold := []RateLimit{{Resource: "core", Remaining: 200, Limit: 5000}} + atThreshold := []RateLimit{{Resource: "core", Remaining: 100, Limit: 5000}} + belowThreshold := []RateLimit{{Resource: "core", Remaining: 50, Limit: 5000}} + noCoreEntry := []RateLimit{{Resource: "search", Remaining: 5, Limit: 30}} + + baseCfg := config.Config{ + GitHubResultsPerPage: 100, + FetchReposConcurrency: 1, + FetchOrgsConcurrency: 1, + FetchOrgReposConcurrency: 1, + FetchUsersConcurrency: 1, + } + + t.Run("no-op when GitHubRateLimit is zero", func(t *testing.T) { + cfg := baseCfg + cfg.GitHubRateLimit = 0 + e := newTestExporter(cfg) + original := e.Client + + e.rateLimitRefresh(&atThreshold) + + assert.Same(t, original, e.Client) + }) + + t.Run("no-op when GitHubRateLimit is negative", func(t *testing.T) { + cfg := baseCfg + cfg.GitHubRateLimit = -1 + e := newTestExporter(cfg) + original := e.Client + + e.rateLimitRefresh(&atThreshold) + + assert.Same(t, original, e.Client) + }) + + t.Run("no-op when rates is nil", func(t *testing.T) { + cfg := baseCfg + cfg.GitHubRateLimit = 100 + e := newTestExporter(cfg) + original := e.Client + + e.rateLimitRefresh(nil) + + assert.Same(t, original, e.Client) + }) + + t.Run("no-op when core remaining is above threshold", func(t *testing.T) { + cfg := baseCfg + cfg.GitHubRateLimit = 100 // remaining=200 > threshold=100 + e := newTestExporter(cfg) + original := e.Client + + e.rateLimitRefresh(&aboveThreshold) + + assert.Same(t, original, e.Client) + }) + + t.Run("no-op when rates has no core entry", func(t *testing.T) { + cfg := baseCfg + cfg.GitHubRateLimit = 100 + e := newTestExporter(cfg) + original := e.Client + + e.rateLimitRefresh(&noCoreEntry) + + assert.Same(t, original, e.Client) + }) + + t.Run("no-op when GitHubApp is false and core is at threshold", func(t *testing.T) { + cfg := baseCfg + cfg.GitHubRateLimit = 100 // remaining=100 <= threshold=100 + cfg.GitHubApp = false + e := newTestExporter(cfg) + original := e.Client + + e.rateLimitRefresh(&atThreshold) + + assert.Same(t, original, e.Client) + }) + + t.Run("no-op when GitHubApp is false and core is below threshold", func(t *testing.T) { + cfg := baseCfg + cfg.GitHubRateLimit = 100 // remaining=50 < threshold=100 + cfg.GitHubApp = false + e := newTestExporter(cfg) + original := e.Client + + e.rateLimitRefresh(&belowThreshold) + + assert.Same(t, original, e.Client) + }) + + t.Run("no-op when GitHubApp is true but GetClient fails", func(t *testing.T) { + cfg := baseCfg + cfg.GitHubRateLimit = 100 + cfg.GitHubApp = true + cfg.GitHubAppConfig = &config.GitHubAppConfig{ + GitHubAppKeyPath: "/nonexistent/key.pem", + GitHubAppId: 1, + GitHubAppInstallationId: 1, + } + e := newTestExporter(cfg) + original := e.Client + + e.rateLimitRefresh(&atThreshold) + + assert.Same(t, original, e.Client) + }) + + t.Run("replaces client when GitHubApp is true and core is at threshold", func(t *testing.T) { + keyPath := writeTempRSAKey(t) + cfg := baseCfg + cfg.GitHubRateLimit = 100 // remaining=100 <= threshold=100 + cfg.GitHubApp = true + cfg.GitHubAppConfig = &config.GitHubAppConfig{ + GitHubAppKeyPath: keyPath, + GitHubAppId: 1, + GitHubAppInstallationId: 1, + } + e := newTestExporter(cfg) + original := e.Client + + e.rateLimitRefresh(&atThreshold) + + assert.NotSame(t, original, e.Client, "expected client to be replaced after rate limit refresh") + }) + + t.Run("replaces client when GitHubApp is true and core is below threshold", func(t *testing.T) { + keyPath := writeTempRSAKey(t) + cfg := baseCfg + cfg.GitHubRateLimit = 100 // remaining=50 < threshold=100 + cfg.GitHubApp = true + cfg.GitHubAppConfig = &config.GitHubAppConfig{ + GitHubAppKeyPath: keyPath, + GitHubAppId: 1, + GitHubAppInstallationId: 1, + } + e := newTestExporter(cfg) + original := e.Client + + e.rateLimitRefresh(&belowThreshold) + + assert.NotSame(t, original, e.Client, "expected client to be replaced after rate limit refresh") + }) +} diff --git a/go.mod b/go.mod index ea67358..19c4984 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,7 @@ module github.com/githubexporter/github-exporter go 1.24.0 -toolchain go1.25.3 +toolchain go1.25.7 require ( github.com/bradleyfalzon/ghinstallation/v2 v2.17.0 @@ -13,6 +13,7 @@ require ( github.com/sirupsen/logrus v1.9.3 github.com/steinfletcher/apitest v1.3.8 github.com/stretchr/testify v1.11.1 + golang.org/x/sync v0.15.0 ) require ( diff --git a/go.sum b/go.sum index 3940cae..c401789 100644 --- a/go.sum +++ b/go.sum @@ -58,6 +58,8 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= diff --git a/test/github_exporter_test.go b/test/github_exporter_test.go index ad55001..d540da1 100644 --- a/test/github_exporter_test.go +++ b/test/github_exporter_test.go @@ -18,7 +18,7 @@ import ( ) func TestHomepage(t *testing.T) { - test, collector := apiTest(withConfig("a/b")) + test, collector := apiTest(withConfig()) defer prometheus.Unregister(&collector) test.Get("/"). @@ -29,14 +29,20 @@ func TestHomepage(t *testing.T) { } func TestGithubExporter(t *testing.T) { - test, collector := apiTest(withConfig("myOrg/myRepo")) + test, collector := apiTest(withConfig()) defer prometheus.Unregister(&collector) test.Mocks( githubRepos(), - githubRateLimit(), githubReleases(), githubPulls(), + githubUserRepos(), + githubUserReleases(), + githubUserPulls(), + githubOrgRepos(), + githubReleases(), + githubPulls(), + githubRateLimit(), ). Get("/metrics"). Expect(t). @@ -65,12 +71,22 @@ func TestGithubExporter(t *testing.T) { Assert(bodyContains(`github_repo_release_downloads{created_at="2019-02-28T08:25:53Z",name="myRepo_1.3.0_windows_amd64.tar.gz",release="1.3.0",repo="myRepo",tag="1.3.0",user="myOrg"} 21`)). Assert(bodyContains(`github_repo_release_downloads{created_at="2019-05-02T15:22:16Z",name="myRepo_2.0.0_checksums.txt",release="2.0.0",repo="myRepo",tag="2.0.0",user="myOrg"} 14564`)). Assert(bodyContains(`github_repo_release_downloads{created_at="2019-05-02T15:22:16Z",name="myRepo_2.0.0_windows_amd64.tar.gz",release="2.0.0",repo="myRepo",tag="2.0.0",user="myOrg"} 55`)). + Assert(bodyContains(`github_repo_forks{archived="false",fork="false",language="Go",license="mit",private="false",repo="myRepo",user="myUser"} 10`)). + Assert(bodyContains(`github_repo_pull_request_count{repo="myRepo",user="myUser"} 3`)). + Assert(bodyContains(`github_repo_open_issues{archived="false",fork="false",language="Go",license="mit",private="false",repo="myRepo",user="myUser"} 2`)). + Assert(bodyContains(`github_repo_size_kb{archived="false",fork="false",language="Go",license="mit",private="false",repo="myRepo",user="myUser"} 946`)). + Assert(bodyContains(`github_repo_stars{archived="false",fork="false",language="Go",license="mit",private="false",repo="myRepo",user="myUser"} 120`)). + Assert(bodyContains(`github_repo_watchers{archived="false",fork="false",language="Go",license="mit",private="false",repo="myRepo",user="myUser"} 5`)). + Assert(bodyContains(`github_repo_release_downloads{created_at="2019-02-28T08:25:53Z",name="myRepo_1.3.0_checksums.txt",release="1.3.0",repo="myRepo",tag="1.3.0",user="myUser"} 7292`)). + Assert(bodyContains(`github_repo_release_downloads{created_at="2019-02-28T08:25:53Z",name="myRepo_1.3.0_windows_amd64.tar.gz",release="1.3.0",repo="myRepo",tag="1.3.0",user="myUser"} 21`)). + Assert(bodyContains(`github_repo_release_downloads{created_at="2019-05-02T15:22:16Z",name="myRepo_2.0.0_checksums.txt",release="2.0.0",repo="myRepo",tag="2.0.0",user="myUser"} 14564`)). + Assert(bodyContains(`github_repo_release_downloads{created_at="2019-05-02T15:22:16Z",name="myRepo_2.0.0_windows_amd64.tar.gz",release="2.0.0",repo="myRepo",tag="2.0.0",user="myUser"} 55`)). Status(http.StatusOK). End() } func TestGithubExporterHttpErrorHandling(t *testing.T) { - test, collector := apiTest(withConfig("myOrg/myRepo")) + test, collector := apiTest(withConfig()) defer prometheus.Unregister(&collector) // Test that the exporter returns when an error occurs @@ -80,7 +96,6 @@ func TestGithubExporterHttpErrorHandling(t *testing.T) { githubRepos(), githubReleases(), githubPullsError(), - githubRateLimit(), ). Get("/metrics"). Expect(t). @@ -101,8 +116,11 @@ func apiTest(conf config.Config) (*apitest.APITest, exporter.Exporter) { Handler(server.Handler), exp } -func withConfig(repos string) config.Config { - _ = os.Setenv("REPOS", repos) +func withConfig() config.Config { + _ = os.Setenv("REPOS", "myOrg/myRepo") + _ = os.Setenv("ORGS", "myOrg") + _ = os.Setenv("USERS", "myUser") + _ = os.Setenv("GITHUB_TOKEN", "12345") cfg, err := config.Init() if err != nil { @@ -121,6 +139,46 @@ func githubRepos() *apitest.Mock { End() } +func githubUserRepos() *apitest.Mock { + return apitest.NewMock(). + Get("https://api.github.com/users/myUser/repos"). + RespondWith(). + Times(1). + Body(readFile("testdata/user_repos_response.json")). + Status(200). + End() +} + +func githubUserReleases() *apitest.Mock { + return apitest.NewMock(). + Get("https://api.github.com/repos/myUser/myRepo/releases"). + RespondWith(). + Times(1). + Body(readFile("testdata/user_releases_response.json")). + Status(200). + End() +} + +func githubUserPulls() *apitest.Mock { + return apitest.NewMock(). + Get("https://api.github.com/repos/myUser/myRepo/pulls"). + RespondWith(). + Times(1). + Body(readFile("testdata/user_pulls_response.json")). + Status(http.StatusOK). + End() +} + +func githubOrgRepos() *apitest.Mock { + return apitest.NewMock(). + Get("https://api.github.com/orgs/myOrg/repos"). + RespondWith(). + Times(1). + Body(readFile("testdata/org_repos_response.json")). + Status(200). + End() +} + func githubRateLimit() *apitest.Mock { return apitest.NewMock(). Get("https://api.github.com/rate_limit"). diff --git a/test/testdata/org_repos_response.json b/test/testdata/org_repos_response.json new file mode 100644 index 0000000..e568077 --- /dev/null +++ b/test/testdata/org_repos_response.json @@ -0,0 +1,104 @@ +[ + { + "id": 163222413, + "node_id": "MDEwOlJlcG9zaXRvcnkxNjMyMjI0MTM=", + "name": "myRepo", + "full_name": "myOrg/myRepo", + "private": false, + "owner": { + "login": "myOrg", + "id": 1219157, + "node_id": "MDQ6VXNlcjEyMTkxNTc=", + "avatar_url": "https://avatars1.githubusercontent.com/u/1219157?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/myOrg", + "html_url": "https://github.com/myOrg", + "followers_url": "https://api.github.com/users/myOrg/followers", + "following_url": "https://api.github.com/users/myOrg/following{/other_user}", + "gists_url": "https://api.github.com/users/myOrg/gists{/gist_id}", + "starred_url": "https://api.github.com/users/myOrg/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/myOrg/subscriptions", + "organizations_url": "https://api.github.com/users/myOrg/orgs", + "repos_url": "https://api.github.com/users/myOrg/repos", + "events_url": "https://api.github.com/users/myOrg/events{/privacy}", + "received_events_url": "https://api.github.com/users/myOrg/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://github.com/myOrg/myRepo", + "description": "A simple and extensible behavioural testing library written in golang. You can use api test to simplify REST API, HTTP handler and e2e tests.", + "fork": false, + "url": "https://api.github.com/repos/myOrg/myRepo", + "forks_url": "https://api.github.com/repos/myOrg/myRepo/forks", + "keys_url": "https://api.github.com/repos/myOrg/myRepo/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/myOrg/myRepo/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/myOrg/myRepo/teams", + "hooks_url": "https://api.github.com/repos/myOrg/myRepo/hooks", + "issue_events_url": "https://api.github.com/repos/myOrg/myRepo/issues/events{/number}", + "events_url": "https://api.github.com/repos/myOrg/myRepo/events", + "assignees_url": "https://api.github.com/repos/myOrg/myRepo/assignees{/user}", + "branches_url": "https://api.github.com/repos/myOrg/myRepo/branches{/branch}", + "tags_url": "https://api.github.com/repos/myOrg/myRepo/tags", + "blobs_url": "https://api.github.com/repos/myOrg/myRepo/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/myOrg/myRepo/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/myOrg/myRepo/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/myOrg/myRepo/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/myOrg/myRepo/statuses/{sha}", + "languages_url": "https://api.github.com/repos/myOrg/myRepo/languages", + "stargazers_url": "https://api.github.com/repos/myOrg/myRepo/stargazers", + "contributors_url": "https://api.github.com/repos/myOrg/myRepo/contributors", + "subscribers_url": "https://api.github.com/repos/myOrg/myRepo/subscribers", + "subscription_url": "https://api.github.com/repos/myOrg/myRepo/subscription", + "commits_url": "https://api.github.com/repos/myOrg/myRepo/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/myOrg/myRepo/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/myOrg/myRepo/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/myOrg/myRepo/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/myOrg/myRepo/contents/{+path}", + "compare_url": "https://api.github.com/repos/myOrg/myRepo/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/myOrg/myRepo/merges", + "archive_url": "https://api.github.com/repos/myOrg/myRepo/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/myOrg/myRepo/downloads", + "issues_url": "https://api.github.com/repos/myOrg/myRepo/issues{/number}", + "pulls_url": "https://api.github.com/repos/myOrg/myRepo/pulls{/number}", + "milestones_url": "https://api.github.com/repos/myOrg/myRepo/milestones{/number}", + "notifications_url": "https://api.github.com/repos/myOrg/myRepo/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/myOrg/myRepo/labels{/name}", + "releases_url": "https://api.github.com/repos/myOrg/myRepo/releases{/id}", + "deployments_url": "https://api.github.com/repos/myOrg/myRepo/deployments", + "created_at": "2018-12-26T22:27:19Z", + "updated_at": "2019-08-22T20:25:14Z", + "pushed_at": "2019-08-22T20:25:16Z", + "git_url": "git://github.com/myOrg/myRepo.git", + "ssh_url": "git@github.com:myOrg/myRepo.git", + "clone_url": "https://github.com/myOrg/myRepo.git", + "svn_url": "https://github.com/myOrg/myRepo", + "homepage": "https://myRepo.dev", + "size": 946, + "stargazers_count": 120, + "watchers_count": 120, + "language": "Go", + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": false, + "has_pages": false, + "forks_count": 10, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 5, + "license": { + "key": "mit", + "name": "MIT License", + "spdx_id": "MIT", + "url": "https://api.github.com/licenses/mit", + "node_id": "MDc6TGljZW5zZTEz" + }, + "forks": 10, + "open_issues": 5, + "watchers": 120, + "default_branch": "master", + "network_count": 10, + "subscribers_count": 5 + } +] \ No newline at end of file diff --git a/test/testdata/user_pulls_response.json b/test/testdata/user_pulls_response.json new file mode 100644 index 0000000..ec0b129 --- /dev/null +++ b/test/testdata/user_pulls_response.json @@ -0,0 +1,986 @@ +[ + { + "url": "https://api.github.com/repos/infinityworks/github-exporter/pulls/46", + "id": 389150323, + "node_id": "MDExOlB1bGxSZXF1ZXN0Mzg5MTUwMzIz", + "html_url": "https://github.com/infinityworks/github-exporter/pull/46", + "diff_url": "https://github.com/infinityworks/github-exporter/pull/46.diff", + "patch_url": "https://github.com/infinityworks/github-exporter/pull/46.patch", + "issue_url": "https://api.github.com/repos/infinityworks/github-exporter/issues/46", + "number": 46, + "state": "open", + "locked": false, + "title": "`github_repo_open_issues` and `github_repo_pull_request_count` separately", + "user": { + "login": "iamrare", + "id": 12945919, + "node_id": "MDQ6VXNlcjEyOTQ1OTE5", + "avatar_url": "https://avatars3.githubusercontent.com/u/12945919?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/iamrare", + "html_url": "https://github.com/iamrare", + "followers_url": "https://api.github.com/users/iamrare/followers", + "following_url": "https://api.github.com/users/iamrare/following{/other_user}", + "gists_url": "https://api.github.com/users/iamrare/gists{/gist_id}", + "starred_url": "https://api.github.com/users/iamrare/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/iamrare/subscriptions", + "organizations_url": "https://api.github.com/users/iamrare/orgs", + "repos_url": "https://api.github.com/users/iamrare/repos", + "events_url": "https://api.github.com/users/iamrare/events{/privacy}", + "received_events_url": "https://api.github.com/users/iamrare/received_events", + "type": "User", + "site_admin": false + }, + "body": "we needed `github_repo_open_issues` and `github_repo_pull_request_count` separately, so I added it\r\n\r\nplease consider merging it upstream in case other people need it\r\nif it's accepted upstream, we can use infinityworks' version rather than maintaining our own\r\n\r\nthanks", + "created_at": "2020-03-16T10:54:19Z", + "updated_at": "2020-03-16T10:55:43Z", + "closed_at": null, + "merged_at": null, + "merge_commit_sha": "29b53b804fffec2aff40ad0475856236565ed72a", + "assignee": null, + "assignees": [], + "requested_reviewers": [], + "requested_teams": [], + "labels": [], + "milestone": null, + "draft": false, + "commits_url": "https://api.github.com/repos/infinityworks/github-exporter/pulls/46/commits", + "review_comments_url": "https://api.github.com/repos/infinityworks/github-exporter/pulls/46/comments", + "review_comment_url": "https://api.github.com/repos/infinityworks/github-exporter/pulls/comments{/number}", + "comments_url": "https://api.github.com/repos/infinityworks/github-exporter/issues/46/comments", + "statuses_url": "https://api.github.com/repos/infinityworks/github-exporter/statuses/560314d1abfbdca21b530039fc9187f96776e419", + "head": { + "label": "iamrare:master", + "ref": "master", + "sha": "560314d1abfbdca21b530039fc9187f96776e419", + "user": { + "login": "iamrare", + "id": 12945919, + "node_id": "MDQ6VXNlcjEyOTQ1OTE5", + "avatar_url": "https://avatars3.githubusercontent.com/u/12945919?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/iamrare", + "html_url": "https://github.com/iamrare", + "followers_url": "https://api.github.com/users/iamrare/followers", + "following_url": "https://api.github.com/users/iamrare/following{/other_user}", + "gists_url": "https://api.github.com/users/iamrare/gists{/gist_id}", + "starred_url": "https://api.github.com/users/iamrare/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/iamrare/subscriptions", + "organizations_url": "https://api.github.com/users/iamrare/orgs", + "repos_url": "https://api.github.com/users/iamrare/repos", + "events_url": "https://api.github.com/users/iamrare/events{/privacy}", + "received_events_url": "https://api.github.com/users/iamrare/received_events", + "type": "User", + "site_admin": false + }, + "repo": { + "id": 247032154, + "node_id": "MDEwOlJlcG9zaXRvcnkyNDcwMzIxNTQ=", + "name": "github-exporter", + "full_name": "iamrare/github-exporter", + "private": false, + "owner": { + "login": "iamrare", + "id": 12945919, + "node_id": "MDQ6VXNlcjEyOTQ1OTE5", + "avatar_url": "https://avatars3.githubusercontent.com/u/12945919?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/iamrare", + "html_url": "https://github.com/iamrare", + "followers_url": "https://api.github.com/users/iamrare/followers", + "following_url": "https://api.github.com/users/iamrare/following{/other_user}", + "gists_url": "https://api.github.com/users/iamrare/gists{/gist_id}", + "starred_url": "https://api.github.com/users/iamrare/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/iamrare/subscriptions", + "organizations_url": "https://api.github.com/users/iamrare/orgs", + "repos_url": "https://api.github.com/users/iamrare/repos", + "events_url": "https://api.github.com/users/iamrare/events{/privacy}", + "received_events_url": "https://api.github.com/users/iamrare/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://github.com/iamrare/github-exporter", + "description": ":octocat: Prometheus exporter for github metrics", + "fork": true, + "url": "https://api.github.com/repos/iamrare/github-exporter", + "forks_url": "https://api.github.com/repos/iamrare/github-exporter/forks", + "keys_url": "https://api.github.com/repos/iamrare/github-exporter/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/iamrare/github-exporter/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/iamrare/github-exporter/teams", + "hooks_url": "https://api.github.com/repos/iamrare/github-exporter/hooks", + "issue_events_url": "https://api.github.com/repos/iamrare/github-exporter/issues/events{/number}", + "events_url": "https://api.github.com/repos/iamrare/github-exporter/events", + "assignees_url": "https://api.github.com/repos/iamrare/github-exporter/assignees{/user}", + "branches_url": "https://api.github.com/repos/iamrare/github-exporter/branches{/branch}", + "tags_url": "https://api.github.com/repos/iamrare/github-exporter/tags", + "blobs_url": "https://api.github.com/repos/iamrare/github-exporter/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/iamrare/github-exporter/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/iamrare/github-exporter/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/iamrare/github-exporter/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/iamrare/github-exporter/statuses/{sha}", + "languages_url": "https://api.github.com/repos/iamrare/github-exporter/languages", + "stargazers_url": "https://api.github.com/repos/iamrare/github-exporter/stargazers", + "contributors_url": "https://api.github.com/repos/iamrare/github-exporter/contributors", + "subscribers_url": "https://api.github.com/repos/iamrare/github-exporter/subscribers", + "subscription_url": "https://api.github.com/repos/iamrare/github-exporter/subscription", + "commits_url": "https://api.github.com/repos/iamrare/github-exporter/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/iamrare/github-exporter/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/iamrare/github-exporter/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/iamrare/github-exporter/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/iamrare/github-exporter/contents/{+path}", + "compare_url": "https://api.github.com/repos/iamrare/github-exporter/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/iamrare/github-exporter/merges", + "archive_url": "https://api.github.com/repos/iamrare/github-exporter/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/iamrare/github-exporter/downloads", + "issues_url": "https://api.github.com/repos/iamrare/github-exporter/issues{/number}", + "pulls_url": "https://api.github.com/repos/iamrare/github-exporter/pulls{/number}", + "milestones_url": "https://api.github.com/repos/iamrare/github-exporter/milestones{/number}", + "notifications_url": "https://api.github.com/repos/iamrare/github-exporter/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/iamrare/github-exporter/labels{/name}", + "releases_url": "https://api.github.com/repos/iamrare/github-exporter/releases{/id}", + "deployments_url": "https://api.github.com/repos/iamrare/github-exporter/deployments", + "created_at": "2020-03-13T09:19:55Z", + "updated_at": "2020-03-16T10:35:24Z", + "pushed_at": "2020-03-16T10:35:26Z", + "git_url": "git://github.com/iamrare/github-exporter.git", + "ssh_url": "git@github.com:iamrare/github-exporter.git", + "clone_url": "https://github.com/iamrare/github-exporter.git", + "svn_url": "https://github.com/iamrare/github-exporter", + "homepage": "", + "size": 111, + "stargazers_count": 0, + "watchers_count": 0, + "language": "Go", + "has_issues": false, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "forks_count": 0, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 0, + "license": { + "key": "mit", + "name": "MIT License", + "spdx_id": "MIT", + "url": "https://api.github.com/licenses/mit", + "node_id": "MDc6TGljZW5zZTEz" + }, + "forks": 0, + "open_issues": 0, + "watchers": 0, + "default_branch": "master" + } + }, + "base": { + "label": "infinityworks:master", + "ref": "master", + "sha": "d94272fb546222c2127767a45757b65c5ee8be7b", + "user": { + "login": "infinityworks", + "id": 11678300, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjExNjc4MzAw", + "avatar_url": "https://avatars1.githubusercontent.com/u/11678300?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/infinityworks", + "html_url": "https://github.com/infinityworks", + "followers_url": "https://api.github.com/users/infinityworks/followers", + "following_url": "https://api.github.com/users/infinityworks/following{/other_user}", + "gists_url": "https://api.github.com/users/infinityworks/gists{/gist_id}", + "starred_url": "https://api.github.com/users/infinityworks/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/infinityworks/subscriptions", + "organizations_url": "https://api.github.com/users/infinityworks/orgs", + "repos_url": "https://api.github.com/users/infinityworks/repos", + "events_url": "https://api.github.com/users/infinityworks/events{/privacy}", + "received_events_url": "https://api.github.com/users/infinityworks/received_events", + "type": "Organization", + "site_admin": false + }, + "repo": { + "id": 67989490, + "node_id": "MDEwOlJlcG9zaXRvcnk2Nzk4OTQ5MA==", + "name": "github-exporter", + "full_name": "infinityworks/github-exporter", + "private": false, + "owner": { + "login": "infinityworks", + "id": 11678300, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjExNjc4MzAw", + "avatar_url": "https://avatars1.githubusercontent.com/u/11678300?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/infinityworks", + "html_url": "https://github.com/infinityworks", + "followers_url": "https://api.github.com/users/infinityworks/followers", + "following_url": "https://api.github.com/users/infinityworks/following{/other_user}", + "gists_url": "https://api.github.com/users/infinityworks/gists{/gist_id}", + "starred_url": "https://api.github.com/users/infinityworks/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/infinityworks/subscriptions", + "organizations_url": "https://api.github.com/users/infinityworks/orgs", + "repos_url": "https://api.github.com/users/infinityworks/repos", + "events_url": "https://api.github.com/users/infinityworks/events{/privacy}", + "received_events_url": "https://api.github.com/users/infinityworks/received_events", + "type": "Organization", + "site_admin": false + }, + "html_url": "https://github.com/infinityworks/github-exporter", + "description": ":octocat: Prometheus exporter for github metrics", + "fork": false, + "url": "https://api.github.com/repos/infinityworks/github-exporter", + "forks_url": "https://api.github.com/repos/infinityworks/github-exporter/forks", + "keys_url": "https://api.github.com/repos/infinityworks/github-exporter/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/infinityworks/github-exporter/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/infinityworks/github-exporter/teams", + "hooks_url": "https://api.github.com/repos/infinityworks/github-exporter/hooks", + "issue_events_url": "https://api.github.com/repos/infinityworks/github-exporter/issues/events{/number}", + "events_url": "https://api.github.com/repos/infinityworks/github-exporter/events", + "assignees_url": "https://api.github.com/repos/infinityworks/github-exporter/assignees{/user}", + "branches_url": "https://api.github.com/repos/infinityworks/github-exporter/branches{/branch}", + "tags_url": "https://api.github.com/repos/infinityworks/github-exporter/tags", + "blobs_url": "https://api.github.com/repos/infinityworks/github-exporter/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/infinityworks/github-exporter/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/infinityworks/github-exporter/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/infinityworks/github-exporter/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/infinityworks/github-exporter/statuses/{sha}", + "languages_url": "https://api.github.com/repos/infinityworks/github-exporter/languages", + "stargazers_url": "https://api.github.com/repos/infinityworks/github-exporter/stargazers", + "contributors_url": "https://api.github.com/repos/infinityworks/github-exporter/contributors", + "subscribers_url": "https://api.github.com/repos/infinityworks/github-exporter/subscribers", + "subscription_url": "https://api.github.com/repos/infinityworks/github-exporter/subscription", + "commits_url": "https://api.github.com/repos/infinityworks/github-exporter/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/infinityworks/github-exporter/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/infinityworks/github-exporter/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/infinityworks/github-exporter/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/infinityworks/github-exporter/contents/{+path}", + "compare_url": "https://api.github.com/repos/infinityworks/github-exporter/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/infinityworks/github-exporter/merges", + "archive_url": "https://api.github.com/repos/infinityworks/github-exporter/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/infinityworks/github-exporter/downloads", + "issues_url": "https://api.github.com/repos/infinityworks/github-exporter/issues{/number}", + "pulls_url": "https://api.github.com/repos/infinityworks/github-exporter/pulls{/number}", + "milestones_url": "https://api.github.com/repos/infinityworks/github-exporter/milestones{/number}", + "notifications_url": "https://api.github.com/repos/infinityworks/github-exporter/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/infinityworks/github-exporter/labels{/name}", + "releases_url": "https://api.github.com/repos/infinityworks/github-exporter/releases{/id}", + "deployments_url": "https://api.github.com/repos/infinityworks/github-exporter/deployments", + "created_at": "2016-09-12T08:12:46Z", + "updated_at": "2020-03-13T00:43:25Z", + "pushed_at": "2020-03-16T10:54:20Z", + "git_url": "git://github.com/infinityworks/github-exporter.git", + "ssh_url": "git@github.com:infinityworks/github-exporter.git", + "clone_url": "https://github.com/infinityworks/github-exporter.git", + "svn_url": "https://github.com/infinityworks/github-exporter", + "homepage": "", + "size": 115, + "stargazers_count": 170, + "watchers_count": 170, + "language": "Go", + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "forks_count": 47, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 11, + "license": { + "key": "mit", + "name": "MIT License", + "spdx_id": "MIT", + "url": "https://api.github.com/licenses/mit", + "node_id": "MDc6TGljZW5zZTEz" + }, + "forks": 47, + "open_issues": 11, + "watchers": 170, + "default_branch": "master" + } + }, + "_links": { + "self": { + "href": "https://api.github.com/repos/infinityworks/github-exporter/pulls/46" + }, + "html": { + "href": "https://github.com/infinityworks/github-exporter/pull/46" + }, + "issue": { + "href": "https://api.github.com/repos/infinityworks/github-exporter/issues/46" + }, + "comments": { + "href": "https://api.github.com/repos/infinityworks/github-exporter/issues/46/comments" + }, + "review_comments": { + "href": "https://api.github.com/repos/infinityworks/github-exporter/pulls/46/comments" + }, + "review_comment": { + "href": "https://api.github.com/repos/infinityworks/github-exporter/pulls/comments{/number}" + }, + "commits": { + "href": "https://api.github.com/repos/infinityworks/github-exporter/pulls/46/commits" + }, + "statuses": { + "href": "https://api.github.com/repos/infinityworks/github-exporter/statuses/560314d1abfbdca21b530039fc9187f96776e419" + } + }, + "author_association": "NONE" + }, + { + "url": "https://api.github.com/repos/infinityworks/github-exporter/pulls/45", + "id": 327215812, + "node_id": "MDExOlB1bGxSZXF1ZXN0MzI3MjE1ODEy", + "html_url": "https://github.com/infinityworks/github-exporter/pull/45", + "diff_url": "https://github.com/infinityworks/github-exporter/pull/45.diff", + "patch_url": "https://github.com/infinityworks/github-exporter/pull/45.patch", + "issue_url": "https://api.github.com/repos/infinityworks/github-exporter/issues/45", + "number": 45, + "state": "open", + "locked": false, + "title": "exporter", + "user": { + "login": "prabhakarms89", + "id": 45841583, + "node_id": "MDQ6VXNlcjQ1ODQxNTgz", + "avatar_url": "https://avatars1.githubusercontent.com/u/45841583?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/prabhakarms89", + "html_url": "https://github.com/prabhakarms89", + "followers_url": "https://api.github.com/users/prabhakarms89/followers", + "following_url": "https://api.github.com/users/prabhakarms89/following{/other_user}", + "gists_url": "https://api.github.com/users/prabhakarms89/gists{/gist_id}", + "starred_url": "https://api.github.com/users/prabhakarms89/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/prabhakarms89/subscriptions", + "organizations_url": "https://api.github.com/users/prabhakarms89/orgs", + "repos_url": "https://api.github.com/users/prabhakarms89/repos", + "events_url": "https://api.github.com/users/prabhakarms89/events{/privacy}", + "received_events_url": "https://api.github.com/users/prabhakarms89/received_events", + "type": "User", + "site_admin": false + }, + "body": "", + "created_at": "2019-10-11T14:01:37Z", + "updated_at": "2020-03-16T10:57:07Z", + "closed_at": null, + "merged_at": null, + "merge_commit_sha": "bbe1d8a6941216363adecb681050b1add697932e", + "assignee": null, + "assignees": [], + "requested_reviewers": [], + "requested_teams": [], + "labels": [], + "milestone": null, + "draft": false, + "commits_url": "https://api.github.com/repos/infinityworks/github-exporter/pulls/45/commits", + "review_comments_url": "https://api.github.com/repos/infinityworks/github-exporter/pulls/45/comments", + "review_comment_url": "https://api.github.com/repos/infinityworks/github-exporter/pulls/comments{/number}", + "comments_url": "https://api.github.com/repos/infinityworks/github-exporter/issues/45/comments", + "statuses_url": "https://api.github.com/repos/infinityworks/github-exporter/statuses/9deb171a4560839a61a239999e4125e9ba0f47a5", + "head": { + "label": "prabhakarms89:master", + "ref": "master", + "sha": "9deb171a4560839a61a239999e4125e9ba0f47a5", + "user": { + "login": "prabhakarms89", + "id": 45841583, + "node_id": "MDQ6VXNlcjQ1ODQxNTgz", + "avatar_url": "https://avatars1.githubusercontent.com/u/45841583?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/prabhakarms89", + "html_url": "https://github.com/prabhakarms89", + "followers_url": "https://api.github.com/users/prabhakarms89/followers", + "following_url": "https://api.github.com/users/prabhakarms89/following{/other_user}", + "gists_url": "https://api.github.com/users/prabhakarms89/gists{/gist_id}", + "starred_url": "https://api.github.com/users/prabhakarms89/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/prabhakarms89/subscriptions", + "organizations_url": "https://api.github.com/users/prabhakarms89/orgs", + "repos_url": "https://api.github.com/users/prabhakarms89/repos", + "events_url": "https://api.github.com/users/prabhakarms89/events{/privacy}", + "received_events_url": "https://api.github.com/users/prabhakarms89/received_events", + "type": "User", + "site_admin": false + }, + "repo": { + "id": 207864274, + "node_id": "MDEwOlJlcG9zaXRvcnkyMDc4NjQyNzQ=", + "name": "github-exporter", + "full_name": "prabhakarms89/github-exporter", + "private": false, + "owner": { + "login": "prabhakarms89", + "id": 45841583, + "node_id": "MDQ6VXNlcjQ1ODQxNTgz", + "avatar_url": "https://avatars1.githubusercontent.com/u/45841583?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/prabhakarms89", + "html_url": "https://github.com/prabhakarms89", + "followers_url": "https://api.github.com/users/prabhakarms89/followers", + "following_url": "https://api.github.com/users/prabhakarms89/following{/other_user}", + "gists_url": "https://api.github.com/users/prabhakarms89/gists{/gist_id}", + "starred_url": "https://api.github.com/users/prabhakarms89/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/prabhakarms89/subscriptions", + "organizations_url": "https://api.github.com/users/prabhakarms89/orgs", + "repos_url": "https://api.github.com/users/prabhakarms89/repos", + "events_url": "https://api.github.com/users/prabhakarms89/events{/privacy}", + "received_events_url": "https://api.github.com/users/prabhakarms89/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://github.com/prabhakarms89/github-exporter", + "description": ":octocat: Prometheus exporter for github metrics", + "fork": true, + "url": "https://api.github.com/repos/prabhakarms89/github-exporter", + "forks_url": "https://api.github.com/repos/prabhakarms89/github-exporter/forks", + "keys_url": "https://api.github.com/repos/prabhakarms89/github-exporter/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/prabhakarms89/github-exporter/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/prabhakarms89/github-exporter/teams", + "hooks_url": "https://api.github.com/repos/prabhakarms89/github-exporter/hooks", + "issue_events_url": "https://api.github.com/repos/prabhakarms89/github-exporter/issues/events{/number}", + "events_url": "https://api.github.com/repos/prabhakarms89/github-exporter/events", + "assignees_url": "https://api.github.com/repos/prabhakarms89/github-exporter/assignees{/user}", + "branches_url": "https://api.github.com/repos/prabhakarms89/github-exporter/branches{/branch}", + "tags_url": "https://api.github.com/repos/prabhakarms89/github-exporter/tags", + "blobs_url": "https://api.github.com/repos/prabhakarms89/github-exporter/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/prabhakarms89/github-exporter/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/prabhakarms89/github-exporter/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/prabhakarms89/github-exporter/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/prabhakarms89/github-exporter/statuses/{sha}", + "languages_url": "https://api.github.com/repos/prabhakarms89/github-exporter/languages", + "stargazers_url": "https://api.github.com/repos/prabhakarms89/github-exporter/stargazers", + "contributors_url": "https://api.github.com/repos/prabhakarms89/github-exporter/contributors", + "subscribers_url": "https://api.github.com/repos/prabhakarms89/github-exporter/subscribers", + "subscription_url": "https://api.github.com/repos/prabhakarms89/github-exporter/subscription", + "commits_url": "https://api.github.com/repos/prabhakarms89/github-exporter/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/prabhakarms89/github-exporter/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/prabhakarms89/github-exporter/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/prabhakarms89/github-exporter/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/prabhakarms89/github-exporter/contents/{+path}", + "compare_url": "https://api.github.com/repos/prabhakarms89/github-exporter/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/prabhakarms89/github-exporter/merges", + "archive_url": "https://api.github.com/repos/prabhakarms89/github-exporter/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/prabhakarms89/github-exporter/downloads", + "issues_url": "https://api.github.com/repos/prabhakarms89/github-exporter/issues{/number}", + "pulls_url": "https://api.github.com/repos/prabhakarms89/github-exporter/pulls{/number}", + "milestones_url": "https://api.github.com/repos/prabhakarms89/github-exporter/milestones{/number}", + "notifications_url": "https://api.github.com/repos/prabhakarms89/github-exporter/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/prabhakarms89/github-exporter/labels{/name}", + "releases_url": "https://api.github.com/repos/prabhakarms89/github-exporter/releases{/id}", + "deployments_url": "https://api.github.com/repos/prabhakarms89/github-exporter/deployments", + "created_at": "2019-09-11T17:04:25Z", + "updated_at": "2019-10-11T13:40:25Z", + "pushed_at": "2019-10-11T13:40:23Z", + "git_url": "git://github.com/prabhakarms89/github-exporter.git", + "ssh_url": "git@github.com:prabhakarms89/github-exporter.git", + "clone_url": "https://github.com/prabhakarms89/github-exporter.git", + "svn_url": "https://github.com/prabhakarms89/github-exporter", + "homepage": "", + "size": 120, + "stargazers_count": 1, + "watchers_count": 1, + "language": "Go", + "has_issues": false, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "forks_count": 1, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 0, + "license": { + "key": "mit", + "name": "MIT License", + "spdx_id": "MIT", + "url": "https://api.github.com/licenses/mit", + "node_id": "MDc6TGljZW5zZTEz" + }, + "forks": 1, + "open_issues": 0, + "watchers": 1, + "default_branch": "master" + } + }, + "base": { + "label": "infinityworks:master", + "ref": "master", + "sha": "d94272fb546222c2127767a45757b65c5ee8be7b", + "user": { + "login": "infinityworks", + "id": 11678300, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjExNjc4MzAw", + "avatar_url": "https://avatars1.githubusercontent.com/u/11678300?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/infinityworks", + "html_url": "https://github.com/infinityworks", + "followers_url": "https://api.github.com/users/infinityworks/followers", + "following_url": "https://api.github.com/users/infinityworks/following{/other_user}", + "gists_url": "https://api.github.com/users/infinityworks/gists{/gist_id}", + "starred_url": "https://api.github.com/users/infinityworks/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/infinityworks/subscriptions", + "organizations_url": "https://api.github.com/users/infinityworks/orgs", + "repos_url": "https://api.github.com/users/infinityworks/repos", + "events_url": "https://api.github.com/users/infinityworks/events{/privacy}", + "received_events_url": "https://api.github.com/users/infinityworks/received_events", + "type": "Organization", + "site_admin": false + }, + "repo": { + "id": 67989490, + "node_id": "MDEwOlJlcG9zaXRvcnk2Nzk4OTQ5MA==", + "name": "github-exporter", + "full_name": "infinityworks/github-exporter", + "private": false, + "owner": { + "login": "infinityworks", + "id": 11678300, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjExNjc4MzAw", + "avatar_url": "https://avatars1.githubusercontent.com/u/11678300?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/infinityworks", + "html_url": "https://github.com/infinityworks", + "followers_url": "https://api.github.com/users/infinityworks/followers", + "following_url": "https://api.github.com/users/infinityworks/following{/other_user}", + "gists_url": "https://api.github.com/users/infinityworks/gists{/gist_id}", + "starred_url": "https://api.github.com/users/infinityworks/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/infinityworks/subscriptions", + "organizations_url": "https://api.github.com/users/infinityworks/orgs", + "repos_url": "https://api.github.com/users/infinityworks/repos", + "events_url": "https://api.github.com/users/infinityworks/events{/privacy}", + "received_events_url": "https://api.github.com/users/infinityworks/received_events", + "type": "Organization", + "site_admin": false + }, + "html_url": "https://github.com/infinityworks/github-exporter", + "description": ":octocat: Prometheus exporter for github metrics", + "fork": false, + "url": "https://api.github.com/repos/infinityworks/github-exporter", + "forks_url": "https://api.github.com/repos/infinityworks/github-exporter/forks", + "keys_url": "https://api.github.com/repos/infinityworks/github-exporter/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/infinityworks/github-exporter/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/infinityworks/github-exporter/teams", + "hooks_url": "https://api.github.com/repos/infinityworks/github-exporter/hooks", + "issue_events_url": "https://api.github.com/repos/infinityworks/github-exporter/issues/events{/number}", + "events_url": "https://api.github.com/repos/infinityworks/github-exporter/events", + "assignees_url": "https://api.github.com/repos/infinityworks/github-exporter/assignees{/user}", + "branches_url": "https://api.github.com/repos/infinityworks/github-exporter/branches{/branch}", + "tags_url": "https://api.github.com/repos/infinityworks/github-exporter/tags", + "blobs_url": "https://api.github.com/repos/infinityworks/github-exporter/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/infinityworks/github-exporter/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/infinityworks/github-exporter/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/infinityworks/github-exporter/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/infinityworks/github-exporter/statuses/{sha}", + "languages_url": "https://api.github.com/repos/infinityworks/github-exporter/languages", + "stargazers_url": "https://api.github.com/repos/infinityworks/github-exporter/stargazers", + "contributors_url": "https://api.github.com/repos/infinityworks/github-exporter/contributors", + "subscribers_url": "https://api.github.com/repos/infinityworks/github-exporter/subscribers", + "subscription_url": "https://api.github.com/repos/infinityworks/github-exporter/subscription", + "commits_url": "https://api.github.com/repos/infinityworks/github-exporter/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/infinityworks/github-exporter/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/infinityworks/github-exporter/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/infinityworks/github-exporter/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/infinityworks/github-exporter/contents/{+path}", + "compare_url": "https://api.github.com/repos/infinityworks/github-exporter/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/infinityworks/github-exporter/merges", + "archive_url": "https://api.github.com/repos/infinityworks/github-exporter/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/infinityworks/github-exporter/downloads", + "issues_url": "https://api.github.com/repos/infinityworks/github-exporter/issues{/number}", + "pulls_url": "https://api.github.com/repos/infinityworks/github-exporter/pulls{/number}", + "milestones_url": "https://api.github.com/repos/infinityworks/github-exporter/milestones{/number}", + "notifications_url": "https://api.github.com/repos/infinityworks/github-exporter/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/infinityworks/github-exporter/labels{/name}", + "releases_url": "https://api.github.com/repos/infinityworks/github-exporter/releases{/id}", + "deployments_url": "https://api.github.com/repos/infinityworks/github-exporter/deployments", + "created_at": "2016-09-12T08:12:46Z", + "updated_at": "2020-03-13T00:43:25Z", + "pushed_at": "2020-03-16T10:54:20Z", + "git_url": "git://github.com/infinityworks/github-exporter.git", + "ssh_url": "git@github.com:infinityworks/github-exporter.git", + "clone_url": "https://github.com/infinityworks/github-exporter.git", + "svn_url": "https://github.com/infinityworks/github-exporter", + "homepage": "", + "size": 115, + "stargazers_count": 170, + "watchers_count": 170, + "language": "Go", + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "forks_count": 47, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 11, + "license": { + "key": "mit", + "name": "MIT License", + "spdx_id": "MIT", + "url": "https://api.github.com/licenses/mit", + "node_id": "MDc6TGljZW5zZTEz" + }, + "forks": 47, + "open_issues": 11, + "watchers": 170, + "default_branch": "master" + } + }, + "_links": { + "self": { + "href": "https://api.github.com/repos/infinityworks/github-exporter/pulls/45" + }, + "html": { + "href": "https://github.com/infinityworks/github-exporter/pull/45" + }, + "issue": { + "href": "https://api.github.com/repos/infinityworks/github-exporter/issues/45" + }, + "comments": { + "href": "https://api.github.com/repos/infinityworks/github-exporter/issues/45/comments" + }, + "review_comments": { + "href": "https://api.github.com/repos/infinityworks/github-exporter/pulls/45/comments" + }, + "review_comment": { + "href": "https://api.github.com/repos/infinityworks/github-exporter/pulls/comments{/number}" + }, + "commits": { + "href": "https://api.github.com/repos/infinityworks/github-exporter/pulls/45/commits" + }, + "statuses": { + "href": "https://api.github.com/repos/infinityworks/github-exporter/statuses/9deb171a4560839a61a239999e4125e9ba0f47a5" + } + }, + "author_association": "NONE" + }, + { + "url": "https://api.github.com/repos/infinityworks/github-exporter/pulls/39", + "id": 264246504, + "node_id": "MDExOlB1bGxSZXF1ZXN0MjY0MjQ2NTA0", + "html_url": "https://github.com/infinityworks/github-exporter/pull/39", + "diff_url": "https://github.com/infinityworks/github-exporter/pull/39.diff", + "patch_url": "https://github.com/infinityworks/github-exporter/pull/39.patch", + "issue_url": "https://api.github.com/repos/infinityworks/github-exporter/issues/39", + "number": 39, + "state": "open", + "locked": false, + "title": "Added two new metrics to collect all the past commit history accordin…", + "user": { + "login": "kpachhai", + "id": 2739378, + "node_id": "MDQ6VXNlcjI3MzkzNzg=", + "avatar_url": "https://avatars1.githubusercontent.com/u/2739378?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/kpachhai", + "html_url": "https://github.com/kpachhai", + "followers_url": "https://api.github.com/users/kpachhai/followers", + "following_url": "https://api.github.com/users/kpachhai/following{/other_user}", + "gists_url": "https://api.github.com/users/kpachhai/gists{/gist_id}", + "starred_url": "https://api.github.com/users/kpachhai/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/kpachhai/subscriptions", + "organizations_url": "https://api.github.com/users/kpachhai/orgs", + "repos_url": "https://api.github.com/users/kpachhai/repos", + "events_url": "https://api.github.com/users/kpachhai/events{/privacy}", + "received_events_url": "https://api.github.com/users/kpachhai/received_events", + "type": "User", + "site_admin": false + }, + "body": "…g to the branch specified", + "created_at": "2019-03-25T18:53:03Z", + "updated_at": "2019-03-25T20:30:43Z", + "closed_at": null, + "merged_at": null, + "merge_commit_sha": "4938102392c0e8cf336b6a8933d4b2b8b8403a79", + "assignee": null, + "assignees": [], + "requested_reviewers": [], + "requested_teams": [], + "labels": [], + "milestone": null, + "draft": false, + "commits_url": "https://api.github.com/repos/infinityworks/github-exporter/pulls/39/commits", + "review_comments_url": "https://api.github.com/repos/infinityworks/github-exporter/pulls/39/comments", + "review_comment_url": "https://api.github.com/repos/infinityworks/github-exporter/pulls/comments{/number}", + "comments_url": "https://api.github.com/repos/infinityworks/github-exporter/issues/39/comments", + "statuses_url": "https://api.github.com/repos/infinityworks/github-exporter/statuses/c136be8275b952f7b82412c3ae31b323fd1c3a06", + "head": { + "label": "kpachhai:master", + "ref": "master", + "sha": "c136be8275b952f7b82412c3ae31b323fd1c3a06", + "user": { + "login": "kpachhai", + "id": 2739378, + "node_id": "MDQ6VXNlcjI3MzkzNzg=", + "avatar_url": "https://avatars1.githubusercontent.com/u/2739378?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/kpachhai", + "html_url": "https://github.com/kpachhai", + "followers_url": "https://api.github.com/users/kpachhai/followers", + "following_url": "https://api.github.com/users/kpachhai/following{/other_user}", + "gists_url": "https://api.github.com/users/kpachhai/gists{/gist_id}", + "starred_url": "https://api.github.com/users/kpachhai/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/kpachhai/subscriptions", + "organizations_url": "https://api.github.com/users/kpachhai/orgs", + "repos_url": "https://api.github.com/users/kpachhai/repos", + "events_url": "https://api.github.com/users/kpachhai/events{/privacy}", + "received_events_url": "https://api.github.com/users/kpachhai/received_events", + "type": "User", + "site_admin": false + }, + "repo": { + "id": 177364106, + "node_id": "MDEwOlJlcG9zaXRvcnkxNzczNjQxMDY=", + "name": "github-exporter", + "full_name": "kpachhai/github-exporter", + "private": false, + "owner": { + "login": "kpachhai", + "id": 2739378, + "node_id": "MDQ6VXNlcjI3MzkzNzg=", + "avatar_url": "https://avatars1.githubusercontent.com/u/2739378?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/kpachhai", + "html_url": "https://github.com/kpachhai", + "followers_url": "https://api.github.com/users/kpachhai/followers", + "following_url": "https://api.github.com/users/kpachhai/following{/other_user}", + "gists_url": "https://api.github.com/users/kpachhai/gists{/gist_id}", + "starred_url": "https://api.github.com/users/kpachhai/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/kpachhai/subscriptions", + "organizations_url": "https://api.github.com/users/kpachhai/orgs", + "repos_url": "https://api.github.com/users/kpachhai/repos", + "events_url": "https://api.github.com/users/kpachhai/events{/privacy}", + "received_events_url": "https://api.github.com/users/kpachhai/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://github.com/kpachhai/github-exporter", + "description": "Prometheus exporter for github metrics", + "fork": true, + "url": "https://api.github.com/repos/kpachhai/github-exporter", + "forks_url": "https://api.github.com/repos/kpachhai/github-exporter/forks", + "keys_url": "https://api.github.com/repos/kpachhai/github-exporter/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/kpachhai/github-exporter/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/kpachhai/github-exporter/teams", + "hooks_url": "https://api.github.com/repos/kpachhai/github-exporter/hooks", + "issue_events_url": "https://api.github.com/repos/kpachhai/github-exporter/issues/events{/number}", + "events_url": "https://api.github.com/repos/kpachhai/github-exporter/events", + "assignees_url": "https://api.github.com/repos/kpachhai/github-exporter/assignees{/user}", + "branches_url": "https://api.github.com/repos/kpachhai/github-exporter/branches{/branch}", + "tags_url": "https://api.github.com/repos/kpachhai/github-exporter/tags", + "blobs_url": "https://api.github.com/repos/kpachhai/github-exporter/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/kpachhai/github-exporter/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/kpachhai/github-exporter/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/kpachhai/github-exporter/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/kpachhai/github-exporter/statuses/{sha}", + "languages_url": "https://api.github.com/repos/kpachhai/github-exporter/languages", + "stargazers_url": "https://api.github.com/repos/kpachhai/github-exporter/stargazers", + "contributors_url": "https://api.github.com/repos/kpachhai/github-exporter/contributors", + "subscribers_url": "https://api.github.com/repos/kpachhai/github-exporter/subscribers", + "subscription_url": "https://api.github.com/repos/kpachhai/github-exporter/subscription", + "commits_url": "https://api.github.com/repos/kpachhai/github-exporter/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/kpachhai/github-exporter/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/kpachhai/github-exporter/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/kpachhai/github-exporter/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/kpachhai/github-exporter/contents/{+path}", + "compare_url": "https://api.github.com/repos/kpachhai/github-exporter/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/kpachhai/github-exporter/merges", + "archive_url": "https://api.github.com/repos/kpachhai/github-exporter/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/kpachhai/github-exporter/downloads", + "issues_url": "https://api.github.com/repos/kpachhai/github-exporter/issues{/number}", + "pulls_url": "https://api.github.com/repos/kpachhai/github-exporter/pulls{/number}", + "milestones_url": "https://api.github.com/repos/kpachhai/github-exporter/milestones{/number}", + "notifications_url": "https://api.github.com/repos/kpachhai/github-exporter/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/kpachhai/github-exporter/labels{/name}", + "releases_url": "https://api.github.com/repos/kpachhai/github-exporter/releases{/id}", + "deployments_url": "https://api.github.com/repos/kpachhai/github-exporter/deployments", + "created_at": "2019-03-24T02:57:24Z", + "updated_at": "2019-03-25T20:30:45Z", + "pushed_at": "2019-03-25T20:30:42Z", + "git_url": "git://github.com/kpachhai/github-exporter.git", + "ssh_url": "git@github.com:kpachhai/github-exporter.git", + "clone_url": "https://github.com/kpachhai/github-exporter.git", + "svn_url": "https://github.com/kpachhai/github-exporter", + "homepage": null, + "size": 100, + "stargazers_count": 0, + "watchers_count": 0, + "language": "Go", + "has_issues": false, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "forks_count": 1, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 0, + "license": { + "key": "mit", + "name": "MIT License", + "spdx_id": "MIT", + "url": "https://api.github.com/licenses/mit", + "node_id": "MDc6TGljZW5zZTEz" + }, + "forks": 1, + "open_issues": 0, + "watchers": 0, + "default_branch": "master" + } + }, + "base": { + "label": "infinityworks:master", + "ref": "master", + "sha": "5111b3bef04b84322b19a7dbcf611b823153babc", + "user": { + "login": "infinityworks", + "id": 11678300, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjExNjc4MzAw", + "avatar_url": "https://avatars1.githubusercontent.com/u/11678300?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/infinityworks", + "html_url": "https://github.com/infinityworks", + "followers_url": "https://api.github.com/users/infinityworks/followers", + "following_url": "https://api.github.com/users/infinityworks/following{/other_user}", + "gists_url": "https://api.github.com/users/infinityworks/gists{/gist_id}", + "starred_url": "https://api.github.com/users/infinityworks/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/infinityworks/subscriptions", + "organizations_url": "https://api.github.com/users/infinityworks/orgs", + "repos_url": "https://api.github.com/users/infinityworks/repos", + "events_url": "https://api.github.com/users/infinityworks/events{/privacy}", + "received_events_url": "https://api.github.com/users/infinityworks/received_events", + "type": "Organization", + "site_admin": false + }, + "repo": { + "id": 67989490, + "node_id": "MDEwOlJlcG9zaXRvcnk2Nzk4OTQ5MA==", + "name": "github-exporter", + "full_name": "infinityworks/github-exporter", + "private": false, + "owner": { + "login": "infinityworks", + "id": 11678300, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjExNjc4MzAw", + "avatar_url": "https://avatars1.githubusercontent.com/u/11678300?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/infinityworks", + "html_url": "https://github.com/infinityworks", + "followers_url": "https://api.github.com/users/infinityworks/followers", + "following_url": "https://api.github.com/users/infinityworks/following{/other_user}", + "gists_url": "https://api.github.com/users/infinityworks/gists{/gist_id}", + "starred_url": "https://api.github.com/users/infinityworks/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/infinityworks/subscriptions", + "organizations_url": "https://api.github.com/users/infinityworks/orgs", + "repos_url": "https://api.github.com/users/infinityworks/repos", + "events_url": "https://api.github.com/users/infinityworks/events{/privacy}", + "received_events_url": "https://api.github.com/users/infinityworks/received_events", + "type": "Organization", + "site_admin": false + }, + "html_url": "https://github.com/infinityworks/github-exporter", + "description": ":octocat: Prometheus exporter for github metrics", + "fork": false, + "url": "https://api.github.com/repos/infinityworks/github-exporter", + "forks_url": "https://api.github.com/repos/infinityworks/github-exporter/forks", + "keys_url": "https://api.github.com/repos/infinityworks/github-exporter/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/infinityworks/github-exporter/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/infinityworks/github-exporter/teams", + "hooks_url": "https://api.github.com/repos/infinityworks/github-exporter/hooks", + "issue_events_url": "https://api.github.com/repos/infinityworks/github-exporter/issues/events{/number}", + "events_url": "https://api.github.com/repos/infinityworks/github-exporter/events", + "assignees_url": "https://api.github.com/repos/infinityworks/github-exporter/assignees{/user}", + "branches_url": "https://api.github.com/repos/infinityworks/github-exporter/branches{/branch}", + "tags_url": "https://api.github.com/repos/infinityworks/github-exporter/tags", + "blobs_url": "https://api.github.com/repos/infinityworks/github-exporter/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/infinityworks/github-exporter/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/infinityworks/github-exporter/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/infinityworks/github-exporter/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/infinityworks/github-exporter/statuses/{sha}", + "languages_url": "https://api.github.com/repos/infinityworks/github-exporter/languages", + "stargazers_url": "https://api.github.com/repos/infinityworks/github-exporter/stargazers", + "contributors_url": "https://api.github.com/repos/infinityworks/github-exporter/contributors", + "subscribers_url": "https://api.github.com/repos/infinityworks/github-exporter/subscribers", + "subscription_url": "https://api.github.com/repos/infinityworks/github-exporter/subscription", + "commits_url": "https://api.github.com/repos/infinityworks/github-exporter/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/infinityworks/github-exporter/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/infinityworks/github-exporter/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/infinityworks/github-exporter/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/infinityworks/github-exporter/contents/{+path}", + "compare_url": "https://api.github.com/repos/infinityworks/github-exporter/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/infinityworks/github-exporter/merges", + "archive_url": "https://api.github.com/repos/infinityworks/github-exporter/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/infinityworks/github-exporter/downloads", + "issues_url": "https://api.github.com/repos/infinityworks/github-exporter/issues{/number}", + "pulls_url": "https://api.github.com/repos/infinityworks/github-exporter/pulls{/number}", + "milestones_url": "https://api.github.com/repos/infinityworks/github-exporter/milestones{/number}", + "notifications_url": "https://api.github.com/repos/infinityworks/github-exporter/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/infinityworks/github-exporter/labels{/name}", + "releases_url": "https://api.github.com/repos/infinityworks/github-exporter/releases{/id}", + "deployments_url": "https://api.github.com/repos/infinityworks/github-exporter/deployments", + "created_at": "2016-09-12T08:12:46Z", + "updated_at": "2020-03-13T00:43:25Z", + "pushed_at": "2020-03-16T10:54:20Z", + "git_url": "git://github.com/infinityworks/github-exporter.git", + "ssh_url": "git@github.com:infinityworks/github-exporter.git", + "clone_url": "https://github.com/infinityworks/github-exporter.git", + "svn_url": "https://github.com/infinityworks/github-exporter", + "homepage": "", + "size": 115, + "stargazers_count": 170, + "watchers_count": 170, + "language": "Go", + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "forks_count": 47, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 11, + "license": { + "key": "mit", + "name": "MIT License", + "spdx_id": "MIT", + "url": "https://api.github.com/licenses/mit", + "node_id": "MDc6TGljZW5zZTEz" + }, + "forks": 47, + "open_issues": 11, + "watchers": 170, + "default_branch": "master" + } + }, + "_links": { + "self": { + "href": "https://api.github.com/repos/infinityworks/github-exporter/pulls/39" + }, + "html": { + "href": "https://github.com/infinityworks/github-exporter/pull/39" + }, + "issue": { + "href": "https://api.github.com/repos/infinityworks/github-exporter/issues/39" + }, + "comments": { + "href": "https://api.github.com/repos/infinityworks/github-exporter/issues/39/comments" + }, + "review_comments": { + "href": "https://api.github.com/repos/infinityworks/github-exporter/pulls/39/comments" + }, + "review_comment": { + "href": "https://api.github.com/repos/infinityworks/github-exporter/pulls/comments{/number}" + }, + "commits": { + "href": "https://api.github.com/repos/infinityworks/github-exporter/pulls/39/commits" + }, + "statuses": { + "href": "https://api.github.com/repos/infinityworks/github-exporter/statuses/c136be8275b952f7b82412c3ae31b323fd1c3a06" + } + }, + "author_association": "NONE" + } +] diff --git a/test/testdata/user_releases_response.json b/test/testdata/user_releases_response.json new file mode 100644 index 0000000..d816c3e --- /dev/null +++ b/test/testdata/user_releases_response.json @@ -0,0 +1,218 @@ +[ + { + "url": "https://api.github.com/repos/myOrg/myRepo/releases/17113288", + "assets_url": "https://api.github.com/repos/myOrg/myRepo/releases/17113288/assets", + "upload_url": "https://uploads.github.com/repos/myOrg/myRepo/releases/17113288/assets{?name,label}", + "html_url": "https://github.com/myOrg/myRepo/releases/tag/2.0.0", + "id": 17113288, + "node_id": "MDc6UmVsZWFzZTE3MTEzMjg4", + "tag_name": "2.0.0", + "target_commitish": "master", + "name": "2.0.0", + "draft": false, + "author": { + "login": "ccojocar", + "id": 591127, + "node_id": "MDQ6VXNlcjU5MTEyNw==", + "avatar_url": "https://avatars1.githubusercontent.com/u/591127?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/ccojocar", + "html_url": "https://github.com/ccojocar", + "followers_url": "https://api.github.com/users/ccojocar/followers", + "following_url": "https://api.github.com/users/ccojocar/following{/other_user}", + "gists_url": "https://api.github.com/users/ccojocar/gists{/gist_id}", + "starred_url": "https://api.github.com/users/ccojocar/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/ccojocar/subscriptions", + "organizations_url": "https://api.github.com/users/ccojocar/orgs", + "repos_url": "https://api.github.com/users/ccojocar/repos", + "events_url": "https://api.github.com/users/ccojocar/events{/privacy}", + "received_events_url": "https://api.github.com/users/ccojocar/received_events", + "type": "User", + "site_admin": false + }, + "prerelease": false, + "created_at": "2019-05-02T07:19:18Z", + "published_at": "2019-05-02T15:22:14Z", + "assets": [ + { + "url": "https://api.github.com/repos/myOrg/myRepo/releases/assets/12352989", + "id": 12352989, + "node_id": "MDEyOlJlbGVhc2VBc3NldDEyMzUyOTg5", + "name": "myRepo_2.0.0_checksums.txt", + "label": "", + "uploader": { + "login": "ccojocar", + "id": 591127, + "node_id": "MDQ6VXNlcjU5MTEyNw==", + "avatar_url": "https://avatars1.githubusercontent.com/u/591127?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/ccojocar", + "html_url": "https://github.com/ccojocar", + "followers_url": "https://api.github.com/users/ccojocar/followers", + "following_url": "https://api.github.com/users/ccojocar/following{/other_user}", + "gists_url": "https://api.github.com/users/ccojocar/gists{/gist_id}", + "starred_url": "https://api.github.com/users/ccojocar/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/ccojocar/subscriptions", + "organizations_url": "https://api.github.com/users/ccojocar/orgs", + "repos_url": "https://api.github.com/users/ccojocar/repos", + "events_url": "https://api.github.com/users/ccojocar/events{/privacy}", + "received_events_url": "https://api.github.com/users/ccojocar/received_events", + "type": "User", + "site_admin": false + }, + "content_type": "text/plain; charset=utf-8", + "state": "uploaded", + "size": 294, + "download_count": 14564, + "created_at": "2019-05-02T15:22:16Z", + "updated_at": "2019-05-02T15:22:16Z", + "browser_download_url": "https://github.com/myOrg/myRepo/releases/download/2.0.0/myRepo_2.0.0_checksums.txt" + }, + { + "url": "https://api.github.com/repos/myOrg/myRepo/releases/assets/12352991", + "id": 12352991, + "node_id": "MDEyOlJlbGVhc2VBc3NldDEyMzUyOTkx", + "name": "myRepo_2.0.0_windows_amd64.tar.gz", + "label": "", + "uploader": { + "login": "ccojocar", + "id": 591127, + "node_id": "MDQ6VXNlcjU5MTEyNw==", + "avatar_url": "https://avatars1.githubusercontent.com/u/591127?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/ccojocar", + "html_url": "https://github.com/ccojocar", + "followers_url": "https://api.github.com/users/ccojocar/followers", + "following_url": "https://api.github.com/users/ccojocar/following{/other_user}", + "gists_url": "https://api.github.com/users/ccojocar/gists{/gist_id}", + "starred_url": "https://api.github.com/users/ccojocar/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/ccojocar/subscriptions", + "organizations_url": "https://api.github.com/users/ccojocar/orgs", + "repos_url": "https://api.github.com/users/ccojocar/repos", + "events_url": "https://api.github.com/users/ccojocar/events{/privacy}", + "received_events_url": "https://api.github.com/users/ccojocar/received_events", + "type": "User", + "site_admin": false + }, + "content_type": "application/octet-stream", + "state": "uploaded", + "size": 4774440, + "download_count": 55, + "created_at": "2019-05-02T15:22:16Z", + "updated_at": "2019-05-02T15:22:17Z", + "browser_download_url": "https://github.com/myOrg/myRepo/releases/download/2.0.0/myRepo_2.0.0_windows_amd64.tar.gz" + } + ], + "tarball_url": "https://api.github.com/repos/myOrg/myRepo/tarball/2.0.0", + "zipball_url": "https://api.github.com/repos/myOrg/myRepo/zipball/2.0.0", + "body": "## Changelog\n\n29cec13 Fix formatting in README, remove prerequisite and reworked the Makefile tests goals (#313)\nb68ac76 Fix formatting\n3e69a8c Append the package load errors to analyser's errors\naac9b00 Refactor properly the package error parsing and cover all test cases\n625718d Refactor the test for Go build errors\n3af4ae9 Fix some lint warnings\nbac6f0f Add tests for an empty package without any test file\n76b2c12 Add a test to cover the processing of empty packages\nb04c1ce Fix error parsing from package\n92b3644 Fix error parsing when the loaded package is empty\n48e3932 Remove tests case from import tracker\n25b5a1a Add tests to cover the import tracker from file\n5ef2bee Track only the import from the file which is checked\nf1ea7f6 Add tests for analyser test pacakge check\n6e5135f Update README with some instructions to enable the tests and vendor folder scanning\nb49c953 Add a flag which allows to scan also the tests files\nf1d49a6 Remove unused code\ned2e0aa Update local install command in README file\n4dfaf0a Refactor the analyzer to process one package at the time\nadcfe94 Fix test for helpers\n5ae5266 Add some tests that covers the helper function which list the package paths\ne419eb8 Exclude correctly the vendor folder from the scanned packages\n85eb8a5 Scan the go packages path recursively starting from a root folder\n8522199 Improve logging in the analyser\nea16ff1 Remove GOPATH check to allow running myRepo outside of GOPATH\n6c174a6 Update README file\n7935fd8 Rework the Dockerfile for Go modules\n806908a Remove the dep tool installation from travis CI\n950e84c Handle errors to fix lint warnings\nee73b9e Remove dep and Use only Go modules to manage dependencies\n85d1808 Go modules support for 1.12 (#297)\neaba99d fix comment.\n4cd14f9 remove panic\n66e7c8d Extract to a constant\n1b28d32 fix sonarIssues struct\n8eab50e update README.md to add support of sonarqube.\n989eb3f Update Hound errors\nddfe54d Add sonarqube output\nc5e6c4a fix no-fail flag logic\n2bd007e Update README\n8b27d1c Update go version to 1.11.5 in the docker file\n9cd538f Fix README typo\n\n" + }, + { + "url": "https://api.github.com/repos/myOrg/myRepo/releases/15826180", + "assets_url": "https://api.github.com/repos/myOrg/myRepo/releases/15826180/assets", + "upload_url": "https://uploads.github.com/repos/myOrg/myRepo/releases/15826180/assets{?name,label}", + "html_url": "https://github.com/myOrg/myRepo/releases/tag/1.3.0", + "id": 15826180, + "node_id": "MDc6UmVsZWFzZTE1ODI2MTgw", + "tag_name": "1.3.0", + "target_commitish": "master", + "name": "1.3.0", + "draft": false, + "author": { + "login": "ccojocar", + "id": 591127, + "node_id": "MDQ6VXNlcjU5MTEyNw==", + "avatar_url": "https://avatars1.githubusercontent.com/u/591127?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/ccojocar", + "html_url": "https://github.com/ccojocar", + "followers_url": "https://api.github.com/users/ccojocar/followers", + "following_url": "https://api.github.com/users/ccojocar/following{/other_user}", + "gists_url": "https://api.github.com/users/ccojocar/gists{/gist_id}", + "starred_url": "https://api.github.com/users/ccojocar/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/ccojocar/subscriptions", + "organizations_url": "https://api.github.com/users/ccojocar/orgs", + "repos_url": "https://api.github.com/users/ccojocar/repos", + "events_url": "https://api.github.com/users/ccojocar/events{/privacy}", + "received_events_url": "https://api.github.com/users/ccojocar/received_events", + "type": "User", + "site_admin": false + }, + "prerelease": false, + "created_at": "2019-02-26T22:24:06Z", + "published_at": "2019-02-28T08:25:52Z", + "assets": [ + { + "url": "https://api.github.com/repos/myOrg/myRepo/releases/assets/11282396", + "id": 11282396, + "node_id": "MDEyOlJlbGVhc2VBc3NldDExMjgyMzk2", + "name": "myRepo_1.3.0_checksums.txt", + "label": "", + "uploader": { + "login": "ccojocar", + "id": 591127, + "node_id": "MDQ6VXNlcjU5MTEyNw==", + "avatar_url": "https://avatars1.githubusercontent.com/u/591127?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/ccojocar", + "html_url": "https://github.com/ccojocar", + "followers_url": "https://api.github.com/users/ccojocar/followers", + "following_url": "https://api.github.com/users/ccojocar/following{/other_user}", + "gists_url": "https://api.github.com/users/ccojocar/gists{/gist_id}", + "starred_url": "https://api.github.com/users/ccojocar/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/ccojocar/subscriptions", + "organizations_url": "https://api.github.com/users/ccojocar/orgs", + "repos_url": "https://api.github.com/users/ccojocar/repos", + "events_url": "https://api.github.com/users/ccojocar/events{/privacy}", + "received_events_url": "https://api.github.com/users/ccojocar/received_events", + "type": "User", + "site_admin": false + }, + "content_type": "text/plain; charset=utf-8", + "state": "uploaded", + "size": 294, + "download_count": 7292, + "created_at": "2019-02-28T08:25:53Z", + "updated_at": "2019-02-28T08:25:53Z", + "browser_download_url": "https://github.com/myOrg/myRepo/releases/download/1.3.0/myRepo_1.3.0_checksums.txt" + }, + { + "url": "https://api.github.com/repos/myOrg/myRepo/releases/assets/11282395", + "id": 11282395, + "node_id": "MDEyOlJlbGVhc2VBc3NldDExMjgyMzk1", + "name": "myRepo_1.3.0_windows_amd64.tar.gz", + "label": "", + "uploader": { + "login": "ccojocar", + "id": 591127, + "node_id": "MDQ6VXNlcjU5MTEyNw==", + "avatar_url": "https://avatars1.githubusercontent.com/u/591127?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/ccojocar", + "html_url": "https://github.com/ccojocar", + "followers_url": "https://api.github.com/users/ccojocar/followers", + "following_url": "https://api.github.com/users/ccojocar/following{/other_user}", + "gists_url": "https://api.github.com/users/ccojocar/gists{/gist_id}", + "starred_url": "https://api.github.com/users/ccojocar/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/ccojocar/subscriptions", + "organizations_url": "https://api.github.com/users/ccojocar/orgs", + "repos_url": "https://api.github.com/users/ccojocar/repos", + "events_url": "https://api.github.com/users/ccojocar/events{/privacy}", + "received_events_url": "https://api.github.com/users/ccojocar/received_events", + "type": "User", + "site_admin": false + }, + "content_type": "application/octet-stream", + "state": "uploaded", + "size": 4249453, + "download_count": 21, + "created_at": "2019-02-28T08:25:53Z", + "updated_at": "2019-02-28T08:25:56Z", + "browser_download_url": "https://github.com/myOrg/myRepo/releases/download/1.3.0/myRepo_1.3.0_windows_amd64.tar.gz" + } + ], + "tarball_url": "https://api.github.com/repos/myOrg/myRepo/tarball/1.3.0", + "zipball_url": "https://api.github.com/repos/myOrg/myRepo/zipball/1.3.0", + "body": "## Changelog\n\n62b5195 Report for Golang errors (#284)\n9cdfec4 Change test\n8048b15 Add more badges in the README file\ne2752bc revert to default GOPATH if necessary (#279)\n04ce7ba add a no-fail flag\na966ff7 Fix -conf example in README.md\nb662615 Fix typo\n5d33e6e Update the README with some details about the configuration file\nf87af5f Detect the unhandled errors even though they are explicitly ignored if the 'audit: enabled' setting is defined in the global configuration (#274)\n14ed63d Do not flag the unhandled errors which are explicitly ignored\n12400f9 Update README with the code coverage batch\n72e95e8 Geneate and upload the test coverage report to codecove.io\n24e3094 Extend the bind rule to handle the case when the net.Listen address in provided from a const\n9b32fca Fix the bind rule to handle the case when the arguments of the net.Listen are returned by a function call\nf14f17f Add a helper function which extracts the string parameters values of a call expression\n\n" + } +] \ No newline at end of file diff --git a/test/testdata/user_repos_response.json b/test/testdata/user_repos_response.json new file mode 100644 index 0000000..9483834 --- /dev/null +++ b/test/testdata/user_repos_response.json @@ -0,0 +1,104 @@ +[ + { + "id": 163222413, + "node_id": "MDEwOlJlcG9zaXRvcnkxNjMyMjI0MTM=", + "name": "myRepo", + "full_name": "myUser/myRepo", + "private": false, + "owner": { + "login": "myUser", + "id": 1219157, + "node_id": "MDQ6VXNlcjEyMTkxNTc=", + "avatar_url": "https://avatars1.githubusercontent.com/u/1219157?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/myUser", + "html_url": "https://github.com/myUser", + "followers_url": "https://api.github.com/users/myUser/followers", + "following_url": "https://api.github.com/users/myUser/following{/other_user}", + "gists_url": "https://api.github.com/users/myUser/gists{/gist_id}", + "starred_url": "https://api.github.com/users/myUser/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/myUser/subscriptions", + "organizations_url": "https://api.github.com/users/myUser/orgs", + "repos_url": "https://api.github.com/users/myUser/repos", + "events_url": "https://api.github.com/users/myUser/events{/privacy}", + "received_events_url": "https://api.github.com/users/myUser/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://github.com/myUser/myRepo", + "description": "A simple and extensible behavioural testing library written in golang. You can use api test to simplify REST API, HTTP handler and e2e tests.", + "fork": false, + "url": "https://api.github.com/repos/myUser/myRepo", + "forks_url": "https://api.github.com/repos/myUser/myRepo/forks", + "keys_url": "https://api.github.com/repos/myUser/myRepo/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/myUser/myRepo/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/myUser/myRepo/teams", + "hooks_url": "https://api.github.com/repos/myUser/myRepo/hooks", + "issue_events_url": "https://api.github.com/repos/myUser/myRepo/issues/events{/number}", + "events_url": "https://api.github.com/repos/myUser/myRepo/events", + "assignees_url": "https://api.github.com/repos/myUser/myRepo/assignees{/user}", + "branches_url": "https://api.github.com/repos/myUser/myRepo/branches{/branch}", + "tags_url": "https://api.github.com/repos/myUser/myRepo/tags", + "blobs_url": "https://api.github.com/repos/myUser/myRepo/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/myUser/myRepo/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/myUser/myRepo/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/myUser/myRepo/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/myUser/myRepo/statuses/{sha}", + "languages_url": "https://api.github.com/repos/myUser/myRepo/languages", + "stargazers_url": "https://api.github.com/repos/myUser/myRepo/stargazers", + "contributors_url": "https://api.github.com/repos/myUser/myRepo/contributors", + "subscribers_url": "https://api.github.com/repos/myUser/myRepo/subscribers", + "subscription_url": "https://api.github.com/repos/myUser/myRepo/subscription", + "commits_url": "https://api.github.com/repos/myUser/myRepo/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/myUser/myRepo/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/myUser/myRepo/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/myUser/myRepo/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/myUser/myRepo/contents/{+path}", + "compare_url": "https://api.github.com/repos/myUser/myRepo/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/myUser/myRepo/merges", + "archive_url": "https://api.github.com/repos/myUser/myRepo/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/myUser/myRepo/downloads", + "issues_url": "https://api.github.com/repos/myUser/myRepo/issues{/number}", + "pulls_url": "https://api.github.com/repos/myUser/myRepo/pulls{/number}", + "milestones_url": "https://api.github.com/repos/myUser/myRepo/milestones{/number}", + "notifications_url": "https://api.github.com/repos/myUser/myRepo/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/myUser/myRepo/labels{/name}", + "releases_url": "https://api.github.com/repos/myUser/myRepo/releases{/id}", + "deployments_url": "https://api.github.com/repos/myUser/myRepo/deployments", + "created_at": "2018-12-26T22:27:19Z", + "updated_at": "2019-08-22T20:25:14Z", + "pushed_at": "2019-08-22T20:25:16Z", + "git_url": "git://github.com/myUser/myRepo.git", + "ssh_url": "git@github.com:myUser/myRepo.git", + "clone_url": "https://github.com/myUser/myRepo.git", + "svn_url": "https://github.com/myUser/myRepo", + "homepage": "https://myRepo.dev", + "size": 946, + "stargazers_count": 120, + "watchers_count": 120, + "language": "Go", + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": false, + "has_pages": false, + "forks_count": 10, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 5, + "license": { + "key": "mit", + "name": "MIT License", + "spdx_id": "MIT", + "url": "https://api.github.com/licenses/mit", + "node_id": "MDc6TGljZW5zZTEz" + }, + "forks": 10, + "open_issues": 5, + "watchers": 120, + "default_branch": "master", + "network_count": 10, + "subscribers_count": 5 + } +] \ No newline at end of file