Update your self-hosted runners before Laravel CI stops picking up jobs
GitHub now blocks old runners in Enterprise Cloud, and its API can flag versions approaching retirement.
The failure will not be in Laravel
Since September 29, 2026, GitHub Enterprise Cloud has enforced minimum version requirements for GitHub Actions self-hosted runners. If one of my runners falls below the version required to execute jobs, workflows can remain queued even when the Laravel code, Composer setup, and test suite are fine. It is an awkward failure mode because it can look like a capacity or label issue while the runner is simply no longer eligible to take the job. It also affects runners that had already been registered before enforcement began.
GitHub treats runner registration and job execution as separate cases. Versions below 2.329.0 can no longer register or re-register with GitHub Enterprise Cloud. There is a different, higher minimum for running jobs, but the announcement does not publish that exact version. That is why I would not manage this by comparing every runner in the fleet with one hard-coded version number.
Start with the versions that are actually deployed
A homelab can easily end up with one runner in a VM, another one in a container, and one more forgotten on a dedicated machine. The GitHub interface is useful for checking individual cases, but I prefer the API when I need an inventory of the whole fleet. The runner listing endpoint returns the installed version alongside the other runner details. Once I have that list, I can request the retirement schedule for every version in use.
This script paginates through the runners in an organization, removes duplicate versions, and retrieves their end-of-life dates. It needs a token with read access to the organization’s self-hosted runners, and the authenticated account needs organization admin access. I keep the token outside the script, such as in an environment variable or in the secret store of the machine running the check. The output is one JSON document per version, which makes it straightforward to send into an alerting system without scraping a web page.
#!/usr/bin/env bash
set -euo pipefail
: "${GITHUB_TOKEN:?GITHUB_TOKEN is required}"
: "${GITHUB_ORG:?GITHUB_ORG is required}"
api="https://api.github.com"
headers=(
-H "Accept: application/vnd.github+json"
-H "Authorization: Bearer ${GITHUB_TOKEN}"
-H "X-GitHub-Api-Version: 2026-03-10"
)
page=1
while :; do
response="$(
curl -fsSL "${headers[@]}" \
"${api}/orgs/${GITHUB_ORG}/actions/runners?per_page=100&page=${page}"
)"
count="$(jq '.runners | length' <<< "${response}")"
jq -r '.runners[].version' <<< "${response}"
if [ "${count}" -lt 100 ]; then
break
fi
page=$((page + 1))
done |
sort -u |
while read -r version; do
schedule="$(
curl -fsSL "${headers[@]}" \
"${api}/orgs/${GITHUB_ORG}/actions/runners/deprecations/${version}"
)"
jq -cn \
--arg version "${version}" \
--argjson schedule "${schedule}" \
'{
version: $version,
registration_deprecates_at: $schedule.registration_deprecates_at,
runtime_deprecates_at: $schedule.runtime_deprecates_at
}'
doneAlert on both dates
The deprecation endpoint takes a specific version in its path: GET /orgs/{org}/actions/runners/deprecations/{version}. Its response identifies the version and can include registration_deprecates_at and runtime_deprecates_at. For runners that are already online, the runtime date matters most because it marks the point when they stop accepting jobs. The registration date matters when rebuilding a VM, replacing a container, or bringing back a runner that was removed.
I would not wait until the retirement date to create an alert. I would run this check daily and treat an approaching date as a maintenance task for the image or VM. When every runner is built from the same Docker image or Proxmox template, the fix should begin there instead of being applied manually to individual instances. After updating or rebuilding the runners, I would confirm that the inventory reports the expected version.
Important limitations
This enforcement applies to GitHub Enterprise Cloud; it is not a GitHub Enterprise Server change. Enforcement had already started on July 31, 2026 for GitHub Enterprise Cloud with Data Residency, so September 29 should not be treated as a universal deadline for every environment. The API can retrieve a version’s schedule, but it does not update the runner binary or rebuild a container image for me. An alert identifies maintenance debt; operating the infrastructure is still my responsibility.
I would not assume automatic updates are enabled either. GitHub notes that the runner application can receive automatic updates, but those updates can be disabled and they do not cover the operating system or the other software installed on the machine. An up-to-date runner can still fail because of a broken image, insufficient permissions, inaccessible Docker, or labels that do not match the workflow. Before changing production runners, I would validate the new image with a Laravel workflow that at least runs composer install, the test suite, and the project’s build step.
Make it part of routine maintenance
The useful part of this change is not memorizing a runner version. It is bringing runners into the ordinary inventory of the homelab. For containers, I review the base image and rebuild process; for VMs, I document the source template and update path. I also keep ephemeral and persistent runners separate, because ephemeral ones are easy to replace while persistent ones collect more state than expected.
That changes the response when GitHub retires a version. Instead of diagnosing why Laravel is not picking up jobs, I can deploy an image that has already been prepared and tested. The same inventory also makes it easier to spot runners that have drifted away from the standard build. That is a much better place to be than discovering the issue through a blocked deployment.