Zum Inhalt

The downloads of the workflows go through the API

  • Status: accepted, followed up by 0134, with which a job that blocks stops when the agent of Harden-Runner doesn't run; since 9 November 2026, the weekly SBOM takes syft through the API as well, and The actions of others names what each action of others downloads, with which token, and what would help (#762); since 10 November 2026, the job release assets signs a Nextcloud app on the PHP 8.3 of the runner as it is, with php-version: pre-installed, for which setup-php downloads nothing, which closes the gap that The actions of others names for it (#800); and by 0139, with which the secret scan runs gitleaks of its release through the API in place of its action, since 10 November 2026 (#806)
  • Date: 2026-11-08

Context

On 8 November 2026, GitHub declared the incident Increase in timeouts for some unauthenticated users at 10:36 UTC; the checks failed from about 08:47 UTC on. For hours, a download from the page of a release on github.com, without a token, timed out or answered 504, while the API, with a token, kept working. The checks of every repository made from the blueprint failed on pull requests and on main, and GitHub mailed every failed run to the maintainer:

  • lycheeverse/lychee-action downloads lychee with an unauthenticated curl -sfLO from the page of its release, which failed with exit code 22: the step Check the links and anchors of the website of the check docs, and the Links workflow. The action has no way to skip its download.
  • The init of github/codeql-action failed with Unable to download and extract CodeQL CLI: Unexpected HTTP response: 504. The runner had CodeQL 2.27.3 in its tool cache, the version that the pinned action brings (src/defaults.json), but GitHub's feature flags asked for 2.27.4, so the action deleted 2.27.3 and downloaded 2.27.4 from github.com, three times without success.
  • The other tools that the workflows download, gitsign, krankerl, Infection and syft, came from the pages of their releases with curl as well, which tried again, but without a token.
  • The flaky-test bot of After the checks failed on a call that GitHub refused because the run was already running again, and a 5xx of the API would have failed it the same way.

Options

  1. Retries alone: longer retries of curl. They don't help in an outage that lasts hours.
  2. Through the API with the token of the job: gh release download asks the API for the asset, which redirects to release-assets.githubusercontent.com, as the authenticated downloads that kept working did.
  3. A cache of the tools: the Actions cache serves the tools from Azure. Its first download still needs GitHub, and the cache of a repository holds a few hundred MB more.

Decision

Option 2, for every download of an asset of a release on GitHub that the blueprint's own steps and scripts make. The actions of others download their tools themselves: The actions of others below names what each takes and what would help.

  • .github/download.sh downloads an asset of a release through the API with the token in GH_TOKEN, tries again after an error of the network or of GitHub, or after a call that stalls for five minutes, since gh waits without a limit, five times in all, 10, 20, 40 and 80 seconds apart, and stops at once when the release or the asset doesn't exist or the token is refused. It checks the download by the sha256 that the caller pins, or without one by the digest that GitHub keeps of the asset, and refuses an asset without one. When GitHub keeps failing, its annotation says that it is an outage of GitHub and not a problem of the change, with the link to githubstatus.com.
  • lychee comes from .github/lychee.sh, which pins its version and sha256 below a line # renovate:, like the other tools of .github/pins.toml, downloads it once in a job and runs it as the action did: the report in Markdown in the log, the summary and the output file, the exit code in the output exit_code, a failure on a broken link or on no link at all, and the token of the job for the links to GitHub. The check docs and the Links workflow keep every argument and the cache of the answers of the sites. The action leaves the allowed actions.
  • CodeQL analyzes with tools: linked, the version that the pinned action brings, which Renovate moves with the action and which the runners usually have in their tool cache. A step before the init looks for it there; when the runner lacks it, .github/download.sh downloads the bundle of that version through the API, checked by GitHub's digest, and the action takes it from that file. The analysis no longer follows the feature flags of GitHub ahead of a release of the action, which also keeps the alerts from moving with a version that no pull request brought.
  • gitsign, krankerl, Infection and syft come through the API as well: krankerl and Infection with .github/download.sh, syft and krankerl of a release with .github/download.sh of the commit that runs the workflow, before the checkout and in a step of its own, so that no step that builds the project has the token of the job, and gitsign of .github/sign-tag.sh and of the verification of a release with gh release download of their own, as they don't depend on the checkout. The weekly SBOM of the repository takes the syft of a release too, with .github/download.sh after the checkout, in place of anchore/sbom-action, whose installer downloaded syft without a token.
  • The flaky-test bot asks GitHub again after a 5xx or a timeout, four times in all, 10, 20 and 40 seconds apart, fails at once on a refusal, and leaves a run alone that runs again already.

Consequences

An outage of GitHub's downloads without a token fails no check; an outage of the API still does, and the annotation of .github/download.sh says so. Each download costs two calls of the API, which count against the limit of the token of the job.

The jobs that block hosts with Harden-Runner already allow api.github.com and release-assets.githubusercontent.com; the other jobs only record their traffic.

CodeQL takes a new version a few days later than GitHub's feature flags would, with the next release of the action that Renovate brings.

The actions of others

