Documentation
¶
Overview ¶
Package ci holds the checks that are about the REPO rather than about any one binary: properties every command under cmd/ must have, asserted by walking the directory rather than by listing the commands, so that a binary added tomorrow is held to them on the day it appears rather than on the day somebody remembers to add it to a list.
It has no exported API. See onboarding_test.go, and docs/ONBOARDING.md for the standard it enforces.
Index ¶
- Constants
- Variables
- func ParseJobLog(job, text string) ([]TestFailure, []Timeout)
- func StripLogLine(line string) string
- type Cancellation
- type CardDone
- type DevMoved
- type EventSink
- type FailForge
- type FailedJob
- type FailedReport
- type FailedStep
- type Forge
- type GHFailForge
- type GHForge
- type Gate
- type GoEnvFinding
- type GoEnvResult
- type NetFinding
- type NetResult
- type PRChecksDone
- type PRState
- type Producer
- type Reactor
- type RebaseWanted
- type RunSelector
- type Snapshot
- type TemplateFinding
- type TemplatesResult
- type TestFailure
- type Timeout
- type Unread
- type WaitFinding
- type WaitsResult
Constants ¶
const ( // GoEnvRemedy is the one thing to do about a go command that inherits the // caller's environment. GoEnvRemedy = "set cmd.Env = goenv.Clean(os.Environ())" // GoEnvRemedyAllow is the one thing to do about an allowlist entry that // names no offender: the list only shrinks. GoEnvRemedyAllow = "delete the stale row; the allowlist only shrinks" )
const ( // NetRemedy is the one thing to do about a real host in a unit test. NetRemedy = "mock the endpoint with httptest or a local fake" // NetRemedyAllow is the one thing to do about an allowlist entry that names // no offender: the file only shrinks. NetRemedyAllow = "fix the host; the allowlist only shrinks" )
const ( // TemplateRemedyJoin is the one thing to do about a raw filepath.Join. TemplateRemedyJoin = "wrap the path in strconv.Quote" // TemplateRemedyLiteral is the one thing to do about an OS path literal. TemplateRemedyLiteral = "wrap the path in strconv.Quote; a backslash there is an escape the literal does not have" // TemplateRemedyAllow is the one thing to do about an allowlist entry that // names no offender: the file only shrinks. TemplateRemedyAllow = "quote the path; the allowlist only shrinks" )
const ( // WaitRemedySleep is the one thing to do about a fixed sleep. WaitRemedySleep = "poll for the event up to NOVA_TEST_WAIT, not a fixed sleep" // WaitRemedyBound is the one thing to do about a short context or timer bound. WaitRemedyBound = "raise the bound to thirty seconds or more, or inject a fake clock" // WaitRemedyElapsed is the one thing to do about an elapsed-time assertion. WaitRemedyElapsed = "assert the event, not the clock; use a fake clock if the code needs one" // WaitRemedyAllow is the one thing to do about an allowlist entry that // names no offender: the file only shrinks. WaitRemedyAllow = "fix the wait; the allowlist only shrinks" )
const ( ChannelCardDone = "card-done" ChannelPRChecksDone = "pr-checks-done" ChannelDevMoved = "dev-moved" ChannelRebaseWanted = "rebase-wanted" )
The pub/sub channels. card-done is a republish of the cards:done stream; the other two are the gh fallback's heartbeat and the rebase request the rebase verb consumes.
const ( StreamCardsDone = "cards:done" GroupEvents = "events" )
The stream, group and keys the bridge shares with the rest of the family. The stream is the record of finished cards; the group is this bridge's cursor, so a restart resumes where it stopped instead of replaying the stream.
const ( ConclusionSuccess = "SUCCESS" ConclusionFailure = "FAILURE" )
The check-suite conclusions this vocabulary distinguishes. PENDING and every not-yet-complete state are not events and are never published.
const ( SourceEvents = "nova-work" VerbEvents = "events" )
SourceEvents and VerbEvents are the source and verb labels every line the bridge writes carries. They are the tool and the verb a reader types, so `{source="nova-work", verb="events"}` is exactly "what did the bridge do".
const ( EventStart = "start" EventDone = "done" EventRefuse = "refuse" )
The verb-level kinds, the SPEC-LOGS spine. A bridge that started and never finished is a start with no done, which is the whole hang test.
const DefaultFailedMaxLines = 8
DefaultFailedMaxLines is how many of a test's own message lines print before the rest are counted rather than shown. Cap and count: the count is the truth about the test whether or not the lines printed.
const FailedVerbLine = "failed read a run's failing jobs; print the failing tests, their file and line"
FailedVerbLine is the help line this verb is entered under, word for word as docs/SPEC-CI.md prints it.
const GoEnvVerbLine = "goenv read every .go under cmd/ and internal/; refuse a child `go` that inherits the caller's environment"
GoEnvVerbLine is the help line the class test is entered under.
const NetVerbLine = "net read every _test.go on the CI path; refuse a real network host or host:port"
NetVerbLine is the help line the class test is entered under, word for word as docs/SPEC-CI.md prints it.
const TemplatesVerbLine = "templates read every _test.go; refuse a filesystem path unquoted in a JSON or template literal"
TemplatesVerbLine is the help line the class test is entered under, word for word as docs/SPEC-CI.md prints it.
const WaitsVerbLine = "waits read every _test.go on the CI path; refuse a fixed wall-clock wait or bound"
WaitsVerbLine is the help line the class test is entered under, word for word as docs/SPEC-CI.md prints it.
Variables ¶
var ErrBadPayload = errors.New("the payload is not the JSON this channel carries")
ErrBadPayload is what a message this reactor could not read is wrapped in.
EDGE 20: one malformed payload killed the whole reactor -- `REACT FAIL`, exit 1 -- although an UNKNOWN CHANNEL is deliberately ignored, so a subscriber that survives a channel it has never heard of died on one bad byte from a channel it knows. A message that is not the JSON its channel promises is one message dropped, said out loud and counted on the closing line; everything else -- a bus that cannot be read, a forge that will not answer, a queue that cannot be written -- is still a failure, because those are the reactor's own edges rather than one publisher's typo.
Functions ¶
func ParseJobLog ¶
func ParseJobLog(job, text string) ([]TestFailure, []Timeout)
ParseJobLog reads one job's raw log -- a self-hosted runner's plain `go test` output, a hosted runner's `go test -json` frames, or a log holding both -- and returns the failing tests and the timeouts it names, in the order the log named them.
func StripLogLine ¶
StripLogLine turns one raw line of a GitHub Actions log into the line `go test` wrote: no carriage return, no ANSI colour, no runner timestamp.
Types ¶
type Cancellation ¶
Cancellation is one step a run cancelled out from under, with how long it had been running. A cancelled sibling is not a failure, but a reader who does not see it will look for a red that is not there.
func CancelledSteps ¶
func CancelledSteps(job FailedJob) []Cancellation
CancelledSteps reads a job's own steps rather than its log: a cancellation leaves nothing in the text, and the step record carries both the name and the clock.
type DevMoved ¶
type DevMoved struct {
SHA string `json:"sha"`
}
DevMoved is the base branch's head changing to a new sha.
type EventSink ¶
EventSink is the writer a caller hands the producer. It exists as a name so the verb's two shapes -- stderr, and the file --log names -- read as the one decision they are.
type FailForge ¶
type FailForge interface {
// ResolveRun turns the caller's words into one run id.
ResolveRun(sel RunSelector) (int64, error)
// Jobs lists the run's jobs, in the forge's own order.
Jobs(runID int64) ([]FailedJob, error)
// JobLog is one job's whole log, raw: timestamps, ANSI and all.
JobLog(jobID int64) (string, error)
}
FailForge is what `nova-ci failed` needs from a forge, and all of it.
type FailedJob ¶
type FailedJob struct {
ID int64
Name string
Conclusion string // success, failure, cancelled, timed_out, skipped, ...
Steps []FailedStep
}
FailedJob is one job of a run.
type FailedReport ¶
type FailedReport struct {
Jobs int // failing jobs this report looked at, read or not
Failures []TestFailure
Cancels []Cancellation
Timeouts []Timeout
Unread []Unread
}
FailedReport is everything one run's failing jobs said.
func ReadFailedRun ¶
func ReadFailedRun(f FailForge, sel RunSelector, jobFilter string) (int64, FailedReport, error)
ReadFailedRun is the verb's whole engine: resolve the run, read every job that did not succeed, and fold the logs into one report. jobFilter keeps only the jobs whose name contains it; empty keeps them all.
func (FailedReport) Empty ¶
func (r FailedReport) Empty() bool
Empty is true when the run's failing jobs held nothing this tool recognises. A log it could not read is NOT nothing: it is the one thing a reader must be told about, or they will read a short report as a small failure.
func (FailedReport) ExitCode ¶
func (r FailedReport) ExitCode() int
ExitCode is 1 when the run said anything red, 0 when it said nothing. A refusal is the caller's exit 2 and never reaches here.
func (FailedReport) Lines ¶
func (r FailedReport) Lines(maxLines int) []string
Lines renders the whole report: one block per failing test, then the cancellations, the timeouts and the logs it could not read, then the summary. maxLines bounds each test's own output; zero or less means the default rather than unlimited, because an unbounded log is the thing this tool exists to replace.
A job name and a step name are QUOTED, not escaped as fields: `test (3/4 studio)` is what the reader pastes back into --job, and oneline.Field would hand them `test\x20(3/4\x20studio)`, which the forge has never heard of. A package, a test name and a file:line hold no space, so they are fields and stay bare for a grep.
func (FailedReport) SummaryLine ¶
func (r FailedReport) SummaryLine() string
SummaryLine is the closing line: the failing jobs looked at and the failing tests found, whether or not every line printed. A job whose log could not be read is counted in unread= so the reader knows the report is short for a reason, and the field is omitted when there is nothing to say.
type FailedStep ¶
FailedStep is one step of a job: enough to say what a cancellation cut short.
type Forge ¶
Forge is the edge between the bridge and the host. It is an interface for the reason merge.Host is: the tests need an edge that cannot reach the network, and the one implementation that shells to gh is then a thing a reader can check line by line.
type GHFailForge ¶
type GHFailForge struct {
Repo string // <owner>/<name>
GH string // the gh executable, "gh" unless the caller names another
Timeout time.Duration
}
GHFailForge is the one FailForge that shells to gh. Every invocation it makes is in this file, so a reader can check them all at once, and none of them writes: this verb reads a run and says what it found.
func NewGHFailForge ¶
func NewGHFailForge(repo, ghPath string, timeout time.Duration) *GHFailForge
NewGHFailForge returns a forge over one repository.
func (*GHFailForge) JobLog ¶
func (g *GHFailForge) JobLog(jobID int64) (string, error)
JobLog fetches one job's whole log. gh refuses to hand over a log holding terminal escape sequences unless it is told to, and a CI log always holds them, so the flag is the normal call; a gh too old to know it is asked again without.
func (*GHFailForge) Jobs ¶
func (g *GHFailForge) Jobs(runID int64) ([]FailedJob, error)
Jobs lists the run's jobs, paging until the forge's own total_count is met.
func (*GHFailForge) ResolveRun ¶
func (g *GHFailForge) ResolveRun(sel RunSelector) (int64, error)
ResolveRun turns the selector into a run id.
type GHForge ¶
type GHForge struct {
Repo string // <owner>/<name>
Base string // the base branch, "dev" unless the lane says otherwise
Prefix string // the head branch prefix that marks this fleet's PRs
Timeout time.Duration
}
GHForge reads one repository's open rowan/* pull requests and its base head through gh.
func NewGHForge ¶
NewGHForge returns a forge over one repository and base branch.
type Gate ¶
type Gate interface {
// Skipped reports whether this pull request is one the queue must leave alone --
// skipped or parked.
Skipped(pr int) (bool, error)
// Held reports whether a hold stands over the whole queue, and its reason.
Held() (held bool, reason string, err error)
}
Gate is THE ONE HOLD AND THE ONE SKIP SET the reactor obeys. nova-merge react implements it over the lane's `<lane>/hold` and `<lane>/queue.json`, which is exactly what `nova-merge queue hold` writes and `nova-merge queue skip` writes, so the two verbs and the reactor can never disagree about whether a pull request may be enqueued.
A nil Gate is an OPEN gate and is only ever nil in a test that is about something else.
type GoEnvFinding ¶
GoEnvFinding is one child `go` that inherits the caller's environment, with its file, line, the function it stands in and the one thing to do about it. Kind is "inherit" for a go command with no sanitized environment, or "allowlist" for a row that names no offender.
func (GoEnvFinding) Render ¶
func (f GoEnvFinding) Render() string
Render is the one-line refusal for this finding.
type GoEnvResult ¶
type GoEnvResult struct {
Files int
Allowlisted int
Findings []GoEnvFinding
Stale []GoEnvFinding
}
GoEnvResult is one run of the checker: the files read, the allowlist entries still honored, the offenders left, and the entries that name no offender.
func CheckGoEnv ¶
func CheckGoEnv(root, allowlistPath string) (GoEnvResult, error)
CheckGoEnv reads every .go file under root/internal and root/cmd and returns the go commands that inherit the caller's environment, the allowlist entries honored, and any entry that names no offender. The tree comes from the caller, never from a walk of the repository; testdata directories and internal/goenv itself are skipped.
func (GoEnvResult) ExitCode ¶
func (r GoEnvResult) ExitCode() int
ExitCode is the status the check would exit with: 2 when anything is refused, 0 when the tree is clean.
func (GoEnvResult) FailLine ¶
func (r GoEnvResult) FailLine() string
FailLine closes a refusal with the counts.
func (GoEnvResult) OKLine ¶
func (r GoEnvResult) OKLine() string
OKLine is the one line a clean run prints.
func (GoEnvResult) Refused ¶
func (r GoEnvResult) Refused() int
Refused is the number of lines the run would print: offenders plus stale allowlist entries.
type NetFinding ¶
NetFinding is one real host, with its file, line, host, kind and the one thing to do about it. Kind is "url" for an http(s) URL, "hostport" for a bare host:port, or "allowlist" for a stale entry.
func (NetFinding) Render ¶
func (f NetFinding) Render() string
Render is the one-line refusal for this finding.
type NetResult ¶
type NetResult struct {
Tests int
Allowlisted int
Findings []NetFinding
Stale []NetFinding
}
NetResult is one run of the checker: the tests read, the allowlist entries still honored, the real hosts left, and the allowlist entries that name no offender.
func CheckNet ¶
CheckNet reads every _test.go under root/internal and root/cmd and returns the real hosts, the allowlist entries honored, and any allowlist entry that names no offender. The tree comes from the caller, never from a walk of the repository; it reuses the waits checker's file walk (testdata directories are skipped so the fixtures are never read as offenders) and its allowlist reader, whose `file:line kind date reason` rows are general enough for both.
func (NetResult) ExitCode ¶
ExitCode is the status the check would exit with: 2 when anything is refused, 0 when the path is clean.
type PRChecksDone ¶
type PRChecksDone struct {
Number int `json:"number"`
Head string `json:"head"`
Conclusion string `json:"conclusion"`
}
PRChecksDone is one completed check suite on an open rowan/* pull request.
type PRState ¶
type PRState struct {
Number int
Branch string
Head string
Conclusion string // SUCCESS, FAILURE, PENDING or ""
Mergeable string // CLEAN, DIRTY, UNKNOWN or ""
}
PRState is one open pull request as the forge sees it, folded to the three facts the bridge needs: where its head is, what its checks concluded, and whether a move of the base left it DIRTY. It is deliberately smaller than merge.PR: this is the event edge, not the merge condition.
type Producer ¶
type Producer struct {
RDB *redis.Client
Forge Forge
Consumer string
Log io.Writer
Clock func() time.Time
// Events is the structured sink of SPEC-LOGS.md Part 2: one JSON line per event
// published, written beside the human line and never instead of it. A nil sink
// writes nothing. Bench is the fleet name that becomes the bench label, and GUID
// is this process run's guid source, which a test injects so it never reads /proc.
Events io.Writer
Bench string
GUID func() string
// contains filtered or unexported fields
}
Producer reads the stream and polls the forge. Its maps are the change detector and live only in this process: the durable cursor is the consumer group, not a file.
func NewProducer ¶
NewProducer returns a producer with the given identity. A nil log discards the lines.
func (*Producer) Announce ¶
Announce writes one verb-level line -- start, done or refuse -- with the elapsed time and, on a failure, the error. It is exported because the verb's own spine is written by cmd/nova-work, where the flags and the deadline live, while the per-event lines are written here, where the publishes happen.
func (*Producer) PollOnce ¶
PollOnce reads the forge once and publishes what changed. It returns how many messages it published, so --once can say so and a test can assert zero for a quiet poll.
func (*Producer) PublishCardsDone ¶
PublishCardsDone reads the unclaimed entries of cards:done once and republishes each as card-done, acking it after the publish. It returns how many it published.
type Reactor ¶
type Reactor struct {
RDB *redis.Client
Forge Forge
// Gate is the hold and the skip set; see Gate.
Gate Gate
// Enqueue is the ONE DOOR. It is the caller's, because the caller is the one that
// knows where the queue is: nova-merge react writes the lane's queue.json.
Enqueue func(ctx context.Context, pr int, head string) error
Log io.Writer
// Dropped counts the messages this reactor could not read and carried on past.
Dropped int
// contains filtered or unexported fields
}
Reactor is the stateful subscriber. sent remembers the rebase-wanted it already published per head, so a republished dev-moved does not cut the same rebase twice.
func NewReactor ¶
func NewReactor(rdb *redis.Client, forge Forge, gate Gate, enqueue func(context.Context, int, string) error, log io.Writer) *Reactor
NewReactor returns a reactor over one bus, one forge, one gate and one door.
THE DOOR IS NOT A MERGE QUEUE, AND THERE IS EXACTLY ONE OF EACH. What react enqueues is the session's list of pull requests whose checks came back green, read by whoever builds the next batch -- and that list is the LANE'S `queue.json`, the file `nova-merge queue` writes, `nova-merge queue status` prints and `nova-merge run` walks. It used to be the redis set `merge:queue`, which nothing in this tree ever read (edge 17): react reported `REACT enqueue pr=N` at exit 0 into a hole, and the batch builder it was supposedly for had no way to see it. Admission to the FORGE's merge queue is a different door altogether -- internal/merge.Enqueuer.Enqueue and its one caller `nova-merge land`, which takes a batch and nothing else (Glenn, 2026-09-18) -- and this reactor never reaches it. A caller that injected an Enqueue reaching a forge directly would be a second entrance, and internal/ci's class rule refuses the spellings that used to build one.
A nil enqueue is therefore NOT a default door any more: it is a reactor with nowhere to put a pull request, and it says so on the first green one rather than reporting an enqueue about a set nobody reads.
func (*Reactor) Handle ¶
Handle acts on one message. The channel names the action; an unknown channel is ignored, because a subscriber that refused a message it does not know would die on the first channel another card adds.
type RebaseWanted ¶
RebaseWanted asks the rebase verb to cut a unit for a PR a merge made DIRTY.
type RunSelector ¶
RunSelector is the one run the caller means, in the words they used: a run id, a pull request, that pull request's merge-queue run, or a branch.
type Snapshot ¶
Snapshot is the forge's answer to one poll: the open rowan/* pull requests and the base branch's current head.
type TemplateFinding ¶
type TemplateFinding struct {
File string
Line int
Kind string // "join", "literal", or "allowlist" for a stale entry
Remedy string
}
TemplateFinding is one unquoted path, with its file, line, kind and the one thing to do about it.
func (TemplateFinding) Render ¶
func (f TemplateFinding) Render() string
Line renders the one-line refusal for this finding.
type TemplatesResult ¶
type TemplatesResult struct {
Tests int
Allowlisted int
Findings []TemplateFinding
Stale []TemplateFinding
}
TemplatesResult is one run of the checker: the tests read, the allowlist entries still honored, the offenders left, and the allowlist entries that name no offender.
func CheckTemplates ¶
func CheckTemplates(root, allowlistPath string) (TemplatesResult, error)
CheckTemplates reads every _test.go under root and returns the offenders, the allowlist entries honored, and any allowlist entry that names no offender. The tree comes from the caller, never from a walk of the repository; testdata directories are skipped so the fixtures are never read as offenders.
func (TemplatesResult) ExitCode ¶
func (r TemplatesResult) ExitCode() int
ExitCode is the status the check would exit with: 2 when anything is refused, 0 when the tree is clean.
func (TemplatesResult) FailLine ¶
func (r TemplatesResult) FailLine() string
FailLine closes a refusal with the counts.
func (TemplatesResult) OKLine ¶
func (r TemplatesResult) OKLine() string
OKLine is the one line a clean run prints.
func (TemplatesResult) Refused ¶
func (r TemplatesResult) Refused() int
Refused is the number of lines the run would print: offenders plus stale allowlist entries. The count is the truth about the tree whether or not the lines printed.
type TestFailure ¶
type TestFailure struct {
Job string
Package string
Test string
At string // "file_test.go:73", or "" when the test printed no position
Lines []string // the test's own message lines, in the order it printed them
}
TestFailure is one failing test: the job it failed in, its package, its name, the first file:line its own output named, and that output.
type Timeout ¶
Timeout is one `panic: test timed out after <d>` with the tests that were still running when the alarm went off. Those tests are suspects, not verdicts.
type Unread ¶
Unread is one job whose log the forge would not hand over, with the reason it gave. It is a LINE, never a refusal that sinks the run: everything the other jobs said is still what the caller asked for. The specimen is a job cancelled while its run is still in progress, whose log blob the forge answers 404 for until the run finishes.
type WaitFinding ¶
WaitFinding is one fixed wait, with its file, line, kind and the one thing to do about it. Kind is "sleep", "bound", "elapsed", or "allowlist" for a stale entry.
func (WaitFinding) Render ¶
func (f WaitFinding) Render() string
Render is the one-line refusal for this finding.
type WaitsResult ¶
type WaitsResult struct {
Tests int
Allowlisted int
Findings []WaitFinding
Stale []WaitFinding
}
WaitsResult is one run of the checker: the tests read, the allowlist entries still honored, the fixed waits left, and the allowlist entries that name no offender.
func CheckWaits ¶
func CheckWaits(root, allowlistPath string) (WaitsResult, error)
CheckWaits reads every _test.go under root/internal and root/cmd and returns the fixed waits, the allowlist entries honored, and any allowlist entry that names no offender. The tree comes from the caller, never from a walk of the repository; testdata directories are skipped so the fixtures are never read as offenders.
func (WaitsResult) ExitCode ¶
func (r WaitsResult) ExitCode() int
ExitCode is the status the check would exit with: 2 when anything is refused, 0 when the path is clean.
func (WaitsResult) FailLine ¶
func (r WaitsResult) FailLine() string
FailLine closes a refusal with the counts.
func (WaitsResult) OKLine ¶
func (r WaitsResult) OKLine() string
OKLine is the one line a clean run prints.
func (WaitsResult) Refused ¶
func (r WaitsResult) Refused() int
Refused is the number of lines the run would print: offenders plus stale allowlist entries. The count is the truth about the CI path whether or not the lines printed.