pr

package
v0.9.0 Latest Latest
Warning

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

Go to latest
Published: Sep 14, 2026 License: Apache-2.0 Imports: 13 Imported by: 0

Documentation

Index

Constants

View Source
const (
	StaleGroupHeader     = "Stale (behind base, failing checks green there -- refresh first)"
	RefreshedGroupHeader = "Refreshed (re-checking)"
	CancelledGroupHeader = "Cancelled (CircleCI auto-cancelled the build -- retry first)"
	RetriedGroupHeader   = "Retried (re-checking)"
)

Section headers shared by the plain-text and TUI summaries for the stale and cancelled buckets, so both outputs name them identically.

Variables

This section is empty.

Functions

func AdjustColumnWidths

func AdjustColumnWidths(cols []TableColumn, prs []PRInfo)

AdjustColumnWidths widens each column so every value fits without truncation.

func AgeColorCode

func AgeColorCode(created, now time.Time) string

AgeColorCode returns the ANSI color prefix for a PR age: "" while the PR is fresh, yellow once it passes ageWarnAfter, red once it passes ageStaleAfter. Callers must reset with \033[0m when non-empty.

func AgeColorFunc

func AgeColorFunc(p PRInfo) string

AgeColorFunc is the table-column color accessor for the Age column.

func AgeDays

func AgeDays(created, now time.Time) int

AgeDays returns the whole number of days since created, or 0 for a zero creation time.

func AgeInfoFunc

func AgeInfoFunc(p PRInfo) string

AgeInfoFunc is the table-column accessor for the Age column.

func AuthorInfoFunc

func AuthorInfoFunc(p PRInfo) string

func ChangeID

func ChangeID(title string) string

ChangeID fingerprints a dependency-update PR by what it updates: the dependency name and the target version parsed from the PR title, e.g. "typescript@v7" for "chore(deps): update dependency typescript to v7". Titles naming no single dependency or no version ("Update all non-major dependencies", "Lock file maintenance") yield "".

func ColorizeRescue

func ColorizeRescue(m *RescueMarker, now time.Time) string

ColorizeRescue wraps the FormatRescue annotation in ANSI colors: a stale marker renders yellow (retriable -- the change differs from the one attempted), a fresh one bold red (a rescue already failed on exactly this change, whether or not the branch was rebased since; a human is needed).

func ColorizeStatus

func ColorizeStatus(state StatusState, detail string) string

func DependencyInfoFunc

func DependencyInfoFunc(p PRInfo) string

func DisableLineWrap

func DisableLineWrap(w *os.File)

DisableLineWrap turns off the terminal's auto-wrap (DECAWM) so rows wider than the terminal are clipped at the right edge instead of wrapping onto a second physical line. UpdateTable's cursor-up math counts logical rows, so a wrapped row would desync the redraw and produce the garbled output users see when CI detail strings push a line past the terminal width.

func EnableLineWrap

func EnableLineWrap(w *os.File)

EnableLineWrap restores the terminal's auto-wrap mode. Always pair it with DisableLineWrap (typically via defer) so an early return does not leave the user's terminal in a clipped state.

func ExtractDependencyName

func ExtractDependencyName(title string) string

func ExtractOwnerRepo

func ExtractOwnerRepo(htmlURL string) (string, string, error)

ExtractOwnerRepo parses owner and repo from a GitHub HTML URL (e.g. "https://github.com/OWNER/REPO/pull/123").

func ExtractTargetVersion

func ExtractTargetVersion(title string) string

ExtractTargetVersion returns the version a dependency update moves to, regardless of whether the title also names the version it moves from.

func ExtractVersion

func ExtractVersion(title string) string

ExtractVersion renders the version change named in a title for display: "4.17.20 -> 4.17.21" for a from/to title, "v1.10.2" for a to-only title, "" when the title names no version.

func FormatAge

func FormatAge(created, now time.Time) string

FormatAge renders the time since created as a compact human string ("5h", "3d", "2w"). It returns "" for a zero creation time so callers can omit the column value when the discovery path did not provide it.

func FormatRescue

func FormatRescue(m *RescueMarker, now time.Time) string

FormatRescue renders the marker as a short human-readable annotation for status output, e.g. "rescue failed 1d ago (klaus): ESM-only", "rescue failed 3d ago (klaus), stale: new commits since" or "rescue blocked 5d ago (klaus), rebased since: same change: ESM-only".

func IsDependencyUpdateTitle

func IsDependencyUpdateTitle(title string) bool

IsDependencyUpdateTitle returns true if the PR title looks like an automated dependency update (Renovate or Dependabot), regardless of who authored it.

func MakeHyperlink(text, url string) string

func ParsePRURL

func ParsePRURL(htmlURL string) (owner, repo string, number int, err error)

ParsePRURL parses owner, repo, and PR number from a GitHub pull request URL (e.g. "https://github.com/OWNER/REPO/pull/123").

func PatchID

func PatchID(files []*github.CommitFile, changedFiles int) string

PatchID computes the content fingerprint of a PR from the files of its compare response (GET /repos/{owner}/{repo}/compare/{base}...{head}). changedFiles is the PR's changed_files count. The compare API returns at most 300 files and omits the patch of a file whose diff is too large, so PatchID returns "" whenever the input cannot describe the whole change rather than fingerprinting part of it.

Per file the hash covers status, previous and current path, and every added or removed line of the patch in order. Hunk headers and context lines are skipped: both shift when the base branch changes around the PR's lines, which is exactly what a rebase produces. A file without a patch and without changed lines (binary content, a pure rename, a mode change) contributes its blob SHA instead.

func PrintPlainResults

func PrintPlainResults(w *os.File, status *PRStatus)

func PrintRow

func PrintRow(w *os.File, e StatusEntry, cols []TableColumn)

func PrintTableHeader

func PrintTableHeader(w *os.File, cols []TableColumn)

func RepoInfoFunc

func RepoInfoFunc(p PRInfo) string

func UpdateTable

func UpdateTable(w *os.File, entries []StatusEntry, cols []TableColumn)

func VersionInfoFunc

func VersionInfoFunc(p PRInfo) string

Types

type Counts

type Counts struct {
	Merged    int
	Failed    int
	Blocked   int
	Stale     int
	Refreshed int
	Cancelled int
	Retried   int
	Skipped   int
}

Counts holds the aggregate tallies of a sweep by outcome category.

Failed covers every action-required outcome (plain and security failures, conflicts, untrusted authors). Blocked (CI could not run because of an Actions budget block), Stale (failing checks are green on the base branch head and the PR is behind it), Refreshed (a stale branch was just updated from its base), Cancelled (CircleCI auto-cancelled the failing builds) and Retried (those builds were just retried) are counted separately from Failed: none of them is a genuine CI failure and none of them belongs in the rescue path.

type Fingerprint

type Fingerprint struct {
	PatchID  string `json:"patch_id,omitempty"`
	ChangeID string `json:"change_id,omitempty"`
}

Fingerprint identifies what a PR changes independently of its head SHA. Renovate force-pushes a rebased branch whenever its base moves, so the head SHA changes while the change itself stays byte-identical; a fingerprint lets a rescue marker survive that.

PatchID is the precise fingerprint: a hash over the PR's compare diff normalised the way `git patch-id --stable` does, minus the context lines -- hunk headers and line numbers are ignored, only file paths and the added/removed lines count. It is empty when the diff could not be fetched or GitHub truncated it (see PatchID).

ChangeID is the cheap approximation for dependency-update PRs: the dependency name and target version parsed from the PR title, e.g. "typescript@v7". A rebase never changes it and a new version always does, but a version update that keeps the title (7.0.1 -> 7.0.2 under "to v7") is invisible to it, so it only decides when no patch ID is available. It is empty when the title is not a recognisable single-dependency update.

func (Fingerprint) Comparable

func (f Fingerprint) Comparable() bool

Comparable reports whether the fingerprint carries anything to compare.

func (Fingerprint) Matches

func (f Fingerprint) Matches(other Fingerprint) bool

Matches reports whether other describes the same change as f. The patch ID decides whenever both sides have one; the title-derived change ID is the fallback for when the diff could not be fingerprinted on either side. Nothing comparable means "not the same" -- a marker is only kept alive on positive evidence.

type PRGroup

type PRGroup struct {
	Key   string
	PRs   []PRInfo
	Count int
}

func GroupByDependency

func GroupByDependency(prs []PRInfo) []PRGroup

func GroupByRepo

func GroupByRepo(prs []PRInfo) []PRGroup

type PRInfo

type PRInfo struct {
	Owner     string
	Repo      string
	Number    int
	Title     string
	URL       string
	Author    string
	CreatedAt time.Time
}

type PRStatus

type PRStatus struct {
	// contains filtered or unexported fields
}

func NewPRStatus

func NewPRStatus() *PRStatus

func (*PRStatus) ActionRequired

func (s *PRStatus) ActionRequired() []StatusEntry

ActionRequired returns the failure entries, oldest PR first: the longer a dependency PR has been open, the more sweeps it has already survived, so the old ones are the most likely to need manual work. Entries without a known creation time sort last, in insertion order.

func (*PRStatus) Add

func (s *PRStatus) Add(pr PRInfo) int

func (*PRStatus) BlockedEntries

func (s *PRStatus) BlockedEntries() []StatusEntry

BlockedEntries returns entries whose CI could not run because a GitHub Actions budget / spending-limit block prevented every job from starting. These are deliberately kept out of ActionRequired and the failed counts: the remedy is "raise or await the Actions budget", not "rescue the code".

func (*PRStatus) CancelledEntries

func (s *PRStatus) CancelledEntries() []StatusEntry

CancelledEntries returns entries whose every failing check is a CircleCI build that CircleCI itself cancelled. Like StaleEntries these are kept out of ActionRequired and the failed counts -- the remedy is "retry the build and read the real verdict", not "rescue the code". Oldest PR first.

func (*PRStatus) FormatSummary

func (s *PRStatus) FormatSummary() string

func (*PRStatus) Len

func (s *PRStatus) Len() int

func (*PRStatus) MergedEntries

func (s *PRStatus) MergedEntries() []StatusEntry

func (*PRStatus) RefreshedEntries

func (s *PRStatus) RefreshedEntries() []StatusEntry

RefreshedEntries returns the stale entries whose branch was updated from its base during this run. Their CI is running again; the next sweep decides what they are.

func (*PRStatus) RescueAt

func (s *PRStatus) RescueAt(idx int) *RescueMarker

RescueAt returns the rescue marker attached to the entry at idx, or nil when none is attached or idx is out of range.

func (*PRStatus) RetriedEntries

func (s *PRStatus) RetriedEntries() []StatusEntry

RetriedEntries returns the cancelled entries whose builds were retried during this run. Their CI is running again; the next sweep decides what they are.

func (*PRStatus) SecurityFailedEntries

func (s *PRStatus) SecurityFailedEntries() []StatusEntry

SecurityFailedEntries returns entries that failed specifically because a security-related check reported a problem.

func (*PRStatus) SetRescue

func (s *PRStatus) SetRescue(idx int, marker *RescueMarker)

SetRescue attaches a prior rescue-attempt marker to an entry.

func (*PRStatus) SkippedEntries

func (s *PRStatus) SkippedEntries() []StatusEntry

func (*PRStatus) Snapshot

func (s *PRStatus) Snapshot() []StatusEntry

func (*PRStatus) StaleEntries

func (s *PRStatus) StaleEntries() []StatusEntry

StaleEntries returns entries whose failure is stale: the PR head is behind its base branch and every failing check is green on the base branch head. Like BlockedEntries these are kept out of ActionRequired and the failed counts -- the remedy is "update the branch and let CI re-run", not "rescue the code". Oldest PR first, like ActionRequired.

func (*PRStatus) StateAt

func (s *PRStatus) StateAt(idx int) StatusState

StateAt returns the current state of the entry at idx, or StatusPending when idx is out of range.

func (*PRStatus) Summary

func (s *PRStatus) Summary() Counts

Summary returns aggregate counts across all entries.

func (*PRStatus) Update

func (s *PRStatus) Update(idx int, state StatusState, detail string)

type RescueMarker

type RescueMarker struct {
	Tool    string    `json:"tool,omitempty"`
	Outcome string    `json:"outcome"`
	Reason  string    `json:"reason,omitempty"`
	HeadSHA string    `json:"head_sha,omitempty"`
	At      time.Time `json:"at,omitempty"`
	Fingerprint

	// Stale and Rebased are computed by MarkStale, never serialized into
	// the marker. Rebased is set when the head moved but the change did not.
	Stale   bool `json:"-"`
	Rebased bool `json:"-"`
}

RescueMarker records a prior automated rescue attempt on a PR. It is embedded as a machine-readable HTML comment inside an ordinary PR comment, so any tool that can comment on a PR (a klaus agent, a Cursor agent, a GitHub Action) can participate without coupling to marge:

<!-- ai-rescue: {"tool":"klaus","outcome":"failed","reason":"...","head_sha":"...","at":"2026-06-09T18:40:00Z"} -->

HeadSHA ties the attempt to the code it was attempted against. Renovate force-pushes on rebase or version change, so a marker whose head_sha no longer matches the PR head describes code that may no longer exist. The embedded Fingerprint (patch_id, change_id; optional, absent from markers written before it existed) tells the two apart: when the head moved but the fingerprint still matches, the branch was only rebased and the attempt still stands; otherwise it is stale and the PR is fair game for another rescue.

func ParseRescueMarker

func ParseRescueMarker(body string) *RescueMarker

ParseRescueMarker extracts the last ai-rescue marker from a comment body. It returns nil when the body contains no parseable marker; a malformed JSON payload is ignored rather than treated as an error so a mangled comment can never break a sweep.

func (*RescueMarker) CommentBody

func (m *RescueMarker) CommentBody() string

CommentBody renders the full PR comment for this marker: a human-readable summary followed by the machine-readable marker.

func (*RescueMarker) MarkStale

func (m *RescueMarker) MarkStale(currentHeadSHA string, current func() Fingerprint)

MarkStale sets Stale and Rebased by comparing the marker with the PR's current head. Same head (short-vs-full SHA prefixes match): fresh. Head moved: the marker stays fresh, flagged Rebased, when the change it was written for is still the change on the branch -- decided by the marker's fingerprint against the current one, which `current` computes on demand. It is only called when the head moved and the marker carries a fingerprint; nil means none can be computed. Otherwise the marker is stale. A marker without a recorded SHA cannot be aged out, so it stays fresh until a newer marker replaces it.

type StatusEntry

type StatusEntry struct {
	PR     PRInfo
	State  StatusState
	Detail string
	// Rescue is the most recent prior automated rescue attempt found on
	// the PR, if any. Only populated for failure-state entries.
	Rescue *RescueMarker
}

func SplitActionRequired

func SplitActionRequired(entries []StatusEntry) (security, other []StatusEntry)

SplitActionRequired partitions the action-required list into security failures and everything else, preserving the input order in each group.

type StatusState

type StatusState int
const (
	StatusPending StatusState = iota
	StatusChecking
	StatusApproving
	StatusMerging
	StatusRetrying
	StatusMerged
	StatusAlreadyMerged
	StatusAutoMerge
	StatusFailed
	StatusFailedSecurity
	StatusBlockedCI
	StatusSkipped
	StatusConflict
	StatusUntrustedAuthor
	// StatusStale marks a failing PR whose head is behind its base branch
	// and whose every failing check is green on the base branch head: the
	// failure was most likely fixed on the base branch after the PR's last
	// build, so the first move is to refresh the branch, not to rescue it.
	StatusStale
	// StatusRefreshed marks a stale PR whose branch was just updated from
	// its base (the "Update branch" button); CI is running again and the
	// next sweep decides.
	StatusRefreshed
	// StatusCancelled marks a failing PR whose every failing check is a
	// CircleCI build that CircleCI itself cancelled (a newer pipeline on the
	// branch, a redundant workflow) rather than one that failed a step.
	// There is no verdict on the code yet: the remedy is a retry, not a
	// rescue.
	StatusCancelled
	// StatusRetried marks a cancelled PR whose builds were just retried on
	// the same commit; CI is running again and the next sweep decides.
	StatusRetried
)

func (StatusState) String

func (s StatusState) String() string

type TableColumn

type TableColumn struct {
	Label string
	Width int
	Fn    func(PRInfo) string
	// Color optionally returns an ANSI color prefix for a value. The
	// value is padded to Width first, then wrapped, so colored cells
	// stay aligned with the rest of the table.
	Color func(PRInfo) string
}

func DependencySelectedColumns

func DependencySelectedColumns() []TableColumn

func FullColumns

func FullColumns() []TableColumn

func RepoSelectedColumns

func RepoSelectedColumns() []TableColumn

Jump to

Keyboard shortcuts

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