GitHub Actions self-hosted runner 업데이트: 30일 버전 제한을 API로 점검하기
GitHub Actions self-hosted runner 업데이트를 점검해야 하는 첫 장면은 서버 터미널이 아니라 작업 화면이다. 워크플로 작업이 Queued에 오래 머물거나 Settings → Actions → Runners에서 러너가 Offline으로 보인다면, 운영자는 작업을 다시 누르기 전에 러너가 연결돼 있는지와 GitHub가 요구하는 버전인지 함께 확인해야 한다.
이 글은 2026년 8월 16일 GitHub 공식 자료와 공개 GitHub REST API를 대조한 절차다. REST API는 HTTP(웹에서 요청과 응답을 주고받는 통신 규칙) 요청으로 러너 상태를 읽는 공식 창구이며, 공개 릴리스 조회의 정상·실패 응답과 버전 판정 분기는 직접 확인했다. 인증된 운영 러너의 교체·재시작·워크플로 실행·롤백은 수행하지 않았으므로 해당 부분은 공식 문서에 따른 운영 체크리스트로 구분한다.
20초 핵심 요약
- 무엇: 대기 또는 오프라인 경고가 난 셀프 호스티드 러너의
version,status,busy를 API로 확인한다. - 왜: 새 버전이 제공된 뒤 30일 안에 갱신하지 않으면 작업을 받지 못할 수 있고, 중요 보안 업데이트는 더 일찍 차단될 수 있다.
- 어떻게: 작업을 비운 뒤 기대 버전으로 교체하고, API의 새 버전·온라인 상태와 실제 작업 한 건의 성공을 함께 확인한다.
1. Queued와 Offline을 하나의 화면 흐름에서 확인한다
먼저 Actions의 대기 작업을 열고, 이어서 Settings → Actions → Runners로 이동한다. Idle은 GitHub에 연결돼 작업을 받을 준비가 된 상태, Active는 작업 실행 중, Offline은 연결이 끊긴 상태다. 일치하는 온라인·유휴 러너가 없으면 작업은 바로 실패하지 않고 최대 24시간 대기할 수 있다.
Queued + Offline이면 서버 전원, 러너 프로그램의 서비스 상태, GitHub 통신 순으로 확인한다. Queued + Idle이면 버전 문제로 단정하지 않는다. 워크플로의 runs-on은 작업을 받을 러너를 고르는 조건이므로, 여기에 적힌 레이블과 러너 그룹이 실제 러너의 레이블·접근 범위와 일치하는지 먼저 본다.
이 단계의 정상 결과는 대기 작업에 맞는 러너를 하나 이상 식별하는 것이다. 찾지 못했다면 업데이트를 시작할 때가 아니라 등록 범위와 라우팅 조건부터 고칠 때다.
2. 읽기 API에서 version·status·busy를 한 번에 모은다
토큰(token)은 비밀번호처럼 API 호출 권한을 증명하는 비밀 문자열이다. 조직 범위를 읽는 세분화 토큰에는 Self-hosted runners: read, 저장소 범위에는 Administration: read 권한이 필요하다. 토큰 값은 명령 출력, 디버그 추적, 캡처에 남기지 않는다.
러너를 등록한 범위에 따라 다음 목록 API 중 하나를 고른다.
GET /repos/{owner}/{repo}/actions/runners
GET /orgs/{org}/actions/runners
GET /enterprises/{enterprise}/actions/runners
응답에서는 name, version, status, busy, ephemeral, labels를 함께 보존한다. status=online은 연결됐다는 뜻이고, busy=false는 현재 작업을 실행하지 않는다는 뜻이다. 두 값이 정상이어도 레이블과 그룹이 runs-on 조건에 맞지 않으면 작업을 받지 못한다.
목록은 기본 30개, 페이지당 최대 100개다. 러너가 더 많다면 응답의 Link 헤더를 따라 마지막 페이지까지 읽어야 한다. 일부 페이지만 읽으면 누락된 러너를 정상으로 오판할 수 있다.
HTTP 401·403은 인증이나 권한, 404는 범위나 URL, 429는 호출 제한을 먼저 확인한다. version이 없거나 페이지 수집이 끝나지 않았을 때도 최신판을 추정하지 말고 unknown, 즉 판정 불가로 닫는다.
3. 2.329.0과 현재 기대 버전을 따로 판정한다
2.329.0은 등록 또는 재등록에 필요한 하한이지, 계속 작업을 받을 수 있다는 영구 기준이 아니다. GitHub의 정책은 새 major·minor·patch 버전이 제공될 때마다 이동한다. 새 버전을 30일 안에 적용하지 않으면 작업 큐잉이 차단될 수 있고, 중요 보안 업데이트는 30일보다 이른 차단이 가능하다.
공개 actions/runner 최신 릴리스는 선행 경보로 사용한다. 2026년 8월 16일 Linux 환경(curl 8.5.0, jq 1.7)에서 공개 Releases API를 조회했을 때 비초안·비프리릴리스 v2.336.0, 공개 시각 2026-07-20T17:45:55Z를 확인했다.
$ response=$(curl --silent --show-error --location --header 'Accept: application/vnd.github+json' --header 'X-GitHub-Api-Version: 2026-03-10' --write-out '\nHTTP_STATUS:%{http_code}' 'https://api.github.com/repos/actions/runner/releases/latest'); curl_exit=$?; printf '%s\n' "$response" | sed -n '/HTTP_STATUS:/p'; printf '%s\n' "$response" | sed '/HTTP_STATUS:/d' | jq '{tag_name, published_at, draft, prerelease}'; jq_exit=$?; printf 'curl_exit=%s jq_exit=%s\n' "$curl_exit" "$jq_exit"
HTTP_STATUS:200
{
"tag_name": "v2.336.0",
"published_at": "2026-07-20T17:45:55Z",
"draft": false,
"prerelease": false
}
jq_exit=0
$ curl [공식 Accept/API-Version 헤더] https://api.github.com/repos/actions/runner/releases/tags/v0.0.0-does-not-exist
http=404 curl_exit=0
{
"message": "Not Found",
"status": "404"
}
curl_exit=0 jq_exit=0

