Documentation
¶
Overview ¶
Package runnerrelease says which actions/runner billet installs, and how close that is to being refused by GitHub.
THIS IS A DEADLINE, NOT A PREFERENCE, and that is the whole reason the package exists. GitHub requires a self-hosted runner to be updated within 30 days of a release, and past that the Actions service simply stops handing it jobs — the server refuses the message rather than asking the runner to update, so nothing on the runner's side can rescue it. A fleet that was working perfectly stops taking work, and the visible symptom is jobs queueing against runners that look healthy.
AND BILLET'S RUNNERS CANNOT UPDATE THEMSELVES OUT OF IT, which is what turns this from a slow-job problem into an outage. A self-hosted runner ordinarily updates itself in place — so a stale image would cost a large download per job and keep working. billet's do not: a JIT configuration minted by GitHub's REST API carries `DisableUpdate = True` alongside `Ephemeral = True`, measured against the live API, and there is no parameter to ask for anything else. So the version baked into the image is the version forever, and republishing is the only way past the deadline.
That is the same posture actions-runner-controller runs in, deliberately, and the same one it gets its `Outdated` scale-set failure from. The difference this package makes is being told before it happens rather than after.
billet bakes the runner into an image, so "update the runner" means "rebuild and republish the image". That is a thing somebody has to do on a schedule, and a thing somebody will forget — so it needs to be something a machine notices.
THERE IS NO API FOR THE MINIMUM VERSION. GitHub publishes no endpoint saying what it will refuse; its own documented advice is to subscribe to release notifications. So the only mechanical signal available is the release feed, and the rule to apply to it is the 30-day one from their documentation.
Index ¶
Constants ¶
const Grace = 30 * 24 * time.Hour
Grace is how long GitHub gives a runner to take up a new release.
From their own documentation: "If you do not perform a software update within 30 days, the GitHub Actions service will not queue jobs to your runner."
COUNTED FROM THE FIRST RELEASE NEWER THAN THE INSTALLED ONE, which is what Freshness computes and is the only timestamp that means anything here. Every major, minor and patch release is an available update, so the clock starts at the first of them and does not restart when the next one lands.
AND IT IS THE ORDINARY WINDOW RATHER THAN A GUARANTEE. GitHub says a critical security release may be required immediately, and it publishes no endpoint saying what it will refuse — its own advice is to subscribe to release notifications. So this is the best mechanical estimate of acceptance there is, and every diagnostic built on it says "ordinary" rather than implying billet knows the answer.
const ReleasesURL = "https://api.github.com/repos/actions/runner/releases"
ReleasesURL is the release history billet reads.
THE HISTORY RATHER THAN /releases/latest, AND THAT IS THE WHOLE POINT OF THIS FILE. GitHub's window opens when the FIRST release newer than the installed one appears, so the newest release's date is the wrong number: a runner that missed two releases has been on the clock since the first of them, and counting from the second moves a deadline that has already passed. Measured against real dates: 2.334.0 went out of date when 2.335.0 was published on 2026-06-08, so its window closed on 2026-07-08 — and the old calculation reported 2026-08-19, because 2.336.0 landed on 2026-07-20. A fleet GitHub had stopped queueing to for six weeks read as having a month left.
UNAUTHENTICATED, because this asks a question about an open-source project rather than about the operator's own account, and needing a token to find out whether your fleet is about to stop working would put this check behind exactly the credential most likely to be missing on the machine running it.
const Warn = 20 * 24 * time.Hour
Warn is when billet starts saying so.
A third of the window, because the action this warns about is not "click update" — it is building an image, verifying it boots and registers, and rolling a fleet onto it. Ten days is enough for that to be scheduled rather than urgent, and it leaves the last ten for the case where the first attempt failed.
Variables ¶
This section is empty.
Functions ¶
func Older ¶
Older reports whether a is an earlier release than b.
COMPARED NUMERICALLY, PART BY PART, because these are versions rather than strings: "2.9.0" sorts after "2.10.0" lexically and is older in fact, and picking the wrong one means watching the tier that is fine while the stale one expires.
HERE RATHER THAN IN THE COMMAND THAT FIRST NEEDED IT. `billet runner check` used this to find the oldest tier and the history calculation needs the same order; two comparators is one comparator that is wrong.
SOMETHING THAT IS NOT A VERSION STILL GETS AN ORDER. This runs over metadata recorded on a generation, which is whatever was written there, on a scheduled path where crashing is a worse answer than a stable guess. Callers that need a real version ask isStableVersion first.
func PinnedSHA256 ¶
func PinnedSHA256() string
PinnedSHA256 is the checksum of that release's linux-x64 tarball.
IT LIVES BESIDE THE VERSION BECAUSE IT IS ONLY TRUE OF THAT VERSION. Held apart — the version in one file and the checksum in a build script — a bump updates one and the build fails its own integrity check, or worse updates the checksum alone and verifies a download against a number for a different release. Together, a bump is one line and cannot be half done.
READ FROM LINE 1 ONLY. It used to Cut the whole embedded file at its first space, which was correct while the file had one line and would silently have returned the entire platform table the moment it had four — a "checksum" containing newlines, handed to a build that then verifies nothing successfully.
func PinnedSHA256For ¶
Types ¶
type Freshness ¶
type Freshness struct {
// Installed is the version the fleet actually runs, as this package read it.
//
// REPORTED BY THE CALLER RATHER THAN ITS OWN COPY, which is the one-representation
// rule this repository has been bitten by three times in internal/config: Resolve
// normalizes what it was given (a leading "v", surrounding space) and answers about
// the result, so a command printing the string it passed IN would attribute this
// answer to a version it was not computed for.
Installed string
// Latest is the newest stable release, which is what a rebuild takes up. It is
// NOT what the deadline is counted from.
//
// ITS PUBLICATION DATE IS DELIBERATELY NOT CARRIED. It decides nothing — the
// deadline comes from FirstNewerPublished — so a field holding it would be one
// nothing reads and one more value a caller could reach for by mistake, which
// is exactly the substitution this package exists to remove.
Latest string
// FirstNewer is the earliest stable release newer than Installed — the one that
// started GitHub's clock — and FirstNewerPublished is when it appeared. Both are
// zero when nothing newer exists.
//
// EARLIEST BY PUBLICATION, NOT BY VERSION. A patch backported after a later
// minor release is newer than Installed and did not start the clock; the
// release that did is whichever appeared first.
FirstNewer string
FirstNewerPublished time.Time
// HistoryComplete says the walk finished because it had read everything it
// needed, rather than because it ran out of budget.
//
// THREE WAYS IT IS TRUE and none of them is "read every release GitHub has": the
// installed release was found (everything below it was published earlier and was
// already available when it shipped), a page came back shorter than requested, or
// an empty one did.
//
// WHAT IT PROMISES, EXACTLY: every release published after the installed one was
// read, so FirstNewer and the deadline are settled. `Latest` is settled over that
// plus the remainder of the page the installed release was on — which is where a
// higher version published earlier realistically sits, and is free because those
// records are already fetched. A higher version published earlier AND more than
// one page further back would be missed, and the fleet would read as current
// while sitting on an old maintenance branch; closing that costs a page per check
// forever to catch a case nobody has produced.
//
// FALSE MEANS OLDER RELEASES WERE NEVER READ, and one of them could be newer than
// Installed and published EARLIER than FirstNewer — the same thing that makes
// stopping at the installed tag unsound (see maxPages). The true window would
// then have opened before the one billet found, so the computed deadline is LATER
// than the truth.
//
// WHICH DIRECTION THAT SPOILS IS THE POINT. An unseen earlier opener only moves
// the deadline EARLIER, so Expired and Due can still only under-report: a proved
// expiry stays a proof. What it costs is the other direction — "not expired" and
// "current" are no longer conclusive — which is why a caller that would report
// nothing-to-do has to look at this.
//
// In practice one page reaches back FIVE YEARS (measured), so this is false only
// for a fleet whose runner predates everything two pages cover; such a runner is
// expired many times over and the sound direction already says so. It is carried
// because the claim has to match what was read, not because the gap is likely.
HistoryComplete bool
// InstalledKnown says the history billet read actually named Installed.
//
// THE THIRD ANSWER, AND IT DECIDES WHICH SENTENCES ARE PROOFS. False means the
// installed release is older than everything fetched, so the true clock started
// at or BEFORE FirstNewerPublished: Expired and Due stay sound (they can only
// under-report), while Remaining is an over-estimate and must be spoken of as
// "at most". Collapsing this into "current" is the false negative this package
// exists to remove; collapsing it into "expired" would refuse a fleet that is
// fine.
InstalledKnown bool
}
Freshness is what the release history says about one installed runner.
ONE MODEL, AND EVERY CALLER ASKS IT. `billet runner check` and `billet images pull` used to compute a deadline each, from two different timestamps, and neither was the one GitHub counts from. A second calculation is a second answer.
func Resolve ¶
Resolve asks GitHub where an installed runner sits in the release history.
A FAILURE HERE IS NOT A VERDICT. Every caller has to be able to tell "this fleet is out of date" apart from "billet could not find out", because the second is an ordinary thing that happens to a machine with no egress and must not be reported as a fleet about to stop working.
func (Freshness) BehindWithoutAWindow ¶
BehindWithoutAWindow reports the state where something newer exists and no ordinary window was derived for it.
IT IS REACHABLE AND IT IS NOT A DEADLINE. A higher version published EARLIER than the installed release sits below the floor: it is read as evidence that the fleet is behind (Latest), and it is deliberately not read as an opener, because it was already available when the installed release shipped. So `Current()` is false while `FirstNewer` is empty, and every timed answer — Deadline, Remaining, Due, Expired — is meaningless.
NAMED, BECAUSE THE CALLERS BOTH FELL THROUGH TO THEIR TIMED BRANCH and printed an empty release name, the year 0001 and a negative number of days, then exited 0. A state that reads as success while rendering nonsense is worse than either answer.
func (Freshness) Current ¶
Current reports that the history billet fetched holds no release newer than the installed one.
ASKED OF THE NEWEST RELEASE RATHER THAN OF THE WINDOW-OPENER. They are different questions — "is this fleet behind" is about versions, and a release can be newer while having been published earlier — so Deadline, Due and Expired key on FirstNewer, which is what they are actually about, and this keys on Latest.
AND IT IS A BOUNDED CLAIM, WHICH NO DESIGN CAN AVOID: proving that nothing newer EXISTS means reading every release GitHub has ever published, and the walk reads a bounded window. So the answer is about what was read, and how much that is worth depends on HistoryComplete — which is why a caller consults that first.
With the floor reached it covers everything published after the installed release, which is where an ordinary newer release is. It is best-effort for a higher version published BEFORE it, since such a release sits further down a list of unbounded length: the rest of the floor's page is read for exactly that case, and one page past it when the floor ends a page. Without the floor — the budget ran out — it covers only the window that was fetched, and HistoryComplete is false.
func (Freshness) Deadline ¶
Deadline is when GitHub stops queueing jobs to the installed release, under the ordinary window. It is zero when nothing newer was found.
COUNTED FROM THE EARLIEST NEWER RELEASE, which is what FirstNewerPublished holds and is the whole point of the model. A caller must ask Current() first: a zero here means the question does not apply, not that the deadline is the epoch.
func (Freshness) Due ¶
Due reports whether the image should be rebuilt now to stay inside the window.