The actions of others download their own tools. Each was looked at on 9 November 2026 (#762): whether it takes a token, by an input or in its environment, or a version that it then finds in the tool cache of the runner, and whether it gets the token of the job, which has only the permissions of its job.

  • Harden-Runner (step-security/harden-runner) downloads its agent in every job, from the page of its release on github.com. Its input token defaults to the token of the job, which it sends with the download, and the workflows leave it so. The page answered 504 on 8 November 2026 all the same, three times in a job of paperless-sync with egress-policy: block, which then ran without its block of hosts. It takes no version of the agent and looks in no cache, so no step of a workflow can bring it the agent; since decision 0134, a job that blocks stops instead. What would help is the agent through the API, GET /repos/step-security/agent/releases/assets/{id} with that token, at least once the page fails; an issue upstream is for the maintainer to open.
  • gitleaks (gitleaks/gitleaks-action) requires the token of the job in GITHUB_TOKEN, but uses it only for the API, and downloads gitleaks from the page of its release without a token. It first restores gitleaks from the cache of Actions, under a key of its version, which the run that downloaded it saved, so only a secret scan without that cache downloads at all: after a new version, or once GitHub evicted the cache. Since 10 November 2026, the secret scan leaves the action and runs gitleaks of its release with .github/download.sh, as the link check runs lychee, and writes the summary of the findings itself (decision 0139).
  • syft came from the installer that anchore/sbom-action downloads from raw.githubusercontent.com at the tag of the version, which downloads syft from the page of its release without a token, unless the tool cache holds the version of the input syft-version. The input github-token serves only the API, for the assets of a release and the dependency graph. The weekly SBOM leaves the action: it downloads the syft of the SBOMs of a release, at the version and sha256 that .github/pins.toml keeps, with .github/download.sh after the checkout, runs it as the action did, for the folder of the repository under its name and commit, and keeps the SBOM as an artifact of the run with actions/upload-artifact. That checks syft by the sha256 that the blueprint pins, and Renovate moves it with the syft of a release; syft-version had no rule of Renovate and had stayed at 1.52.0 while a release took 1.55.0. Only the step of the download has the token, and the action leaves the allowed actions.
  • uv (astral-sh/setup-uv) takes the version that the workflows pin from the tool cache, else from the mirror of Astral, releases.astral.sh, else from its release on GitHub with its input github-token, which defaults to the token of the job. Nothing changes; the Variants workflow already takes uv from GitHub when the mirror hangs.
  • Python and Node.js (actions/setup-python, actions/setup-node) take a version from the tool cache of the runner when it has it, which covers most jobs. Otherwise they download Python from the releases of actions/python-versions and Node.js from those of actions/node-versions, with the token of the job by their input token, but from the page of the release on github.com, the kind of download that answered 504 for Harden-Runner; Node.js then tries nodejs.org. Nothing changes: no step can give them another way.
  • PHP (shivammathur/setup-php) gives the token of the job only to the API and to the auth.json of Composer. It downloads its install script and the build of PHP from the pages of the releases of shivammathur/php-ubuntu, and composer.phar from those of shivammathur/composer-cache, with a plain curl -sL without a token, and after a failure from mirrors: cdn.jsdelivr.net, setup-php.com, artifacts.setup-php.com, dl.cloudsmith.io and getcomposer.org. The checks of a PHP project only record their traffic, so the mirrors take over there. The job release assets of build-release.yml, which signs a Nextcloud app, blocks every host but GitHub's, so in such an outage its setup of PHP 8.4 failed, and with it the build of the release, until it ran again after the outage. Allowing the mirrors in that job would let a step that runs next to the signing reach five more hosts for an outage of hours a year. Since 10 November 2026, the job takes the PHP 8.3 of the runner instead, which Nextcloud 35 runs on and which the runner ubuntu-24.04 ships with every extension that the signing needs and with Composer (#800). It asks for it with php-version: pre-installed, not with 8.3: on an image of GitHub for Ubuntu without ppa:ondrej/php, which none of them has, setup-php updates any version that it is given to its latest patch, even the one that the runner ships, and downloads that build from shivammathur/php-ubuntu as above; a run of the Variants with 8.3 logs Updated to PHP 8.3.36. Only for pre-installed does it take the PHP of the runner as it is and update nothing, so a change back to a fixed php-version would open the gap again. With tools: none it downloads no Composer either: the Composer of the runner stays for the before_cmds of a project's krankerl.toml. That also keeps setup-php from writing the token of the job, which may write the contents of the repository, into ~/.composer/auth.json, where the steps that build the project, krankerl and scripts/check-package.sh, could read it. A test keeps pre-installed and tools: none, and keeps the PHP that the runner of the job ships, in its list of runners, within the PHP that the occ of NEXTCLOUD_VERSION runs on; and the build runs the lib/versioncheck.php of that Nextcloud before it signs, and fails with an annotation when the image of the runner brings a PHP that this Nextcloud doesn't run on. Then the job moves to a runner that ships a PHP that it runs on, rather than downloading one. What would still help the checks is setup-php sending the token of the job with its downloads from github.com, or downloading them through the API, an issue upstream as well.
  • Images and packages: Renovate, Scorecard, OSV-Scanner and the publication on PyPI run images from ghcr.io, the container registry of GitHub, and zizmor installs its wheel from PyPI by the hashes of its lock, none of them an asset of a release; the context above names none of them among what failed. docker/setup-qemu-action pulls tonistiigi/binfmt and docker/setup-buildx-action starts moby/buildkit, both images from Docker Hub, in the job of a container image, which only records its traffic: no asset of a release on GitHub either. The runner pulls the image of a Docker action before the first step of its job, where no step can give it a token.

The other actions bring their code with them, or ask the API with the token of the job, such as actions/checkout and actions/create-github-app-token, and download no tool. The jobs that block keep their allowed hosts: the weekly SBOM only records its traffic, the API and release-assets.githubusercontent.com were allowed already, and the job release assets needs no mirror of setup-php, as said above.

The downloads of the published website of GitHub Pages, of raw.githubusercontent.com and of other hosts stay with curl, which tries again: they need no token, or aren't GitHub's.

A test fails on a curl or wget of the page of a release on github.com, and on the action of lychee, in the workflows and scripts of every kind of project.