이 정상 조회와 달리 존재하지 않는 태그를 기본 curl로 조회했을 때는 HTTP 404인데도 curl_exit=0이었다. JSON(요청 결과를 이름과 값의 구조로 담는 텍스트 형식) 해석까지 성공하므로, 자동 점검이 JSON 파싱만 보면 실패를 놓친다. --fail-with-body를 추가해 같은 실패를 다시 실행하자 curl_exit=22로 바뀌었다.
$ curl --silent --show-error --fail-with-body --location --header 'Accept: application/vnd.github+json' --header 'X-GitHub-Api-Version: 2026-03-10' 'https://api.github.com/repos/actions/runner/releases/tags/v0.0.0-does-not-exist' | jq '{message, status}'; pipeline_status=("${PIPESTATUS[@]}"); printf 'curl_exit=%s jq_exit=%s\n' "${pipeline_status[0]}" "${pipeline_status[1]}"
curl: (22) The requested URL returned error: 404
{
"message": "Not Found",
"status": "404"
}
curl_exit=22 jq_exit=0

따라서 자동 점검은 HTTP 상태를 별도로 검사하거나 curl --fail-with-body를 사용해야 한다. 두 캡처는 bash의 가상 터미널(PTY) 세션을 터미널 에뮬레이터가 실행 중에 표시한 화면이며, 같은 실행의 원시 세션 기록을 검토 자료로 보존했다.
공개 최신판이 곧 모든 조직·저장소의 기대 버전인 것은 아니다. GitHub는 릴리스를 점진 배포할 수 있다고 밝힌다. Settings → Actions → Runners → New self-hosted runner의 다운로드 안내에서 자신의 관리 범위에 제시된 버전을 다시 확인하고, 공개판과 다르면 강제 교체하지 말고 unknown으로 남긴다.
30일 계산에도 같은 주의가 필요하다. 최신 릴리스 하나의 나이만 계산하면 훨씬 오래된 설치본이 중간 릴리스를 이미 놓쳤다는 사실을 가릴 수 있다. 의미 버전(SemVer)은 major.minor.patch 숫자를 차례로 비교하는 버전 규칙이다. 문자열 정렬은 2.99와 2.100의 순서를 틀릴 수 있으므로, 설치 버전보다 높은 적용 대상 릴리스 중 처음 놓친 버전을 찾아야 한다.
직접 만든 고정 입력 비교에서는 설치 버전만 바꿨을 때 다음처럼 판정이 갈렸다. 이 결과는 판정 분기를 확인한 것이며 GitHub의 실제 작업 차단을 재현한 결과가 아니다.
$ check_runner_version() { installed=$1; latest=$2; published=$3; now=$4; case "$installed" in [0-9]*.[0-9]*.[0-9]*) ;; *) printf 'status=unknown reason=invalid_runner_version\n'; return 2 ;; esac; age_days=$(( ($(date -u -d "$now" +%s) - $(date -u -d "$published" +%s)) / 86400 )); if [ "$(printf '%s\n%s\n' "$installed" "$latest" | sort -V | head -n1)" = "$installed" ] && [ "$installed" != "$latest" ]; then printf 'status=watch installed=%s latest=%s release_age_days=%s\n' "$installed" "$latest" "$age_days"; else printf 'status=current installed=%s latest=%s release_age_days=%s\n' "$installed" "$latest" "$age_days"; fi; }; for installed in 2.335.0 2.336.0 missing; do printf 'input=%s latest=2.336.0 published=2026-07-20T17:45:55Z now=2026-08-16T00:35:01Z\n' "$installed"; check_runner_version "$installed" 2.336.0 2026-07-20T17:45:55Z 2026-08-16T00:35:01Z; printf 'exit=%s\n' "$?"; done
input=2.335.0 latest=2.336.0 published=2026-07-20T17:45:55Z now=2026-08-16T00:35:01Z
status=watch installed=2.335.0 latest=2.336.0 release_age_days=26
exit=0
input=2.336.0 latest=2.336.0 published=2026-07-20T17:45:55Z now=2026-08-16T00:35:01Z
status=current installed=2.336.0 latest=2.336.0 release_age_days=26
exit=0
input=missing latest=2.336.0 published=2026-07-20T17:45:55Z now=2026-08-16T00:35:01Z
status=unknown reason=invalid_runner_version
exit=2
2026년 6월 12일 공지는 일반 GitHub Enterprise Cloud의 brownout을 2026년 8월 24일부터, 전체 적용을 9월 25일로 안내했다. brownout은 낡은 러너가 작업을 받지 못하는 상황을 미리 확인하도록 일정 시간 수신을 막는 시험이다. 데이터 저장 지역을 선택하는 Data Residency 환경의 전체 적용일은 7월 31일이었다. 그러나 2026년 8월 16일 현재 이 공지는 Retired, 즉 폐기된 일정으로 표시돼 있으므로 이 날짜들을 현재 확정 일정으로 사용하면 안 된다. 정확히 30일째의 내부 판정 방식도 공개되지 않았으므로, 게시·적용 직전에 후속 공식 안내를 다시 확인하고 30일째를 안전 여유로 쓰지 않는다.
4. busy가 풀릴 때까지 기다리고 되돌릴 지점을 만든다
busy=true인 러너는 지금 작업을 실행하고 있다. 업데이트를 위해 바로 중지하지 말고 busy=false가 될 때까지 기다린다. 새 작업 유입을 막는 방식은 러너 그룹, 레이블, 자동 확장 구성에 따라 다르므로 현재 환경의 정책으로 선택한다.
기다리는 동안 현재 패키지, 컨테이너 이미지 태그, 구성, 서비스 이름을 기록한다. 되돌릴 이전 패키지나 이미지를 보존하고, 같은 테스트 작업을 다시 실행할 수 있게 준비한다. 강제 종료가 불가피하다면 해당 워크플로를 안전하게 재실행할 수 있는지와 외부 시스템에 남길 부작용을 별도 승인 절차에서 먼저 판단해야 한다.
아래 상태도는 왼쪽의 사용자가 보는 Queued에서 시작해 화살표를 따라 읽는다. 아래쪽 실패 화살표는 어느 단계에서든 로그 보존과 롤백으로 돌아간다.

