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
- func Failed(conclusion string) bool
- func MatchesOrg(pattern, org string) bool
- func MissingSecrets(referenced, repoSecrets, orgSecrets []string) []string
- func ParseAutodiscoverFilter(js string) ([]string, string)
- func SecretsReferenced(workflow string) []string
- func SecretsSummary(missing []string) string
- func Streak(runs []RunRecord) (failures int, sawSuccess bool)
- func UnreadFilters(rs []Result) []string
- type AppCoverage
- type Cron
- type LagStats
- type Result
- type RunRecord
- type Runner
- type Schedule
- type Verdict
Constants ¶
const AppCoveredBy = "the Renovate App"
AppCoveredBy is what the covered line says when an App is the reason.
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.
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.
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.
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
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 ¶
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
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 ¶
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
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
SecretsSummary renders the finding for a report line.
func Streak ¶ added in v0.65.0
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 ¶
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 ¶
ParseCron reads one cron line: five space-separated fields, each a list of "*", "n", "a-b", or any of those with a "/step" suffix.
type LagStats ¶
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.
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 ¶
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 ¶
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 ¶
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 ¶
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
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 ¶
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.
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 ¶
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?".