rerun

package
v1.228.0-test.39 Latest Latest
Warning

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

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

Documentation

Overview

Package rerun classifies the non-success jobs of a GitHub Actions workflow-run attempt as infrastructure zombies or real failures, and turns that classification into a rerun verdict. It backs the `go tool mage ci:classifyInfraFailures` and `ci:rerunInfraFailures` targets used by .github/workflows/rerun-infra-failures.yml (via .github/actions/classify-infra-failures), which re-runs a `Tests` run only when nothing in it genuinely failed. See docs/fixes/2026-09-03-rerun-infra-cancelled-ci-runs.md.

classify.go is deliberately HTTP-free: Classify/Verdict/WriteTSV/ WriteMarkdownSummary are pure functions of a decoded job list, which keeps the classification rules unit-testable against recorded API payloads (testdata/run-*.json) with no network or mocking involved. The network calls this package's callers need - listing a run attempt's jobs, reading a pull request's head SHA, and requesting a rerun - live in fetch.go and actions.go behind the small RESTClient interface, so those too are testable without a live GitHub API.

Package rerun is a generated GoMock package.

Index

Constants

View Source
const DefaultStuckGap = 5 * time.Minute

DefaultStuckGap is the minimum delay between a cancelled job's last step and its job-level completion for it to count as runner-stuck-after-complete. Ordinary cancellations land within seconds; reaped zombies take 30 minutes or more.

Variables

This section is empty.

Functions

func PRHeadSHA

func PRHeadSHA(ctx context.Context, client RESTClient, repo string, number int) (string, error)

PRHeadSHA returns the current head commit SHA of pull request number in repo (owner/name form).

func RerunFailedJobs

func RerunFailedJobs(ctx context.Context, client RESTClient, repo, runID string) (stillRunning bool, err error)

RerunFailedJobs requests a rerun of every non-success job in runID (repos/{repo}/actions/runs/{runID}/rerun-failed-jobs), mirroring `gh run rerun <runID> --failed`. When the API rejects the request because the run has not finished yet, it reports stillRunning=true instead of returning an error.

func RunAttempt

func RunAttempt(ctx context.Context, client RESTClient, repo, runID string) (int, error)

RunAttempt returns runID's current attempt number (repos/{repo}/actions/runs/{id}). Callers gating on an attempt-count cap should call this immediately before acting on it rather than trusting an attempt number from an earlier event payload: `gh run rerun`/rerun-failed-jobs creates a new attempt from the run's latest execution regardless of which attempt triggered the caller, so a stale count can under-count how many attempts already exist.

func WriteMarkdownSummary

func WriteMarkdownSummary(w io.Writer, run RunRef, classified []Classified, outcome Outcome, reason string) error

WriteMarkdownSummary renders a classification as a GitHub Actions step-summary block: a header linking to the run, a Markdown table of non-success jobs, and the verdict line.

func WriteTSV

func WriteTSV(w io.Writer, classified []Classified) error

WriteTSV writes one `job<TAB>conclusion<TAB>class` line per entry.

Types

type Class

type Class string

Class is the verdict for one non-success job.

const (
	// ClassRunnerStuckAfterComplete is a cancelled job whose every step
	// succeeded (or was skipped) but whose job-level completion landed at
	// least Options.StuckGap after its last step: the runner finished all its
	// work, never reported completion, and GitHub reaped it. On Windows this
	// is harden-runner's post step killing its agent mid DNS-restore.
	ClassRunnerStuckAfterComplete Class = "runner-stuck-after-complete"
	// ClassRunnerLost is a failed job with no failed step: the runner vanished.
	ClassRunnerLost Class = "runner-lost"
	// ClassCheckCascade is a failed job whose only failed steps are aggregator
	// steps named in checkResultStepNames (test.yml's test-required, k3s and
	// terraform-registry-cache verdict jobs). Those steps only inspect other
	// jobs' conclusions and never run tests, so they fail as a consequence of
	// a zombie upstream job. Coupled to test.yml's step-naming convention.
	ClassCheckCascade Class = "check-cascade"
	// ClassSuperseded is any other cancellation: a step was interrupted or the
	// job never started, because a newer run replaced it or a human cancelled.
	ClassSuperseded Class = "superseded"
	// ClassRealFailure is everything else: a step genuinely failed.
	ClassRealFailure Class = "real-failure"
)

type Classified

type Classified struct {
	Job        string
	Conclusion string
	Class      Class
}

Classified is the class assigned to one non-success job.

func Classify

func Classify(jobs []Job, opts Options) []Classified

Classify returns one Classified entry per job whose conclusion is not success, skipped or empty (still running), in input order.

type Job

type Job struct {
	Name        string     `json:"name"`
	Status      string     `json:"status"`
	Conclusion  string     `json:"conclusion"`
	CompletedAt *time.Time `json:"completed_at"`
	Steps       []Step     `json:"steps"`
}

Job is one job of a workflow-run attempt as returned by the GitHub jobs API.

func DecodeJobs

func DecodeJobs(r io.Reader) ([]Job, error)

DecodeJobs reads the jobs of a run attempt from r. It accepts the raw `{"jobs":[...]}` response, several such pages concatenated (the shape of `gh api ... --paginate` output) or a bare array of jobs.

func FetchJobs

func FetchJobs(ctx context.Context, client RESTClient, repo, runID, runAttempt string) ([]Job, error)

FetchJobs pages through repos/{repo}/actions/runs/{runID}/attempts/{runAttempt}/jobs (per_page=100), following the Link header until it stops offering a "next" page - the same request `gh api ... --paginate` makes. It is the only exported function in this package that talks to the network on the read path; classify.go's Classify/Verdict/etc. remain pure.

type MockRESTClient

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

MockRESTClient is a mock of RESTClient interface.

func NewMockRESTClient

func NewMockRESTClient(ctrl *gomock.Controller) *MockRESTClient

NewMockRESTClient creates a new mock instance.

func (*MockRESTClient) EXPECT

EXPECT returns an object that allows the caller to indicate expected use.

func (*MockRESTClient) RequestWithContext

func (m *MockRESTClient) RequestWithContext(ctx context.Context, method, path string, body io.Reader) (*http.Response, error)

RequestWithContext mocks base method.

type MockRESTClientMockRecorder

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

MockRESTClientMockRecorder is the mock recorder for MockRESTClient.

func (*MockRESTClientMockRecorder) RequestWithContext

func (mr *MockRESTClientMockRecorder) RequestWithContext(ctx, method, path, body any) *gomock.Call

RequestWithContext indicates an expected call of RequestWithContext.

type Options

type Options struct {
	// StuckGap overrides DefaultStuckGap when positive.
	StuckGap time.Duration
}

Options tunes classification.

type Outcome

type Outcome string

Outcome is the rerun verdict for a whole run attempt.

const (
	// OutcomeRerun means every non-success job is a zombie or a cascade of one.
	OutcomeRerun Outcome = "rerun"
	// OutcomeNoRerun means at least one job vetoes a rerun, or nothing is a zombie.
	OutcomeNoRerun Outcome = "no-rerun"
	// OutcomeNoJobs means the attempt has no non-success jobs at all.
	OutcomeNoJobs Outcome = "no-jobs"
)

func Verdict

func Verdict(classified []Classified) (Outcome, string)

Verdict decides whether a run attempt should be rerun: only when at least one job is runner-stuck-after-complete or runner-lost and every other one is check-cascade. Any real-failure or superseded job vetoes the rerun. The reason is a short human-readable justification for the log.

type RESTClient

type RESTClient interface {
	RequestWithContext(ctx context.Context, method, path string, body io.Reader) (*http.Response, error)
}

RESTClient is the slice of *api.RESTClient's (github.com/cli/go-gh/v2/pkg/api) method set that this package needs: one authenticated request, returning the raw response so callers can read pagination headers. Its narrowness lets tests inject a fake instead of a live GitHub API.

type RunRef

type RunRef struct {
	Repo       string
	RunID      string
	RunAttempt string
}

RunRef identifies the run/attempt a WriteMarkdownSummary header links to.

type Step

type Step struct {
	Name       string `json:"name"`
	Status     string `json:"status"`
	Conclusion string `json:"conclusion"`
	// CompletedAt is nil when the API returns null. encoding/json parses
	// RFC 3339 timestamps with or without fractional seconds.
	CompletedAt *time.Time `json:"completed_at"`
}

Step is one step of a job as returned by the GitHub jobs API.

Jump to

Keyboard shortcuts

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