Documentation
¶
Overview ¶
Package forge talks to the code-hosting platform — today, GitHub.
`analyze` needs it for the metrics that do not exist in a git repository at all: how long a pull request waited for its first review, whether a commit that reached the default branch was ever approved by anyone, how long an issue sat unanswered. None of that is in the object database; it lives in the forge, and without credentials it cannot be known.
Which is why the authentication check runs *before* any scanning starts and fails loudly. Discovering at the end of a five-minute analysis that a third of the report is null because a token was missing is a waste of the user's time, and a report full of nulls is exactly the kind of thing people stop reading.
Index ¶
- Variables
- func IsGitHubHost(host string) bool
- func IsRateLimit(err error) bool
- type Actor
- type CheckResult
- type Client
- func (c *Client) Budget() *budget
- func (c *Client) FetchCommitReviews(ctx context.Context, repo Repo, shas []string) ([]CommitReview, error)
- func (c *Client) FetchIssues(ctx context.Context, repo Repo, since time.Time) ([]Issue, error)
- func (c *Client) FetchPullRequests(ctx context.Context, repo Repo, since time.Time) ([]PullRequest, error)
- func (c *Client) GitHub() *github.Client
- type CommitReview
- type Credentials
- type Issue
- type PullRequest
- type Repo
- type Review
Constants ¶
This section is empty.
Variables ¶
var ErrNoCredentials = fmt.Errorf(`no GitHub credentials found.
vulnetix analyze reads pull requests, reviews and issues from GitHub to compute review
coverage, response times, and whether commits reached the default branch unreviewed. None
of that exists in the git repository itself.
Authenticate in any one of these ways:
export GITHUB_TOKEN=<token> # in GitHub Actions this is already set for you:
# env:
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
export GH_TOKEN=<token>
gh auth login # the gh CLI's own credentials are picked up
The token needs read access to the repository's pull requests, issues and contents.
Or run with --no-forge to skip these metrics entirely. They will be reported as
"not measured" rather than zero — the report never claims we looked when we did not`)
ErrNoCredentials carries the fix, not just the failure. An error that tells you what went wrong and not what to do about it makes the reader do work we already know how to do.
Functions ¶
func IsGitHubHost ¶
IsGitHubHost reports whether a repo host is GitHub. A GitLab repository must not fail for want of a GitHub token — refusing to analyze a repo we were never going to call GitHub about would be a bug wearing a security hat.
func IsRateLimit ¶
IsRateLimit reports whether an error is GitHub telling us to stop. Distinguishing it from a real failure matters: one means "come back later", the other means something is broken, and treating them the same makes both harder to diagnose.
Types ¶
type CheckResult ¶
type CheckResult struct {
Login string
Source string
RateLimitRemaining int
RateLimitLimit int
RateLimitResetsAt time.Time
}
CheckResult is what the startup check learned. RateLimitRemaining is reported because a user whose token has 12 requests left should find that out now, not two thousand API calls into the run.
func Check ¶
func Check(ctx context.Context, creds *Credentials, host string) (*CheckResult, error)
Check verifies the credentials actually work, before any scanning begins.
It is one API call. A token that is expired, revoked, or scoped to the wrong org fails here — in the first second, with an error naming the problem — rather than producing a report where every forge metric is mysteriously null.
func (CheckResult) String ¶
func (c CheckResult) String() string
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client wraps go-github with the rate-limit discipline this tool needs.
func (*Client) Budget ¶
func (c *Client) Budget() *budget
Budget reports the remaining request allowance.
func (*Client) FetchCommitReviews ¶
func (c *Client) FetchCommitReviews(ctx context.Context, repo Repo, shas []string) ([]CommitReview, error)
FetchCommitReviews answers, for each commit, whether it was ever approved.
This is github-metrics-aggregator's compliance metric and it is the best one in the survey: not "were PRs reviewed" but "did anything reach the default branch that nobody approved". A repo can have perfect review coverage on its PRs and still have half its commits pushed straight to main.
func (*Client) FetchIssues ¶
FetchIssues returns issues (not PRs) updated within the window.
func (*Client) FetchPullRequests ¶
func (c *Client) FetchPullRequests(ctx context.Context, repo Repo, since time.Time) ([]PullRequest, error)
FetchPullRequests returns every PR updated within the window, newest first, with its reviews and the timestamps the metrics need.
Stops early and reports `truncated` when the budget runs out. It never returns a partial set silently — the caller checks the budget and declares the shortfall.
type CommitReview ¶
type CommitReview struct {
SHA string
// PRNumber and PRURL are zero/empty when no pull request could be associated with the
// commit at all.
PRNumber int
PRURL string
// Status is one of:
// approved — a PR exists and someone other than the author approved it
// changes_requested — a PR exists and the last word on it was "no"
// review_required — a PR exists and nobody approved it
// unknown — no PR could be associated with this commit
//
// `unknown` and `review_required` are kept apart on purpose. One means "we could not
// tell", the other means "we could tell, and it was not reviewed". Collapsing them turns
// an absence of evidence into an accusation.
Status string
}
CommitReview is the answer to GMA's question: did this commit, which is on the default branch, ever get approved by anybody?
type Credentials ¶
Credentials are a token and the story of where it came from. The source is reported to the user because "which of my four possible tokens is this thing actually using" is a question people should never have to reverse-engineer.
func ResolveCredentials ¶
func ResolveCredentials(ctx context.Context) (*Credentials, error)
ResolveCredentials finds a GitHub token, in the order a user would expect one to win.
- GITHUB_TOKEN — always present inside GitHub Actions, which is where analyze runs in anger. If it is set, it is what CI intends us to use.
- GH_TOKEN — the same idea, the variable the gh CLI itself honours first.
- The gh CLI's own credential store, via `gh auth token`. We shell out rather than reading ~/.config/gh/hosts.yml directly: gh may keep the token in the system keyring instead of that file, and hand-parsing its config would break silently the day it changes storage. Asking gh is the only way to get an answer that stays true.
type Issue ¶
type Issue struct {
Number int
URL string
Title string
Author Actor
State string
StateReason string
Labels []string
CreatedAt time.Time
ClosedAt time.Time
FirstResponseAt time.Time
}
Issue is one issue (never a PR — GitHub's REST API returns PRs from the issues endpoint, and conflating the two double-counts everything).
type PullRequest ¶
type PullRequest struct {
Number int
URL string
Title string
Author Actor
State string // open | closed | merged
CreatedAt time.Time
ClosedAt time.Time
MergedAt time.Time // zero when the PR was closed without merging. Never a substitute for ClosedAt.
MergedBy Actor
// ReadyForReviewAt is when the PR left draft, derived from its timeline. Response and
// merge times anchor here rather than at CreatedAt, so time a PR spent as a draft is not
// charged against the people who reviewed it once it was ready.
ReadyForReviewAt time.Time
// TimeInDraftSeconds is summed across *every* draft interval, not just the first — a PR
// can be flipped back to draft, and pretending it cannot understates the wait.
TimeInDraftSeconds int
Additions int
Deletions int
ChangedFiles int
FirstResponseAt time.Time // first comment or review by someone who is not the author, and not a bot
FirstReviewAt time.Time // first *submitted review*, which is not the same thing as a comment
CommentCount int
Reviews []Review
}
PullRequest is one PR, with the timestamps the timing metrics anchor on.