The problem
A GitHub Actions job stayed in the queued state even though the self-hosted runner service was running and GitHub showed the runner as online. The workflow's runs-on labels matched the labels displayed in the repository, so it initially looked like a runner-group or label-routing problem.
The real cause was the runner version. GitHub's self-hosted runner enforcement for GitHub Enterprise Cloud began on September 29, 2026. The host had an old runner binary that was no longer eligible to register or execute work under the current enforcement schedule.
What I checked first
I started with the normal routing checks rather than assuming the new policy was responsible:
jobs:
build:
runs-on: [self-hosted, linux, x64, production]I compared every requested label with the runner's labels, confirmed that the repository could access its runner group, and checked that no other job was occupying the runner. I also inspected the runner service logs and its locally installed version.
For an organization-level inventory, the REST API makes the state easier to audit:
gh api \
-H "X-GitHub-Api-Version: 2026-03-10" \
/orgs/ORG/actions/runnersGitHub also provides a version deprecation endpoint. Replace the organization and runner version before using it:
gh api \
-H "X-GitHub-Api-Version: 2026-03-10" \
/orgs/ORG/actions/runners/deprecations/2.328.0That separated an outdated-runner failure from two common lookalikes: a missing custom label and a runner group that does not allow the repository.
The fix
This runner came from an immutable machine image, so I updated the runner release in the image build instead of patching the live host manually. I then rebuilt the image, drained the old instance, launched a replacement, and let the new runner register with the same approved group and labels.
For mutable long-lived hosts, the equivalent fix is to stop the runner service, follow GitHub's official update procedure, and restart it. If automatic updates were deliberately disabled, the image or package pipeline must still keep the runner inside GitHub's supported window.
I avoided unregistering and re-registering the old binary repeatedly. Registration attempts do not make an unsupported version current, and they can hide the useful error among new tokens and service logs.
Verification
After the replacement registered, I verified all four conditions:
- GitHub displayed the expected new runner version.
- Runner group and labels still matched the workflow.
- A small diagnostic workflow was assigned immediately.
- The production workflow completed on the replacement host.
I also added a scheduled inventory check so the team sees runner versions before enforcement affects jobs. The alert distinguishes an offline runner, an idle-but-ineligible runner, and a valid runner with a label mismatch.
The lesson
“Online” is not the same as “eligible to run this job.” When a self-hosted job remains queued, check access, labels, capacity, and version support as separate dimensions. Immutable runners also need a regular image rebuild cadence; disabling in-place auto-update transfers the update responsibility to your image pipeline.
For a complete audit and rollout process, see the self-hosted runner enforcement upgrade guide.
Learn GitHub Actions
If you want structured practice with runners, workflows, and CI/CD troubleshooting, see this GitHub Actions course on Udemy.
Affiliate link: DevOpsBoys may earn a commission at no extra cost to you.