Skip to content

Latest commit

 

History

History
175 lines (120 loc) · 14.1 KB

File metadata and controls

175 lines (120 loc) · 14.1 KB

GitHub Actions

Watches the workflows of every repository the token can see, or of one organization or user when the connection names one. Repositories with no push in ninety days, archived ones and disabled ones are skipped.

Credential

A fine grained personal access token with Actions read and write and Metadata read on the repositories to watch, or a classic token with the repo scope. Or sign in: the device flow, or the browser flow once an OAuth App is registered. See Authentication.

A fine grained token also needs Pull requests read and Contents read to say what became of a failed branch, under Rows; without them a failed branch goes only once the default branch has built since.

For GitHub Enterprise Server enter the server URL; the API is reached under /api/v3.

Rows

One per workflow, showing its latest run on the repository's default branch, which the repository listing names. A run on another branch that is running, queued or failed gets a row of its own. Pull request runs link to the pull request. A run from a fork is shown on the fork's branch behind its owner, someone:main, so a fork's main is never taken for the repository's, and its branch opens in the fork. GitHub leaves the pull request off a run from a fork, so that row has no pull request button.

A failed branch whose pull request has been merged or closed, or which has been deleted, loses its row (see the window). The connection asks GitHub, for every failed branch of a repository on its host, whichever service built it: an AppVeyor, Travis CI or Azure Pipelines build of a GitHub repository is asked about here too. A pull request is asked for its state; a fork's branch, whose run names no pull request, for the newest pull request from it; any other branch whether it, or a tag of its name, is still there, since a release workflow's run is named for its tag. A pull request found open or a branch found there is asked about again after ten minutes, and one the token may not see after an hour. Merged, closed or deleted is not asked again, unless the branch builds again.

Actions

  • Retry re-runs only the failed jobs of a failed run, and the whole run otherwise
  • Cancel cancels a queued or running run
  • Log copies the logs of the jobs that failed or timed out in the latest attempt

A classic token or a sign in lists its scopes, and without repo its rows offer neither Retry nor Cancel; the connection shows as watch only in Connections. With repo, a repository the user can only read offers neither. A fine grained token lists nothing, so its rows offer both, and a refusal says what the token needs.

Estimates

GitHub gives none. The countdown comes from the median of the workflow's last ten successful runs.

Polling

Runs are fetched a repository at a time, each on its own schedule (see Poll intervals), with a conditional request: GitHub answers an unchanged repository with a 304 that does not count against the five thousand requests an hour. GitHub also limits requests a minute and counts those 304s, so no more than 450 requests a minute are sent, the most urgent repositories first. The hourly limit is shared with every other tool signed in as the same account.

Each poll interval, page 1 of the repository list is read again, most recently pushed first, with the same conditional request discovery uses, so it costs nothing while nothing is pushed. A repository whose last push moved is fetched at once rather than when its schedule comes round. Runs that start without a push, such as scheduled runs, manual dispatches, re-runs started on the web and pull requests from forks, wait for the schedule: up to five minutes on a quiet repository.

A repository's page of runs holds five a workflow, and a batch of pull requests can fill a workflow's five. Its latest run on the default branch is kept past the five where the page holds it, and where it does not, that workflow's runs on the default branch are asked for, with the same conditional request. A workflow with none since the history cutoff, such as one only pull requests trigger, is not asked again for an hour.

Discovery lists a repository's workflows again only once it has been pushed to since they were last listed, and every repository's once an hour, so a workflow enabled or disabled without a push can take up to an hour to show or go.

After each cycle the connection asks about the failed branches due an answer, one question a branch however many workflows failed on it, up to twenty a cycle and eight at a time. They are conditional requests too, count towards the same 450 a minute, and wait while that is spent. A question that fails is asked again after a minute, doubling, and leaves the connection's health alone.

---
config:
  flowchart:
    wrappingWidth: 400
---
flowchart TD
    wake(["Wake: something is due,<br/>or Refresh, Retry or Cancel"]) --> listed{"Listed repositories in<br/>the last 10 minutes,<br/>with this token?"}
    listed -- "no" --> discover["GET user for the token's<br/>scopes, then user/repos?sort=pushed<br/>or the owner's, up to 5 pages,<br/>then the workflows of each<br/>repository pushed since they<br/>were listed, or of every one<br/>pushed in 90 days each hour"]
    listed -- "yes" --> probed{"Probed in the<br/>last 30 seconds?"}
    probed -- "no" --> probe["GET page 1 of the same list,<br/>a free 304 while<br/>nothing was pushed"]
    probe --> moved{"A repository's<br/>pushed_at moved?"}
    moved -- "yes" --> nudge["Fetch that repository<br/>now, then every 30 s<br/>for 3 minutes"]
    discover --> interval["Each repository is polled<br/>as its busiest workflow needs"]
    probed -- "yes" --> interval
    moved -- "no" --> interval
    nudge --> interval
    interval --> finishing["Running, from its fastest<br/>recent run to 90 s past<br/>its slowest, or with no<br/>history: every 10 s"]
    interval --> running["Running for less than its<br/>fastest recent run, or<br/>queued: every 30 s, and<br/>again when it reaches that"]
    interval --> overrun["Running over 90 s past<br/>its slowest recent run:<br/>the time beyond that ÷ 10,<br/>30 s to 5 minutes"]
    interval --> quiet["Quiet: the time since<br/>the last run ÷ 30,<br/>30 s to 5 minutes"]
    interval --> failed["Quiet after a failure:<br/>the time since the<br/>last run ÷ 120,<br/>30 s to 5 minutes"]
    finishing --> stretch["Up to 8 times longer while<br/>under a quarter of the<br/>hourly limit is left"]
    running --> stretch
    overrun --> stretch
    quiet --> stretch
    failed --> stretch
    stretch --> due{"Due, and within<br/>450 requests a minute?"}
    due -- "no" --> sleep(["Sleep until a repository,<br/>the probe or the<br/>listing is due"])
    due -- "yes, most urgent first" --> fetch["GET repos/{owner}/{repo}/actions/runs<br/>with If-None-Match,<br/>8 at a time"]
    fetch -- "200 or 304" --> held{"A run on the default<br/>branch for each workflow,<br/>or none in the last hour?"}
    held -- "yes" --> rows["Update its rows"]
    held -- "no" --> workflow["GET repos/{owner}/{repo}/actions/<br/>workflows/{id}/runs?branch={default}<br/>for each workflow without one"]
    workflow --> rows
    fetch -- "429, or 403 from<br/>a secondary limit" --> pause["Pause the connection<br/>as long as GitHub asks,<br/>or from a minute, doubling"]
    fetch -- "other failure" --> backoff["Back off that repository,<br/>doubling up to 10 minutes"]
    rows --> branches{"A failed branch of a<br/>repository on this host,<br/>from any service, not<br/>answered for good?"}
    branches -- "yes, up to 20" --> ask["GET its pull request, the newest<br/>pull request from a fork's branch,<br/>or the branch, then tags of its name"]
    branches -- "no" --> sleep
    ask --> sleep
    pause --> sleep
    backoff --> sleep