완료 지점은 서비스 시작이 아니라 새 버전의 러너가 실제 작업 한 건을 성공시킨 순간이다.
5. 공식 패키지나 새 이미지를 준비해 서비스를 교체한다
운영체제와 CPU(명령을 계산하고 실행하는 중앙 처리 장치) 아키텍처에 맞는 공식 릴리스 자산을 받고 게시된 SHA-256 체크섬과 대조한다. SHA-256은 파일 내용을 고정 길이 값으로 바꾸는 해시 방식이며, 받은 파일이 공식 배포본과 같은지 확인하는 데 필요하다.
기본 러너는 자동 업데이트하지만 업데이트 서비스에 접근할 수 있어야 한다. --disableupdate를 사용했거나, ARC(Actions Runner Controller, 쿠버네티스에서 러너 수를 늘리고 줄이는 구성 요소)에 disableUpdate=true를 설정했거나, 고정 컨테이너 이미지를 쓴다면 운영자가 새 이미지와 배포를 준비해야 한다. 실행 중인 디렉터리를 덮기보다 새 이미지로 교체하고 이전 이미지를 복귀 후보로 남긴다.
6. 서비스를 시작하고 GitHub 연결 표시를 확인한다
Linux에서 프로그램을 백그라운드 서비스로 관리하는 systemd를 쓴다면 설치 디렉터리에서 공식 명령을 사용한다.
sudo ./svc.sh stop
sudo ./svc.sh start
sudo ./svc.sh status
정상 시작의 첫 표시는 active/running, Connected to GitHub, Listening for Jobs다. 여기서 멈추지 않는다. 시작에 실패하면 설치 디렉터리 _diag의 Runner_* 로그, Linux의 journalctl, config.sh --check 네트워크 결과를 확인한다. 자동 업데이트 문제는 _diag의 SelfUpdate 로그와 업데이트 서비스 접근성을 본다. TLS는 전송 내용을 암호화하고 접속 상대를 확인하는 보안 규약이며, 검증을 끄는 임시 우회는 사용하지 않는다.
7. 같은 API에서 새 버전과 online을 재확인한다
교체 전과 같은 인벤토리 조회를 다시 실행한다. 해당 러너의 version이 기대값으로 바뀌고 status=online, busy=false여야 다음 확인으로 넘어간다. 서비스는 실행 중인데 버전이 그대로라면 잘못된 설치 경로, 남아 있는 이전 프로세스, 고정 이미지나 자동 확장 템플릿을 조사한다. offline이면 서비스·로그·네트워크 문제부터 복구한다.
8. 실제 작업 한 건의 success로 완료를 판정한다
마지막으로 그 러너만 고를 수 있는 테스트 레이블을 사용해 안전한 워크플로 한 건을 실행한다. 화면 또는 workflow runs/jobs API에서 queued → in_progress → completed 순서와 최종 conclusion=success를 확인한다. online은 연결 성공일 뿐 실제 작업 성공이 아니다.
테스트가 실패하면 Worker_*와 워크플로 로그를 보존하고 새 작업 유입을 다시 막는다. 원인을 바로 고칠 수 없다면 이전 패키지나 이미지로 돌아간 뒤, API의 버전·온라인 상태와 같은 테스트 작업의 성공을 다시 확인한다. 이 두 확인을 통과해야 롤백도 완료다.
| 보이는 결과 | 먼저 볼 곳 | 다음 행동 |
|---|---|---|
Queued + Offline |
서비스 상태, _diag/Runner_*, 네트워크 |
연결을 복구한 뒤 API를 다시 조회한다. |
Queued + online/idle |
runs-on 레이블, 그룹 접근 권한, 저장소 허용 범위 |
라우팅 조건을 맞추고 작업 배정을 다시 본다. |
API 401·403·404·429 또는 version 누락 |
토큰 권한, 등록 범위, URL, 호출 제한, API 버전, 페이지네이션 | 값을 추정하지 않고 수집을 고친다. |
| 서비스 active, API 버전은 이전 | 설치 경로, 이전 프로세스, 이미지·템플릿 | 실제 실행 중인 대상을 교체한다. |
| API는 새 버전·online, 작업은 실패 | Worker_*, 워크플로 로그 |
유입을 막고 수정하거나 이전 버전으로 롤백한다. |
운영 자동화는 unknown에서 멈추게 한다
공개 최신 릴리스 API는 유용한 경보다. 그러나 특정 고객 범위의 제공 시각과 실제 차단 여부까지 혼자 증명하지는 못한다. 인증 실패, 누락된 페이지, 잘못된 버전 문자열, 관리 화면과 공개판의 불일치 중 하나라도 있으면 자동 교체 대신 unknown으로 실패시키는 편이 안전하다.
이 절차는 GitHub.com과 GitHub Enterprise Cloud를 기준으로 한다. GHES(GitHub Enterprise Server, 조직이 자체 서버에 설치하는 GitHub)는 2026년 3월 13일 공지 당시 이 변경의 적용 대상이 아니었으므로 자신의 GHES 버전 문서를 별도로 확인해야 한다. 운영에 넣기 전에는 GitHub 최소 버전 적용 공지와 자신의 Runners 다운로드 안내를 다시 확인한다.
참고 링크
- GitHub Docs: Self-hosted runners reference
- GitHub Docs: REST API endpoints for self-hosted runners
- GitHub Docs: Monitoring and troubleshooting self-hosted runners
- GitHub Docs: Configuring the runner application as a service
- GitHub actions/runner releases
- GitHub Docs: REST API endpoints for workflow runs