Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| A running job's log is live, as on GitHub. A step's output reaches its run's page within about two seconds: the runner sends what is printed every second, and the page asks every second while a step runs, every three while none does; an answer that fails (a 429, a 5xx, the network) is asked again after a wait that doubles to half a minute, or as long as Retry-After says, where before one such answer stopped the log for good and the step said "No output yet." until the page was reloaded, and a signed-out viewer's run page, refreshing beside its log, ran into the 30-a-minute limit within a minute; the live log now counts as a page, and only log downloads as costly. A hidden tab stops asking and catches up when shown, and the page says when it is reconnecting. The running step opens by itself, its timer counts, its log follows the newest line while its end is on screen and offers Jump to latest once the viewer scrolls up, a step that has printed nothing for a while says "Running · no output for 2m 10s", and the job's header names the step it is on. Colour that tools print (bold, dim, italic, underline, the 16 colours as theme tokens for light and dark, 256 colours and 24-bit colour, text and background) is drawn as printed, other escape codes are dropped and links keep their text, and search reads the plain text and keeps the colours around what it marks. Every line is stamped with the time it was printed, as GitHub's raw logs are, so log.txt and the run's zip have a time at the start of each line, and a clock button beside Search logs shows the times, remembered by the browser. Set up job now gives the runner's version and name, the operating system, the runner image, the machine's architecture, CPUs and memory, the token's permissions under G1T_TOKEN Permissions, the secret source and the actions it downloads; each run step opens with GitHub's folded Run group, the script in cyan, the shell's whole path and the step's env, each uses step with its with inputs (an action's defaults among them) and env; cache and artifact transfers say how far they have got every five seconds; and a last Complete job step sets the job's outputs and stops what the steps left running, found by RUNNER_TRACKING_ID as GitHub's runner finds it. Two batches of a job's log written at once can no longer reach for the same seq and lose one. The Actions guide and the rate limits page say so. | 1 | /** |
| 2 | * When a job's page asks for more of its log (components/actions.tsx, | |
| 3 | * `useJobLog`): about every second while a step is running, every few | |
| 4 | * seconds while the job waits between steps or for a machine, and, when | |
| 5 | * an answer fails (a 429, a 5xx, the network), again after a wait that | |
| 6 | * doubles up to half a minute, or as long as `Retry-After` says. Nothing | |
| 7 | * stops it for good but the job finishing or being gone. | |
| 8 | */ | |
| 9 | ||
| 10 | /** While a step is running. */ | |
| 11 | export const RUNNING_POLL_MS = 1000; | |
| 12 | /** While the job is live but no step is running: queued, starting, between steps. */ | |
| 13 | export const IDLE_POLL_MS = 3000; | |
| 14 | /** The first wait after a failure, doubled after each one that follows. */ | |
| 15 | export const FIRST_RETRY_MS = 2000; | |
| 16 | /** The longest wait between tries after failures. */ | |
| 17 | export const MAX_RETRY_MS = 30_000; | |
| 18 | /** A finished job's last fetch is tried this many times before it gives up. */ | |
| 19 | export const FINISHED_TRIES = 5; | |
| 20 | ||
| 21 | export type PollAnswer = | |
| 22 | /** An answer: whether the job is done, and whether the page was full (more is waiting). */ | |
| 23 | | { ok: true; done: boolean; full: boolean } | |
| 24 | /** No answer: its status, if there was one, and its `Retry-After` in milliseconds. */ | |
| 25 | | { ok: false; status?: number; retryAfterMs?: number }; | |
| 26 | ||
| 27 | export type PollState = { | |
| 28 | /** Failures in a row so far. */ | |
| 29 | failures: number; | |
| 30 | /** Whether the page shows the job as still going. */ | |
| 31 | live: boolean; | |
| 32 | /** Whether one of its steps is running now. */ | |
| 33 | running: boolean; | |
| 34 | }; | |
| 35 | ||
| 36 | /** How long to wait before the next fetch, or null to stop; and the failures in a row after this answer. */ | |
| 37 | export function nextPoll(answer: PollAnswer, state: PollState): { delayMs: number | null; failures: number } { | |
| 38 | if (answer.ok) { | |
| 39 | if (answer.full) return { delayMs: 0, failures: 0 }; | |
| 40 | if (answer.done || !state.live) return { delayMs: null, failures: 0 }; | |
| 41 | return { delayMs: state.running ? RUNNING_POLL_MS : IDLE_POLL_MS, failures: 0 }; | |
| 42 | } | |
| 43 | const failures = state.failures + 1; | |
| 44 | // The job is not there (deleted, or the repository is gone): nothing to wait for. | |
| 45 | if (answer.status === 404) return { delayMs: null, failures }; | |
| 46 | if (!state.live && failures >= FINISHED_TRIES) return { delayMs: null, failures }; | |
| 47 | const backoff = Math.min(MAX_RETRY_MS, FIRST_RETRY_MS * 2 ** (failures - 1)); | |
| 48 | const asked = answer.retryAfterMs !== undefined ? Math.min(answer.retryAfterMs, 2 * MAX_RETRY_MS) : 0; | |
| 49 | return { delayMs: Math.max(backoff, asked), failures }; | |
| 50 | } | |
| 51 | ||
| 52 | /** How long a running step may print nothing before its log says how long it has been quiet. */ | |
| 53 | export const QUIET_AFTER_MS = 10_000; | |
| 54 | ||
| 55 | /** `45s`, `2m 10s`, `1h 5m`, as the run's page counts time. */ | |
| 56 | export function elapsed(ms: number): string { | |
| 57 | const seconds = Math.max(0, Math.round(ms / 1000)); | |
| 58 | if (seconds < 60) return `${seconds}s`; | |
| 59 | const minutes = Math.floor(seconds / 60); | |
| 60 | if (minutes < 60) return `${minutes}m ${seconds % 60}s`; | |
| 61 | return `${Math.floor(minutes / 60)}h ${minutes % 60}m`; | |
| 62 | } | |
| 63 | ||
| 64 | /** | |
| 65 | * What a running step's log says under its last line: that nothing has | |
| 66 | * come yet, and for how long, or, once it has gone quiet for a while | |
| 67 | * (a build that prints nothing as it links, a download), for how long it | |
| 68 | * has printed nothing. Null while output is flowing. Times in ms. | |
| 69 | */ | |
| 70 | export function quietWords(started: number | null, lastOutput: number | null, now: number): string | null { | |
| 71 | if (lastOutput === null) return started === null ? "Running · no output yet" : `Running · no output yet, ${elapsed(now - started)}`; | |
| 72 | if (now - lastOutput < QUIET_AFTER_MS) return null; | |
| 73 | return `Running · no output for ${elapsed(now - lastOutput)}`; | |
| 74 | } | |
| 75 | ||
| 76 | /** `Retry-After` in milliseconds: seconds, or an HTTP date. */ | |
| 77 | export function retryAfterMs(header: string | null, now = Date.now()): number | undefined { | |
| 78 | if (!header) return undefined; | |
| 79 | const seconds = Number(header.trim()); | |
| 80 | if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1000; | |
| 81 | const at = Date.parse(header); | |
| 82 | return Number.isFinite(at) ? Math.max(0, at - now) : undefined; | |
| 83 | } |