Documentation
¶
Overview ¶
Package forge is the code-host boundary (GitHub today; AGENTS.md §11 leaves GitLab as an interface with no adaptor until a GitLab repo needs one). Forge is activity-shaped (AGENTS.md §4.3): every method is func(ctx, In) (Out, error) with JSON-serialisable, secret-free inputs and outputs — the credential itself never crosses this boundary, only its effect. pkg/forge/github resolves auth from the user's own `gh` login via go-gh; this package never reads a token, an env var, or a flag naming one.
Index ¶
- Variables
- type CheckSummary
- type Comment
- type Commit
- type Comparison
- type Fake
- func (f *Fake) AddComment(prNumber int, c Comment)
- func (f *Fake) BlameLines(_ context.Context, ref, path string, lines []int) (map[int]LineOrigin, error)
- func (f *Fake) Checks(_ context.Context, sha string) (CheckSummary, error)
- func (f *Fake) Comments(_ context.Context, prNumber int, since time.Time) ([]Comment, error)
- func (f *Fake) CommitFiles(_ context.Context, sha string) ([]string, bool, error)
- func (f *Fake) CommitsTouching(_ context.Context, ref, path string, since time.Time) ([]string, error)
- func (f *Fake) Compare(_ context.Context, base, head string) (Comparison, error)
- func (f *Fake) CreatePR(_ context.Context, spec PRSpec) (PR, error)
- func (f *Fake) FindPR(_ context.Context, headBranch, bodyMarker string) (PR, bool, error)
- func (f *Fake) GetPR(_ context.Context, number int) (PR, bool, error)
- func (f *Fake) IsAllowedAuthor(_ context.Context, login string) (bool, error)
- func (f *Fake) MergePR(_ context.Context, prNumber int, expectedHeadSHA string) (PR, error)
- func (f *Fake) PRs() []PR
- func (f *Fake) ReadFile(_ context.Context, ref, path string) ([]byte, bool, error)
- func (f *Fake) ResolveRef(_ context.Context, ref string) (string, bool, error)
- func (f *Fake) SetBase(prNumber int, base string)
- func (f *Fake) SetChecks(sha string, sum CheckSummary)
- func (f *Fake) SetClosed(prNumber int, closed bool)
- func (f *Fake) SetHeadSHA(prNumber int, sha string)
- func (f *Fake) Tags(_ context.Context) ([]GitTag, error)
- type Forge
- type GitTag
- type LineOrigin
- type PR
- type PRSpec
Constants ¶
This section is empty.
Variables ¶
var ErrStaleHead = errors.New("forge: PR head does not match the expected sha")
ErrStaleHead is returned (wrapped) by MergePR when the PR's current head sha no longer matches expectedHeadSHA: something else moved the branch since this promotion last observed it pushed, and merging blind could squash-merge content this promotion never verified (AGENTS.md named adversary; R-003's neighbor). Callers distinguish this from a plain transport error with errors.Is.
var ErrUnknownRef = errors.New("forge: no such ref")
ErrUnknownRef is returned (wrapped) by Compare when base or head names nothing the forge knows — a tag that was deleted, a sha only ever pushed to a fork, or a private repo the token cannot see (a GitHub 404 is the same for all three). Callers distinguish it from a transport error with errors.Is: pkg/migrate turns it into "not in the app repo" rather than "unknown", since the revision itself was resolved and it is the repo that lacks it.
Functions ¶
This section is empty.
Types ¶
type CheckSummary ¶
type CheckSummary struct {
Total, Pending, Success, Failure, Skipped int
FailedNames, SkippedNames []string
}
CheckSummary is the combined check-run AND commit-status rollup for one commit sha (GitHub reports CI through two genuinely separate mechanisms — check-runs and the older Statuses API — and a repository can use either or both; an implementation must query and fold in both, never just one). FailedNames lists the name of every check-run or status context (not empty run titles) that concluded in something other than success/neutral, so CIGreenStep can name which checks failed rather than just reporting a count (M4). Skipped has no equivalent among commit statuses (the Statuses API has no "skipped" state) — SkippedNames only ever names check-runs.
Skipped is its own bucket, not folded into Success: a `skipped` conclusion means the check never actually ran (a path filter, a conditional job) — GitHub reports it the same way whether the check was optional or required, and this type carries no required-vs-optional distinction at all, so there is no way to tell "safely skipped" from "a required gate that silently never ran" at this layer. CIGreenStep therefore treats any Skipped>0 as blocking, the same as Failure>0 (AGENTS.md §2 principle 5: "warn, don't block, except where the runbook blocks" — a required check that never ran is exactly that exception). SkippedNames mirrors FailedNames.
type Comment ¶
Comment is one issue/PR comment. AuthorType is the GitHub account "type" of the commenter ("User", "Organization", "Bot", …) — added in M4 so a caller can tell a bot apart from a legitimate non-"User" account (an org-owned login is real, per the precedent in pkg/forge/github's Comments doc comment) without trusting the comment body for anything.
type Commit ¶
type Commit struct {
SHA string
Subject string
Body string
Author string
Date time.Time // author date
}
Commit is one commit as Compare reports it (M10, for the migration delta and the commit history the confirm screens lead with). Subject is the first line of the message and Body everything after the first blank line ("" for a one-line message). Author is the commit author's display name, not a login — a squash-merged commit's author may have no account. Every string here is upstream text nothing in hoist wrote; adaptors pass each through pkg/redact before returning it, the same way Checks does for check-run names.
type Comparison ¶
type Comparison struct {
Status string
AheadBy int
BehindBy int
Commits []Commit // oldest first, as the forge orders them
Total int
Truncated bool
Files []string
FilesTruncated bool
}
Comparison is base...head, three-dot: the commits reachable from head but not from base. Status is the forge's own word for the relationship ("ahead", "behind", "diverged", "identical"); a caller that asked base...head and gets "behind" is looking at a rollback and should compare the other way to see what is being un-applied. Total is the forge's own count of commits in the range even when Commits is shorter, and Truncated says so explicitly — an adaptor pages to its own bound rather than crawl an unbounded range. Files is every path changed across the whole range (not per commit; use CommitFiles for that), FilesTruncated when the forge capped that list.
type Fake ¶
type Fake struct {
HeadSHAs map[string]string
// GitTags is what Tags returns; TagsErr, when set, is returned instead.
GitTags []GitTag
TagsErr error
// CreateErr, FindErr and GetErr, when set, are returned by every call to the matching method.
CreateErr error
FindErr error
GetErr error
// ChecksBySHA and ChecksErr configure Checks: a sha not present in the map reports the
// zero CheckSummary (no checks reported at all), exactly like a real repo with no CI
// configured; ChecksErr, when set, is returned instead for every call (simulating a 404 or
// permissions hiccup a caller must retry rather than treat as authoritative absence).
ChecksBySHA map[string]CheckSummary
ChecksErr error
// CommentsByPR and CommentsErr configure Comments. AddComment is the intended way tests
// populate CommentsByPR (thread-safe); the field itself may also be set directly before any
// concurrent use begins.
CommentsByPR map[int][]Comment
CommentsErr error
// Allowed configures IsAllowedAuthor: logins mapped to true are collaborators with write
// permission; anything absent is not. AllowedErr, when set, is returned instead — simulating
// a token missing the scope IsAllowedAuthor needs.
Allowed map[string]bool
AllowedErr error
// MergeErr, when set, is returned by every call to MergePR regardless of head sha.
MergeErr error
// M10 (pkg/migrate) configuration. Refs maps a ref name (tag, branch, full or abbreviated
// sha) to the full sha ResolveRef answers with; absent means ok=false. Comparisons is keyed
// "base...head" exactly as a caller would write the range. FilesBySHA feeds CommitFiles;
// Touching is keyed "ref path" (one space) and feeds CommitsTouching, whose since filter is
// NOT applied by the fake (the caller is asserting on attribution, not on dates — a test
// that needs the filter seeds only the shas it expects). Blames is keyed "ref path" and maps
// line -> LineOrigin. Files is keyed "ref path" and feeds ReadFile. Each *Err, when set, is
// returned by every call to the matching method.
Refs map[string]string
ResolveErr error
Comparisons map[string]Comparison
CompareErr error
FilesBySHA map[string][]string
FilesCapped map[string]bool
FilesErr error
Touching map[string][]string
TouchingErr error
Blames map[string]map[int]LineOrigin
BlameErr error
Files map[string][]byte
ReadErr error
// Calls records every method invocation, in order, for tests asserting call counts
// ("CreatePR was called exactly once across both kill/resume attempts").
Calls []string
// contains filtered or unexported fields
}
Fake is an in-memory Forge for tests in other packages (internal/engine's resume tests in particular): no test anywhere points at a real GitHub repo (AGENTS.md hard constraints). HeadSHAs, keyed by branch name, lets a test tell the Fake what a real forge would have discovered on its own (the pushed tip) — Fake has no access to git, so it cannot compute this itself; a test that cares about PR.HeadSHA sets it before calling CreatePR.
func (*Fake) AddComment ¶
AddComment appends c to prNumber's comments, thread-safely — the way a test simulates a human commenting on the PR mid-poll.
func (*Fake) BlameLines ¶
func (f *Fake) BlameLines(_ context.Context, ref, path string, lines []int) (map[int]LineOrigin, error)
BlameLines implements Forge against Blames (keyed "ref path"). Lines the test never seeded are absent from the result, exactly like a line past the file's end on a real forge.
func (*Fake) Checks ¶
Checks implements Forge: sha's configured CheckSummary (ChecksBySHA), or the zero value (no checks reported) when nothing was configured for it.
func (*Fake) Comments ¶
Comments implements Forge: prNumber's configured comments (CommentsByPR) whose CreatedAt is at or after since, oldest first — exactly the ordering and filter the real adaptor's own "since" query param gives.
func (*Fake) CommitFiles ¶
CommitFiles implements Forge against FilesBySHA; a sha listed in FilesCapped is reported truncated, the forge's "300 files and counting" answer.
func (*Fake) CommitsTouching ¶
func (f *Fake) CommitsTouching(_ context.Context, ref, path string, since time.Time) ([]string, error)
CommitsTouching implements Forge against Touching (keyed "ref path"); since is recorded in Calls but not applied — see the field's doc comment.
func (*Fake) Compare ¶
Compare implements Forge against Comparisons. A range the test never seeded is reported as ErrUnknownRef — the same answer a real forge gives for a ref it does not have — so a test asserting the unknown-ref path needs no special hook, and a test that forgot to seed a range fails loudly rather than getting an empty, plausible-looking comparison.
func (*Fake) CreatePR ¶
CreatePR implements Forge. As a safety net for tests exercising the resume property, it refuses to open a second open PR for a head branch that already has one — the real GitHub API enforces the same thing (422 "A pull request already exists"), so this is not a behavior unique to the fake, just enforced here too rather than only against production.
func (*Fake) FindPR ¶
FindPR implements Forge: first by head branch, then by searching every stored PR's body for bodyMarker (mirroring the real adaptor's fallback for a renamed or recreated branch).
func (*Fake) GetPR ¶
GetPR implements Forge: the stored PR with that number, ok=false for any other. GetErr, when set, is returned instead.
func (*Fake) IsAllowedAuthor ¶
IsAllowedAuthor implements Forge against the Allowed map.
func (*Fake) MergePR ¶
MergePR implements Forge: squash-merges prNumber only if its recorded HeadSHA still equals expectedHeadSHA (simulating the real API's atomic "merge iff head is X" — Known bug classes: a stale head must be refused, not raced). Merging an already-merged PR returns an error, the same way a second real merge call against an already-merged PR would 405 — exercising the caller's "re-check FindPR before concluding failure" path (the named adversary: a process killed mid-merge-call).
func (*Fake) PRs ¶
PRs returns a snapshot of every PR the fake has created, for a test asserting "exactly one PR exists".
func (*Fake) ResolveRef ¶
ResolveRef implements Forge against the Refs map.
func (*Fake) SetBase ¶
SetBase is the test-only hook standing in for another actor retargeting a PR's base after this promotion last observed it — the same rare, real race MergedStep's own success-path base check (round-6 hardening) exists to catch (mirrors SetHeadSHA's own stale-head test hook).
func (*Fake) SetChecks ¶
func (f *Fake) SetChecks(sha string, sum CheckSummary)
SetChecks records sha's CheckSummary, thread-safely — the way a test simulates CI going from pending to green (or to a named failure) between polls.
func (*Fake) SetClosed ¶
SetClosed is the test-only hook standing in for an operator closing a PR on GitHub without merging it (round-9 finding: PROpenedStep must refuse to adopt one of these as satisfied).
func (*Fake) SetHeadSHA ¶
SetHeadSHA overwrites prNumber's recorded HeadSHA — how a test simulates "something else moved the branch after this promotion last observed it pushed" (the stale-head-at-merge adversary) without needing a second real git push.
type Forge ¶
type Forge interface {
// CreatePR opens a new pull request from spec.Head into spec.Base. The caller (internal/
// engine's PROpened step) always calls FindPR first — CreatePR is not itself required to
// deduplicate, though implementations may refuse an obviously duplicate head branch as a
// safety net (Fake does; the real GitHub API does this on its own, returning 422).
CreatePR(ctx context.Context, spec PRSpec) (PR, error)
// FindPR looks for an existing pull request for this promotion: first by headBranch, then
// — so that a PR survives its branch being renamed or the branch being deleted and
// recreated — by searching bodyMarker (the exact "<!-- hoist:id=... -->" line) across
// open PRs and, if that search is empty, recently closed/merged ones within a bound.
// ok=false means no matching PR exists yet, not an error.
FindPR(ctx context.Context, headBranch, bodyMarker string) (PR, bool, error)
// GetPR fetches one pull request by number, ok=false when no such PR exists. It is the
// exact lookup a caller uses once a promotion has recorded its PR's number (the state file
// as an index of where to look — AGENTS.md §4.1), in place of FindPR's bounded body-marker
// scan, which cannot find a merged, branch-deleted PR once enough later PRs have closed
// past its window (issue #45). The caller still checks the returned PR is its own.
GetPR(ctx context.Context, number int) (PR, bool, error)
// Checks reports the check-run rollup for sha. Stubbed/minimal is fine for M3; M4 extends
// it into a gate.
Checks(ctx context.Context, sha string) (CheckSummary, error)
// Comments lists comments on prNumber posted at or after since. Stubbed/minimal is fine
// for M3; M4 scans these for the approval magic comment.
Comments(ctx context.Context, prNumber int, since time.Time) ([]Comment, error)
// IsAllowedAuthor reports whether login is a collaborator with write (or higher) permission
// on the repo — the second way (besides RepoConfig.Approvers) an Approved comment's author
// can be accepted (R-001). A permission-scope error (the gh token lacking what this needs)
// must be returned as an error, never silently folded into false — AGENTS.md §6.1's "the gh
// token may be missing the repo scope this needs" gotcha applies here exactly as it does to
// every other adaptor call.
IsAllowedAuthor(ctx context.Context, login string) (bool, error)
// MergePR squash-merges prNumber, but only if the PR's current head sha still equals
// expectedHeadSHA — using the forge's own atomic "merge iff head is X" primitive, never a
// client-side check-then-merge (a race the caller cannot close on its own). A stale head is
// reported as an error satisfying errors.Is(err, ErrStaleHead). Merging an already-merged PR
// is not an error the caller must avoid causing — MergePR may report it either way, and a
// caller must re-check FindPR before concluding a merge failed outright (a killed process
// cannot always tell whether its own call landed server-side).
MergePR(ctx context.Context, prNumber int, expectedHeadSHA string) (PR, error)
// Tags lists this forge's repo's git tags, each with the date of the commit it points to
// — bounded, not exhaustive: an adaptor may cap how many it returns rather than crawl an
// unbounded tag list in full (pkg/forge/github's own Client.Tags stops after
// maxTagPages*100 = 300 tags today). Added for M6: the tag picker prefers the app repo's
// own git tags for ordering registry tags by recency (AGENTS.md invariant 3) over the
// registry's own unordered, timestamp-free tag list (AGENTS.md §6.1 item 3). Order is
// unspecified — callers sort by Date themselves; a repo with no tags returns an empty
// slice, not an error.
Tags(ctx context.Context) ([]GitTag, error)
// ResolveRef resolves a tag name, branch name, or (possibly abbreviated) commit sha to a
// full commit sha. ok=false means the forge has no such ref, or the abbreviation is
// ambiguous — not an error. A scope or permission failure IS an error, never folded into
// ok=false: pkg/migrate reads ok=false as "try the next source" and an error as "stop", and
// a token gap read as "unknown" would silently degrade every revision to unresolved
// (AGENTS.md §6.1 item 1's scope gotcha, at this boundary). M10.
ResolveRef(ctx context.Context, ref string) (sha string, ok bool, err error)
// Compare lists the commits and changed files between base and head, three-dot (reachable
// from head, not from base). Bounded: an adaptor pages up to its own cap and reports
// Comparison.Truncated rather than crawl further. Either ref unknown to the forge is an
// error satisfying errors.Is(err, ErrUnknownRef). M10.
Compare(ctx context.Context, base, head string) (Comparison, error)
// CommitFiles lists the paths one commit changed, bounded the same way (truncated=true
// when the forge capped the list). M10.
CommitFiles(ctx context.Context, sha string) (files []string, truncated bool, err error)
// CommitsTouching lists, newest first, the shas of commits reachable from ref that changed
// anything under path (a directory prefix or a file), committed at or after since. Bounded
// by the adaptor's page cap. M10: how pkg/migrate attributes migration files to the commits
// that added them without reading every commit in a range.
CommitsTouching(ctx context.Context, ref, path string, since time.Time) ([]string, error)
// BlameLines reports, for each requested 1-based line of path at ref, the commit that last
// changed it. A line past the file's end is absent from the map, not an error. M10: "how
// long has the image this promotion replaces been live" is the age of the manifest line,
// not of the file — one file holds several image lines and any one of them moving would
// reset a file-level date.
BlameLines(ctx context.Context, ref, path string, lines []int) (map[int]LineOrigin, error)
// ReadFile returns the content of path at ref. ok=false means the file does not exist
// there — not an error, since the one caller (pkg/migrate's per-app-repo `.hoist.yaml`)
// treats absence as "fall back to config". M10.
ReadFile(ctx context.Context, ref, path string) (content []byte, ok bool, err error)
}
Forge is the seam between internal/engine and the code host. Every adaptor (pkg/forge/github, and Fake for tests) implements the same interface, so internal/engine never knows which one it is talking to.
type GitTag ¶
GitTag is one git tag on the forge as Tags reports it: the tag name and the date of the commit it points to. Date is deliberately the *commit's* date, not an annotated tag object's own creation date — "when was this released" (M6's tag-ordering use, AGENTS.md invariant 3) means when the code was committed, not when someone got around to tagging it, and a lightweight tag has no object date to read anyway.
type LineOrigin ¶
LineOrigin is the commit that last changed one line of one file at one ref, as BlameLines reports it. Date is the committer date.
type PR ¶
type PR struct {
Number int
URL string
HeadBranch string
HeadSHA string
Base string
Merged bool
MergeSHA string
CreatedAt time.Time
// Closed is true for a PR someone closed WITHOUT merging (GitHub's own "state": "closed"
// with Merged still false) — a dead PR a merge call can never succeed against (a real 405).
// FindPR's query (state=all, so a renamed-branch/body-marker fallback can still find a
// promotion's own history) can return one of these, so a caller adopting a found PR must
// check this explicitly rather than assuming "found" means "usable."
Closed bool
}
PR is a pull request as this package cares about it: identity, where it points, and its merge state. Body is deliberately not part of PR — only PRSpec carries it in, since nothing downstream of CreatePR/FindPR needs to read it back (FindPR searches it internally).
Directories
¶
| Path | Synopsis |
|---|---|
|
Package github implements pkg/forge.Forge against the real GitHub REST API, using github.com/cli/go-gh/v2 (AGENTS.md §4.7's one sanctioned new dependency for M3 — not google/go-github, not a hand-rolled REST client, not the full gh CLI as a library).
|
Package github implements pkg/forge.Forge against the real GitHub REST API, using github.com/cli/go-gh/v2 (AGENTS.md §4.7's one sanctioned new dependency for M3 — not google/go-github, not a hand-rolled REST client, not the full gh CLI as a library). |