Standup hears “the date moved.” Someone pasted the GitHub Changelog from 28 September 2026. The coding agent pasted the headline and filed all-clear: “we have extra time; skip the fleet this week.”
I stop the run there. A moved self-hosted runner deadline is not an all-clear. Official GitHub changelog for 28 September 2026 is blunt: the enforcement date for GitHub Actions minimum version requirements for self-hosted runners on GitHub Enterprise Cloud changed. The change ships Monday 28 September 2026. Full enforcement begins Tuesday 29 September 2026. That is instead of the date previously announced. The requirements themselves are unchanged. Today is Wednesday 30 September 2026. The extra day already ended. [Source: https://github.blog/changelog/2026-09-28-self-hosted-runner-version-enforcement-date-has-moved/]
I already refused to treat a 2,500+ Actions badge as an exact run inventory in A 2,500+ Actions Count Is Not an Exact Run Inventory. I already refused to treat ubuntu-latest as a runner image I already tested in Ubuntu-latest Is Not a Runner Image You Already Tested. I already refused to treat a timed-out Laravel job as a killed worker pool in A Timed-Out Job Is Not a Killed Worker. This post is the same desk rule for a moved date. Print the moved date. Print both floors. Name who owns the self-hosted fleet.
The question is not whether a moved date demos well in a screenshot. The question is whether the named owner can tell a moved date from a refused job before paging CI.

The ticket that looks like extra time
Juniors treat “date has moved” the way they treat a calendar invite that slipped a week. The headline is kind. The queue is not.
Two jobs collide on that screen.
- Stop treating a one-day slip as a freeze. GitHub’s own 28 September note says the change ships Monday and full enforcement begins Tuesday. The previous GHEC date on the June timeline was 25 September 2026. The new note does not reopen brownouts. It does not pause registration. It does not pause job execution. It moves the remaining full-enforcement day. [Source: https://github.blog/changelog/2026-09-28-self-hosted-runner-version-enforcement-date-has-moved/] [Source: https://github.blog/changelog/2026-06-12-github-actions-minimum-version-enforcement-timeline-for-self-hosted-runners/]
- Keep the two floors that did not move. Runners below the registration minimum cannot register or reregister. Existing runners below the runtime minimum stop running jobs even if they were previously registered. The runtime floor is higher than the registration floor. A pin that only meets registration is not a pin that still picks up work. [Source: https://github.blog/changelog/2026-09-28-self-hosted-runner-version-enforcement-date-has-moved/] [Source: https://github.blog/changelog/2026-06-12-github-actions-minimum-version-enforcement-timeline-for-self-hosted-runners/]
If you only screenshot “date has moved,” you will file extra time. You will not file the fleet.
I do not invent a fake overnight wipe. I use the public contract. The changelog page is the ticket, not a version pin in the title.
Three tickets, one owner
Laravel plus Vue work on this desk still sits next to GitHub Actions: a workflow under .github/workflows/, a self-hosted label for PHPUnit, a coding agent that treats a moved date as a pause. Mixing the calendar, the registration floor, and the runtime floor into one “we are fine” thread hides the owner.
| Ticket | What it means | What you do | Owner |
|---|---|---|---|
| Changelog says the date moved | Calendar slipped one day | Print ship day and full-enforcement day. Do not close the fleet | Named human who owns Actions runners |
| Runner cannot register | Below the registration floor | Stop ./config.sh on the old image. Recreate from a current binary | Same named human |
| Runner is online and still idle | Below the runtime floor, or auto-update is off and the 30-day window closed | Print version from the runners list and the deprecation dates for that version | Same named human |
Do not paste one headline and call the fleet current. If the screen says the date moved, you are on a calendar ticket. If ./config.sh fails on an old image, you are on a registration ticket. If the runner is online and jobs stay queued, you are on a runtime ticket.
Dates and floors are evidence, not the hook
2.329.0 cannot register or reregister. Runtime floor named in that note: a higher version than the registration minimum; existing runners below it stop jobs even if previously registered. June 12 already said 2.329.0 is the registration minimum, not a permanent job-execution pin, and that auto-update off (including ARC disableUpdate=true) needs a manual cadence. Print those strings on the ticket. Do not put 2.329.0 in a social title as a version hook. [Source: https://github.blog/changelog/2026-09-28-self-hosted-runner-version-enforcement-date-has-moved/] [Source: https://github.blog/changelog/2026-06-12-github-actions-minimum-version-enforcement-timeline-for-self-hosted-runners/]
What the list and deprecation endpoints actually return
I do not invent a fake JSON field. I use the public contract.
Official REST: GET /orgs/{org}/actions/runners lists self-hosted runners for an organization. Authenticated users need admin access on the org. The example response includes name, os, status, busy, ephemeral, version, and labels. Anyone writing “the runner is fine” without version is guessing. [Source: https://docs.github.com/en/rest/actions/self-hosted-runners]
Official REST also documents GET /orgs/{org}/actions/runners/deprecations/{version}. GitHub’s 3 September 2026 Actions update says the same path exists at repository, organization, or enterprise level as GET /actions/runners/deprecations/{version}. The 3 September note says the response includes runner_version, runtime_deprecates_at, and registration_deprecates_at. The docs example on the self-hosted-runners page shows runner_version and runtime_deprecates_at. If a key is absent in the JSON you actually saved, print MISSING. Do not invent a date. [Source: https://docs.github.com/en/rest/actions/self-hosted-runners] [Source: https://github.blog/changelog/2026-09-03-github-actions-early-september-2026-updates/]
Save the list. Save one deprecation payload. Probe the files. Do not live-loop the API from a coding agent until someone names the owner.
1#!/usr/bin/env python3
2"""Print moved-date vs floors from saved GitHub JSON. No live token."""
3from __future__ import annotations
4
5import json
6import sys
7from pathlib import Path
8
9REGISTRATION_FLOOR = "2.329.0"
10
11
12def ver_tuple(v: str) -> tuple[int, ...]:
13 return tuple(int(p) for p in v.split(".") if p.isdigit())
14
15
16def main() -> int:
17 runners_path = Path(sys.argv[1])
18 dep_path = Path(sys.argv[2]) if len(sys.argv) > 2 else None
19 payload = json.loads(runners_path.read_text(encoding="utf-8"))
20 rows = payload.get("runners") or []
21 print(f"RUNNER_COUNT={len(rows)}")
22 print(f"REGISTRATION_FLOOR={REGISTRATION_FLOOR}")
23 for r in rows:
24 name = r.get("name")
25 version = str(r.get("version") or "")
26 status = r.get("status")
27 busy = r.get("busy")
28 below_reg = (
29 bool(version) and ver_tuple(version) < ver_tuple(REGISTRATION_FLOOR)
30 )
31 print(
32 f"name={name} version={version} status={status} "
33 f"busy={busy} below_registration_floor={below_reg}"
34 )
35 if dep_path and dep_path.exists():
36 dep = json.loads(dep_path.read_text(encoding="utf-8"))
37 print(f"DEP_RUNNER_VERSION={dep.get('runner_version', 'MISSING')}")
38 print(
39 "DEP_REGISTRATION="
40 f"{dep.get('registration_deprecates_at', 'MISSING')}"
41 )
42 print(
43 f"DEP_RUNTIME={dep.get('runtime_deprecates_at', 'MISSING')}"
44 )
45 print("VERDICT=moved date is not all-clear")
46 return 0
47
48
49if __name__ == "__main__":
50 raise SystemExit(main())
Run it against files the named owner saved, not against a headline:
1python3 probe_runner_floors.py runners.json deprecation.json
A green status: online next to below_registration_floor=True is still a registration ticket the next time that box needs ./config.sh. A green status: online with a runtime_deprecates_at already in the past is a runtime ticket even if the box registered last year.

Registration floor versus runtime floor
June 12 already split the work. September 28 said the requirements are unchanged.
Registration. To configure or reregister, the runner must be on 2.329.0 or later. That is the minimum for the new architecture to recognize the runner. Older binaries fail ./config.sh. They do not quietly self-upgrade after an old config script. [Source: https://github.blog/changelog/2026-06-12-github-actions-minimum-version-enforcement-timeline-for-self-hosted-runners/]
Runtime. To keep executing jobs, the runner must install each new runner release within 30 days of publication. Auto-update on meets that rule when the box can reach the update service. Auto-update off, including Actions Runner Controller with disableUpdate=true, needs a manual cadence. A runner pinned forever at the registration minimum will, in GitHub’s own words, not pick up jobs. Any release — major, minor, or patch — counts as an available update. A critical security update pauses job queuing until applied. [Source: https://github.blog/changelog/2026-06-12-github-actions-minimum-version-enforcement-timeline-for-self-hosted-runners/]
That is why the 28 September note repeats two bullets, not one. Below the registration floor: no register, no reregister. Below the runtime floor: stop running jobs even if previously registered. [Source: https://github.blog/changelog/2026-09-28-self-hosted-runner-version-enforcement-date-has-moved/]
A workflow file does not prove either floor.
1name: phpunit
2on: [push]
3jobs:
4 phpunit:
5 runs-on: [self-hosted, linux, x64]
6 steps:
7 - uses: actions/checkout@v4
8 - name: This label is not a runner version
9 run: php artisan test
runs-on tells Actions which label to match. It does not print version. It does not print registration_deprecates_at. It does not print runtime_deprecates_at. The owner still opens the runners list and the deprecation payload.
If you need a live check after the files exist, the public org-scoped curl shape is the one in the docs. Replace ORG and VERSION. Do not paste a token into a ticket.
1curl -sS -L \
2 -H "Accept: application/vnd.github+json" \
3 -H "Authorization: Bearer $GH_TOKEN" \
4 -H "X-GitHub-Api-Version: 2026-03-10" \
5 "https://api.github.com/orgs/ORG/actions/runners" \
6 -o runners.json
7
8curl -sS -L \
9 -H "Accept: application/vnd.github+json" \
10 -H "Authorization: Bearer $GH_TOKEN" \
11 -H "X-GitHub-Api-Version: 2026-03-10" \
12 "https://api.github.com/orgs/ORG/actions/runners/deprecations/VERSION" \
13 -o deprecation.json
Save. Probe. Write the verdict next to ACTIONS_RUNNER_OWNER. Then close the laptop.
Scope still matters. The 28 September shift is GitHub Enterprise Cloud. GitHub Enterprise Server is not impacted by that note. Data Residency already enforced on 31 July 2026. A GHES screenshot is the wrong org. A GHEC screenshot of “date moved” on Wednesday 30 September is a day after full enforcement, not a pause. [Source: https://github.blog/changelog/2026-09-28-self-hosted-runner-version-enforcement-date-has-moved/]

What you must not mix into this ticket
This is not yesterday’s capped count. A 2,500+ badge is an inventory ticket. A 2,500+ Actions Count Is Not an Exact Run Inventory already owns that field.
This is not the runner-label ticket. ubuntu-latest moving later this year is a pin-and-test ticket for GitHub-hosted images. Ubuntu-latest Is Not a Runner Image You Already Tested already owns that field.
This is not the queue-timeout ticket. A job clock that kills one queue:work process is a Laravel worker ticket. A Timed-Out Job Is Not a Killed Worker already owns that field.
GSC this week still has no striking-distance query on those URLs. I am not refreshing them. This is a new field: a moved self-hosted runner deadline is not an all-clear.
If you need the broader habit, start at /ai-agent-operations/. Tooling notes live under /developer-tools/. Laravel plus Vue notes live under /laravel-vue-saas/. A first-week map is at /start-here/.
What you must not do
Forbidden:
- File an all-clear ticket without printing the moved date, the full-enforcement day, GHEC versus GHES, the registration floor, the runtime floor, and one human name on Actions runners.
- Put a runner version in the title or the first line as a version-style hook. The field name is the moved date, not a pin table.
- Mix this field with a 2,500+ run inventory, an
ubuntu-latestimage pin, or a Laravel job timeout. Those are other posts. - Treat “the date moved” as extra weeks. The 28 September note moved remaining full enforcement to 29 September 2026. [Source: https://github.blog/changelog/2026-09-28-self-hosted-runner-version-enforcement-date-has-moved/]
- Treat GitHub Enterprise Server as covered. The note says GHES is not impacted. [Source: https://github.blog/changelog/2026-09-28-self-hosted-runner-version-enforcement-date-has-moved/]
- Treat a runner at the registration minimum as permanently able to run jobs. June 12 already said that pin is not a permanent execution floor. [Source: https://github.blog/changelog/2026-06-12-github-actions-minimum-version-enforcement-timeline-for-self-hosted-runners/]
- Leave auto-update off, including ARC
disableUpdate=true, and still file all-clear. That fleet needs a named upgrade cadence. [Source: https://github.blog/changelog/2026-06-12-github-actions-minimum-version-enforcement-timeline-for-self-hosted-runners/] - Write a permission-bypass, a forged registration, or a runner that pretends to be newer than the binary on disk.
- Recommend buying Actions minutes, a Copilot plan, or a seat because a self-hosted box is old.
- Close the ticket from
runs-on: self-hostedin YAML. Labels are not versions.
Allowed:
- Print ship day, full-enforcement day, and org type (GHEC, GHEC with Data Residency, or GHES).
- Save
GET /orgs/{org}/actions/runnersand one deprecation payload. Runprobe_runner_floors.py. - Name one human as
ACTIONS_RUNNER_OWNER. - Recreate runners from a current binary before
./config.shwhen registration is the ticket. - Turn auto-update on, or schedule the manual cadence, when runtime is the ticket.
- Leave GHES out of this note unless a later official page says otherwise.
A changelog bullet about a moved date is not permission to skip the owner.
What you should do Monday morning
- Open the org that actually ships. Export
ACTIONS_RUNNER_OWNERto a human name. Save one runners list and one deprecation payload. Runprobe_runner_floors.py. Write the verdict on the ticket next to that name. - Open the 28 September changelog from the incident. Copy ship day. Copy full-enforcement day. Copy GHEC versus GHES. If today is after 29 September 2026 on GHEC, file “full enforcement already began,” not “we have extra time.” [Source: https://github.blog/changelog/2026-09-28-self-hosted-runner-version-enforcement-date-has-moved/]
- If a box needs to register, compare
versionto the registration floor before./config.sh. If a box is already registered and jobs sit queued, compareruntime_deprecates_atfor that version. Do not reuse the headline. [Source: https://docs.github.com/en/rest/actions/self-hosted-runners] [Source: https://github.blog/changelog/2026-09-03-github-actions-early-september-2026-updates/] - If someone pasted only a YAML
runs-onlabel, reject it. Point at the runners list and the deprecation endpoint. - Confirm coding-agent instructions on this desk name the same owner and forbid “the date moved, skip the fleet” without the six lines: owner, ship day, enforcement day, org type, registration floor, runtime floor.
- Do not refresh the 2,500+ post, the ubuntu-latest post, or the timed-out-job post. Those URLs already exist. This URL is the unused moved-date field.
The question is not whether a moved date demos well in a screenshot. The question is whether the named owner can still tell a moved date from a refused job after handoff.
Further reading
Source GitHub Changelog — Self-hosted runner version enforcement date has moved
Source GitHub Changelog — Minimum version enforcement timeline for self-hosted runners
Source GitHub Docs — REST API endpoints for self-hosted runners