Loading

API notes

Researched 2026-09-14. [live] means checked with requests against api.github.com; [docs] names the evidence. See Provider APIs for every provider side by side.

Rate limits

  • REST allows 5,000 requests an hour per user, shared by every OAuth app and personal access token of that user; 15,000 for apps owned by an Enterprise Cloud organization; 60 unauthenticated [docs].
  • Headers: x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-used, x-ratelimit-reset (Unix seconds) and x-ratelimit-resource [docs, live].
  • Secondary limits: 100 concurrent requests, 900 points a minute for REST where a GET costs 1, 90 seconds of CPU per 60 seconds, and 80 content creating requests a minute [docs].
  • Either limit answers 403 or 429. Honour retry-after; if remaining is 0, wait until the reset; otherwise wait at least a minute and back off exponentially. Continuing while limited risks the integration being banned [docs].
  • GraphQL has its own 5,000 points an hour, and a secondary limit of 2,000 points a minute [docs].

Conditional requests

  • Lists carry weak ETags. A request with If-None-Match answered 304 and left x-ratelimit-remaining unchanged, on both actions/runs and orgs/{org}/repos [docs, live]. It is only free when the request carries the Authorization header.
  • The docs do not exempt a 304 from the secondary limits.
  • ETags are per page: a 304 for page 1 says nothing about page 2.
  • Lists send Cache-Control: private, max-age=60, and event feeds send X-Poll-Interval: 60 [live].

Change detection

  • user/repos, orgs/{org}/repos and users/{user}/repos accept sort=pushed and return pushed_at, so page 1 holds the most recently pushed repositories and answers 304 while nothing changed [docs, live].
  • users/{user}/repos lists public repositories only.
  • pushed_at does not move for scheduled runs, manually dispatched runs, re-runs, or pull requests from forks.

Batching

  • GraphQL can batch: one query returned the head commit and check suites of 75 repositories for 1 point, and nodes(ids:) over workflow node_ids returns runs(first: N) for up to 100 workflows for 1 point [live].
  • WorkflowRun has no status, conclusion, branch or start time of its own. They come from its checkSuite (status, conclusion, branch, matchingPullRequests) and that suite's checkRuns (startedAt, completedAt) [live].
  • GraphQL has no conditional requests.
  • actions/runs filters on actor, branch, event, status, created, exclude_pull_requests, check_suite_id and head_sha, and returns at most 1,000 results when filtered [docs].

Permissions

Checked 2026-09-16.

  • X-OAuth-Scopes lists the scopes of an OAuth App's token or a classic token, comma separated: gist, read:org, repo, workflow on GET user [docs, live]. The documentation describes it for no other token, so its absence says nothing.
  • Re-running a run, re-running its failed jobs and cancelling it need the repo scope on those tokens, and Actions write on a fine grained one [docs]. With no repository scope a token reads public information only [docs]. Whether public_repo alone is enough is not documented.
  • The repository lists carry permissions, with push for write access [docs], which re-running and cancelling need. Whether a fine grained token's are its user's or its own is not documented.
  • X-Accepted-GitHub-Permissions names the permissions an endpoint requires, not those a token holds [docs].
  • GET repos/{owner}/{repo}/pulls/{number} and the pull request list need Pull requests read on a fine grained token; branches/{branch} and git/matching-refs/{ref} need Contents read [docs].

Branches and pull requests

Checked 2026-09-23.

  • A pull request carries state, open or closed, and merged_at, set once merged [docs, live].
  • The pull request list filters on head=owner:branch, a fork's too, and state=all keeps the closed ones; the newest comes first [docs, live]. A fork's run names no pull request, so this is how one is found.
  • branches/{branch} takes a branch with slashes in it unescaped, and answers 404 for a branch that is not there and for a repository the token cannot see alike [live].
  • git/matching-refs/tags/{name} lists every tag starting with the name, so v1 lists v1.1; it answers [] for a repository the token can see with no such tag, and 404 for one it cannot [live].

Alternatives considered

  • The Events API. users/{user}/events/orgs/{org} includes private events, but events arrive 30 seconds to 6 hours late, only 300 events or 30 days are kept, there are no workflow run, check or status events, and a PushEvent carries only repository_id, push_id, ref, head and before [docs].
  • Search commits. It has its own limit of 30 requests a minute, covers the default branch only, has undocumented index lag, and commits are not runs [docs].

Sources