forge

package
v3.59.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 11, 2026 License: AGPL-3.0 Imports: 9 Imported by: 0

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

Constants

This section is empty.

Variables

View Source
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

func IsGitHubHost(host string) bool

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

func IsRateLimit(err error) bool

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 Actor

type Actor struct {
	Login string
	IsBot bool
}

Actor is a GitHub account, with the one fact every metric needs to know about it.

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 NewClient

func NewClient(creds *Credentials, host string) (*Client, error)

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

func (c *Client) FetchIssues(ctx context.Context, repo Repo, since time.Time) ([]Issue, error)

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.

func (*Client) GitHub

func (c *Client) GitHub() *github.Client

GitHub reports the raw client for the calls the collector makes directly.

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

type Credentials struct {
	Token  string
	Source string // GITHUB_TOKEN | GH_TOKEN | gh CLI
}

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.

  1. 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.
  2. GH_TOKEN — the same idea, the variable the gh CLI itself honours first.
  3. 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.

type Repo

type Repo struct {
	Owner         string
	Name          string
	DefaultBranch string
}

Repo identifies what we are fetching.

type Review

type Review struct {
	Reviewer    Actor
	State       string // approved | changes_requested | commented | dismissed
	SubmittedAt time.Time
}

Review is one submitted review.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL