Help us shape the future of Go! Take the 2026 Go Developer Survey

quiet

package
v0.69.0 Latest Latest
Warning

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

Go to latest
Published: Nov 1, 2026 License: BSD-3-Clause Imports: 7 Imported by: 0

Documentation

Overview

Package quiet decides whether a scheduled runner has stopped running.

The decision lives here, away from the network, because it is the part that can be wrong in a way nobody notices: a watcher that says "all quiet" when a runner died is indistinguishable from a working one until the day it matters.

Index

Constants

View Source
const AppCoveredBy = "the Renovate App"

AppCoveredBy is what the covered line says when an App is the reason.

View Source
const DefaultPushWarn = 45 * 24 * time.Hour

DefaultPushWarn is when to start warning about the 60-day clock. All runner repositories here are public, and a .github repository whose only content is the runner receives no pushes except Renovate's own -- so if Renovate stops, the repository goes quiet, and at day 60 GitHub disables the schedule, which makes the silence permanent. Fifteen days of warning before that latch.

View Source
const DefaultSlack = 12 * time.Hour

DefaultSlack is how far behind its own schedule a run may be before the runner counts as stopped.

Twelve hours is not a round number chosen for comfort. GitHub's scheduler was measured on this fleet running 1h59m to 7h56m behind its crons and still growing; a one-hour tolerance would have paged on 113 runners that were working perfectly, and eight hours left four minutes of headroom against the worst lag observed. A pager that goes off on a fleet with nothing wrong is the same failure as an informational section nobody reads, and it costs more.

quietscan prints the observed lag distribution on every pass, and says so out loud when the worst lag passes three quarters of this, so the number keeps being re-measured rather than becoming folklore.

View Source
const InactivityLimit = 60 * 24 * time.Hour

InactivityLimit is GitHub's rule for public repositories: 60 days with no repository activity and scheduled workflows are switched off.

View Source
const NarrowedWhy = "narrowed pass: filters and Apps of organisations outside -orgs were not read"

NarrowedWhy is why a narrowed pass cannot answer "no runner reaches this".

Variables

This section is empty.

Functions

func Failed added in v0.65.0

func Failed(conclusion string) bool

Failed says whether a run's `conclusion` means the runner did not do its job.

It is an ALLOWLIST OF FAILURES, not "anything that is not success", and the difference is the point: `cancelled` is usually a person, `skipped` is a condition that did not hold, `neutral` is a run that declined to judge, and paging on any of those is how a watcher earns being ignored. They fall through to the timeliness verdicts and the printed line still shows the word, so a reader can see a cancellation without being woken for it.

An empty conclusion is NOT a failure: it means nobody read the field.

func MatchesOrg

func MatchesOrg(pattern, org string) bool

MatchesOrg reports whether one autodiscoverFilter pattern reaches an organisation. Only the owner half of "owner/repo" decides that: a runner that watches `go-ruby-*/**` reaches every go-ruby-* organisation, whatever it then does per repository.

func MissingSecrets added in v0.66.0

func MissingSecrets(referenced, repoSecrets, orgSecrets []string) []string

MissingSecrets returns the referenced names that appear in NEITHER the repository's nor the organisation's secret listing.

⚠ IT IS DELIBERATELY BIASED TOWARDS SAYING NOTHING. An organisation secret can be scoped to selected repositories, and this does not ask which -- so a secret listed at the organisation but not shared with THIS repository reads here as present. That is a false negative, and it is the right direction for a check that pages: the `broken` verdict still catches the run that fails, whereas a watcher that cries wolf over organisation-wide secrets gets muted and then catches nothing at all.

The caller must not call this at all when either listing could not be read. An unread listing is unread, never empty -- the whole fleet's rule.

func ParseAutodiscoverFilter

func ParseAutodiscoverFilter(js string) ([]string, string)

ParseAutodiscoverFilter reads the patterns out of a Renovate config.js.

It reads only a LITERAL array. `go-attest/renovate-runner` builds its filter with `mine.map((o) => `${o}/**`)` over an inline list sliced by an env var, and no amount of string scanning makes that knowable. The second return says why it could not be read, and an unreadable filter is reported as unknown coverage -- never as no coverage, which is the same mistake in a smaller coat.

func SecretsReferenced added in v0.66.0

func SecretsReferenced(workflow string) []string

SecretsReferenced returns the secret names a workflow asks GitHub to inject, sorted and deduplicated, excluding the ones GitHub provides by itself.

func SecretsSummary added in v0.66.0

func SecretsSummary(missing []string) string

SecretsSummary renders the finding for a report line.

func Streak added in v0.65.0

func Streak(runs []RunRecord) (failures int, sawSuccess bool)

Streak counts, newest first, how many runs failed in a row, and says whether a success appears anywhere in the window at all.

A run still in flight is SKIPPED rather than counted either way: it has not failed, and it is not a success that would end the streak. A cancellation ends the streak without being part of it -- the run did not fail, so claiming an unbroken run of failures through it would overstate the evidence.

func UnreadFilters

func UnreadFilters(rs []Result) []string

UnreadFilters names every LIVE runner whose autodiscoverFilter could not be read. A disabled runner's unread filter creates no doubt, because a disabled runner covers nothing either way.

Types

type AppCoverage

type AppCoverage int

AppCoverage is what a read of `orgs/<org>/installations` said. A GitHub App installation covers an organisation through a mechanism no workflow census can see: there is no runner, no cron and no repository to be quiet, and Renovate opens pull requests all the same.

const (
	// AppUnread: the installations endpoint was not readable. The token may
	// simply not see installations on this organisation.
	AppUnread AppCoverage = iota
	// AppAbsent: read, and no Renovate App is installed.
	AppAbsent
	// AppPresent: read, and the Renovate App is installed.
	AppPresent
)

type Cron

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

Cron is one POSIX five-field schedule line, evaluated in UTC -- which is what GitHub Actions uses regardless of where the repository or its owner is.

func ParseCron

func ParseCron(expr string) (*Cron, error)

ParseCron reads one cron line: five space-separated fields, each a list of "*", "n", "a-b", or any of those with a "/step" suffix.

func (*Cron) Next

func (c *Cron) Next(t time.Time) (time.Time, bool)

Next returns the first occurrence strictly after t.

func (*Cron) Prev

func (c *Cron) Prev(t time.Time) (time.Time, bool)

Prev returns the last occurrence at or before t. It is what turns a run timestamp into the lag behind the schedule that asked for it.

func (*Cron) String

func (c *Cron) String() string

type LagStats

type LagStats struct {
	N                     int
	Min, Median, P90, Max time.Duration
}

LagStats summarises how far behind their crons the runs actually started. Printing this every pass is what keeps the slack an observation rather than a belief: if the distribution creeps towards the threshold, the threshold is wrong before it pages on healthy runners.

func Lags

func Lags(rs []Result) LagStats

Lags summarises the lag of every result that has one.

type Result

type Result struct {
	Runner
	Verdict Verdict
	// Due is the occurrence the runner owes: the next one after its last run,
	// or after its birth if it has never run.
	Due time.Time
	// Late is how far past Due+slack the clock now is. Positive only when the
	// verdict pages.
	Late time.Duration
	// Lag is how far behind its own cron the last run actually started. This is
	// the measurement that justifies the slack, so it is collected on healthy
	// runners too.
	Lag      time.Duration
	HasLag   bool
	DaysIdle float64
	PushWarn bool
	// CoveredBy is the shared runner that reaches this organisation, filled in
	// fleet-wide by ApplyCoverage.
	CoveredBy        string
	CoveredByVerdict Verdict
	// MissingSecrets are the referenced secret names that exist nowhere this
	// tool can see. Empty whenever the listings could not be read.
	MissingSecrets []string
	// UnknownWhy says what could not be read, when the verdict is
	// CoverageUnknown. A watcher that cannot say WHY it does not know is not
	// much better than one that guesses.
	UnknownWhy string
}

Result is one runner's verdict and the numbers behind it.

func ApplyAppCoverage

func ApplyAppCoverage(rs []Result, app map[string]AppCoverage) []Result

ApplyAppCoverage is the third and last form of coverage, after an organisation's own runner and another runner's autodiscoverFilter. It is consulted ONLY for organisations still reading NoRunner, which is both cheaper and the only place it can change an answer.

An organisation whose installations could not be read does not become "uncovered": it becomes CoverageUnknown, with a line of its own. Reading a refusal as an absence is how openweft -- which has the App installed and 16 open pull requests from it -- was reported as a gap.

func ApplyCoverage

func ApplyCoverage(rs []Result) []Result

ApplyCoverage resolves, fleet-wide, which runner reaches each organisation, and turns "this organisation has no runner" into "this organisation is covered by X" wherever one does. What is left in NoRunner is then what the verdict was built for: an organisation no runner reaches at all.

func ApplyNarrowed

func ApplyNarrowed(rs []Result) []Result

ApplyNarrowed makes a narrowed pass safe by construction rather than safe if someone reads the banner.

Coverage is decided by OTHER organisations' runners, so a pass restricted to a handful of organisations cannot establish that nothing reaches one of them: `quietscan -orgs go-ruby-aasm` reads no_runner for an organisation that go-ruby-stdlib/renovate-runner covers perfectly well. A fact not established is not a fact, so the verdict becomes CoverageUnknown and says why.

func ApplyUncertainty

func ApplyUncertainty(rs []Result, unread []string) []Result

ApplyUncertainty is the rule that a failed read must never become a verdict.

If some live runner's filter could not be read, then an organisation "no filter reaches" might in fact be reached by that filter, and calling it no_runner is a verdict derived from a call that did not answer. It becomes CoverageUnknown instead, naming what could not be read.

This matters most exactly when it is least convenient: a rate-limited pass fails many reads at once, and a watcher that turns those failures into "nothing watches these 204 organisations" is worse than one that says nothing at all.

func Classify

func Classify(r Runner, now time.Time, slack, pushWarn time.Duration) Result

Classify decides one runner's verdict.

The birth date is keyed on the workflow's own created_at, NEVER on the last commit that touched the workflow file. A healthy Renovate rewrites that file whenever it bumps its own action pin, so a monitor keyed on file mtime resets its own clock every time Renovate works -- and goes blind precisely when things are fine.

type RunRecord added in v0.65.0

type RunRecord struct {
	Status     string // queued | in_progress | completed
	Conclusion string
}

RunRecord is one scheduled run, reduced to what a health check needs.

type Runner

type Runner struct {
	Org string
	// Repo is which repository holds the runner: `.github` for an
	// organisation's own, `renovate-runner` for a shared one. Empty when the
	// organisation holds no runner anywhere.
	Repo          string
	RepoExists    bool
	Archived      bool
	PushedAt      time.Time // .github repository's last push; drives the 60-day clock
	WorkflowFound bool
	WorkflowState string // active | disabled_inactivity | disabled_manually
	Born          time.Time
	BornSource    string // what Born was keyed on, so a wrong clock is visible
	Schedule      Schedule
	LastRun       time.Time // zero means: no scheduled run has ever completed
	// LastConclusion is what became of that run: GitHub's `conclusion`, one of
	// success, failure, timed_out, startup_failure, cancelled, skipped, neutral,
	// stale, action_required.
	//
	// ⚠ EMPTY MEANS NOT READ, AND MUST NOT READ AS EITHER VERDICT. A reader that
	// leaves this blank gets the old timeliness-only classification rather than
	// a silent `broken` -- an unread field is unread, the same rule that makes a
	// refused call Unreadable instead of healthy.
	LastConclusion string
	// ConsecutiveFailures is how many of the most recent scheduled runs failed
	// in a row, and RunsRead how many were on the page they were counted from.
	// SawSuccess says the streak ENDED inside that window -- that a run in
	// living memory actually worked.
	//
	// ⭐ THIS IS THE DIFFERENCE BETWEEN "IT BROKE LAST NIGHT" AND "IT HAS NEVER
	// WORKED", and the two take different fixes. What made the sixteen
	// diagnosable on 2026-10-31 was not the failure, it was the streak:
	// go-approx had 11 runs, 11 failures, zero successes, so the runner was
	// never configured rather than recently regressed. It costs nothing to know
	// -- the same single request returns the page.
	ConsecutiveFailures int
	RunsRead            int
	SawSuccess          bool
	// SecretsWanted are the secret names the workflow references, and
	// Repo/OrgSecrets the names that exist. SecretsNote says why they could not
	// be read.
	//
	// ⛔ NOTHING HERE IS A SECRET. These are NAMES, which is what the API
	// returns -- "without revealing their encrypted values". A secret is
	// verified by its properties, never by printing it, and a name is a
	// property.
	//
	// ⚠ A NON-EMPTY SecretsNote MUST SUPPRESS THE VERDICT. An unread listing is
	// unread, not empty: concluding "the secret is absent" from a refused call
	// would invent the exact finding this check exists to report.
	SecretsWanted []string
	RepoSecrets   []string
	OrgSecrets    []string
	SecretsNote   string
	// Covers is the runner's autodiscoverFilter: the organisations it watches,
	// which are NOT the organisation it lives in.
	Covers     []string
	FilterNote string // why Covers could not be read, if it could not
	ReadError  string
}

Runner is everything read about one organisation's runner. Every field is filled from a GET; nothing here can start a run.

type Schedule

type Schedule []*Cron

Schedule is every cron line on one workflow. A workflow may carry several, and the fleet's runners are staggered across the day, so the period is never a constant: it is whatever THIS runner's own lines say.

func ParseSchedule

func ParseSchedule(yaml string) (Schedule, []error)

ParseSchedule reads the `cron:` lines out of a workflow file. It is a deliberate line scan rather than a YAML parse: the workflow's shape is fixed by the fleet's own template, and a scan cannot fail to build for want of a dependency in a module that has none.

func (Schedule) Next

func (s Schedule) Next(t time.Time) (time.Time, bool)

Next is the earliest next occurrence across every line.

func (Schedule) Prev

func (s Schedule) Prev(t time.Time) (time.Time, bool)

Prev is the latest occurrence at or before t across every line.

func (Schedule) String

func (s Schedule) String() string

type Verdict

type Verdict string

Verdict is what to DO about a runner, not how bad it is. Four of these are four different fixes, and a single count would hide all four:

Archived            no pull request can fix it; the repository is frozen
DisabledInactivity  re-enable it -- and it expires again in 60 days
DisabledManually    someone turned it off; that may be entirely legitimate
Overdue/NeverFired  the runner itself has stopped
const (
	// Healthy: it ran, and the next occurrence it owes is not yet due.
	Healthy Verdict = "healthy"
	// MissingSecret: the workflow references a secret that exists in neither the
	// repository's nor the organisation's listing. The runner will start, run
	// with an EMPTY STRING where the secret should be, and fail.
	//
	// ⭐ IT NAMES THE FIX INSTEAD OF POINTING AT A LOG. Broken says "read that
	// run's logs and check the secret"; this says which secret, having checked.
	// The sixteen organisations of 2026-10-31 were exactly this, invisible for
	// ten days because nothing compared what the workflow asked for against
	// what existed.
	//
	// ⚠ IT REPLACES Broken OR NeverFired, AND NEVER Healthy. Only the
	// repository's secret listing can be read -- the organisation's needs
	// admin:org, which this fleet's token deliberately does not have -- so a
	// name absent from it proves nothing on a runner that WORKS: a successful
	// run is itself proof the secret resolves. On a failing one the same
	// absence is the cause. A healthy runner carries the fact on its line
	// instead, so nothing is hidden and nothing cries wolf.
	MissingSecret Verdict = "missing_secret"
	// Broken: the schedule fires on time and every run it produces FAILS.
	//
	// ⛔⛔ THIS TOOL REPORTED SIXTEEN OF THESE AS `healthy`. On 2026-10-31,
	// sixteen organisations had their Renovate runner failing in about ten
	// seconds every night -- `RENOVATE_TOKEN` was never set, and an unset secret
	// expands to the empty string -- so NONE of them had ever received a
	// dependency update. go-approx's entire history was 11 runs, 11 failures,
	// zero successes. This file asked WHEN the last run started and never what
	// became of it, so all sixteen were timely, and timely read as fine.
	//
	// ⭐ THE LESSON IS THE MEASUREMENT'S SUBJECT, NOT THE MISSING FIELD.
	// `redscan` caught them only incidentally, because the runner's workflow
	// happens to live on a default branch. The specialised sweep -- the one whose
	// whole job is Renovate runners -- was the one that said nothing. Activity is
	// not health, and a runner that fails fast is the most active-looking
	// failure there is.
	Broken Verdict = "broken"
	// NotYetDue: it has never run, but its first scheduled occurrence has not
	// arrived. This is NOT the same as never having fired, and conflating the
	// two is what produced a false alarm about 26 dead runners that were merely
	// queued behind a schedule they had not reached.
	NotYetDue Verdict = "not_yet_due"
	// NeverFired: it has never run and its first occurrence is long past.
	NeverFired Verdict = "never_fired"
	// Overdue: it ran once, and the occurrence after that is long past.
	Overdue Verdict = "overdue"
	// DisabledInactivity: GitHub switched the schedule off after 60 days with
	// no push. Re-enabling starts the same 60-day clock again.
	DisabledInactivity Verdict = "disabled_inactivity"
	// DisabledManually: a person switched it off. One runner in this fleet is
	// deliberately retired, so this is reported and never paged on.
	DisabledManually Verdict = "disabled_manually"
	// Archived: the repository is archived. Nothing can be pushed to it.
	Archived Verdict = "archived"
	// NoSchedule: the workflow exists but carries no cron -- dispatch-only, so
	// nothing will ever start it on its own.
	NoSchedule Verdict = "no_schedule"
	// Covered: this organisation has no runner of its own and does not need
	// one -- a shared runner's autodiscoverFilter reaches it, or the Renovate
	// App is installed on it. Reporting these as missing runners is how a
	// watcher earns 199 lines nobody reads.
	Covered Verdict = "covered"
	// CoverageUnknown: no runner and no filter reaches it, and whether the
	// Renovate App does could not be read. An unread installation is unread,
	// not absent -- the same rule as an autodiscoverFilter that will not parse.
	CoverageUnknown Verdict = "coverage_unknown"
	// NoRunner: no runner reaches this organisation at all -- neither one of
	// its own nor any shared runner's filter. A recency check over runners
	// structurally cannot see this: there is no runner to be quiet.
	//
	// Whether the organisation holds an empty `.github` repository or none at
	// all is a detail of the same gap, so it is said in the line and not split
	// across two verdicts. One number that means one thing is the point.
	NoRunner Verdict = "no_runner"
	// Unreadable: the API would not say. Reported, never silently counted as
	// healthy -- a channel that cannot answer must not read as "fine".
	Unreadable Verdict = "unreadable"
)

func (Verdict) Live

func (v Verdict) Live() bool

Live reports whether this runner's schedule still exists and is switched on. An archived, disabled or cron-less runner covers nothing -- which is the whole point about `go-attest/renovate-runner`, retired and disabled_manually, and therefore no longer an answer to "what watches these organisations?".

func (Verdict) Pages

func (v Verdict) Pages() bool

Pages reports whether this verdict is worth waking someone for. Archived and a manual disable are real findings with real fixes, but neither is a runner that stopped without anyone deciding it should.

Jump to

Keyboard shortcuts

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