fix(ci): preserve ECS cleanup on runner API failures

Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>
This commit is contained in:
discord9
2026-09-08 21:05:10 +08:00
parent c8ace87cf0
commit 34661ea125
6 changed files with 78 additions and 16 deletions
@@ -90,9 +90,15 @@ tool contract. Build a new ECS image from it:
ALIBABA_CLOUD_ACCESS_KEY_ID=... ALIBABA_CLOUD_ACCESS_KEY_SECRET=... \
uv run .github/runner-scale-sets/query-regression/ecs-image/build-ecs-image.py \
--region-id <region> --vswitch-id <vsw-...> --security-group-id <sg-...> \
--base-image-id <ubuntu-24.04-image-id>
--base-image-id <validated-base-image-id>
```
The configured image has passed the workflow host checks for mold `2.40.4`
and Python `3.14.4`. The builder installs those host tools from distribution
packages; it does not pin or copy them from the container. A stock Ubuntu 24.04
base does not supply these versions. Validate the chosen base and the resulting
host against the workflow checks before replacing the configured image.
The script boots a temporary builder instance, `docker build`s the runner
image, materializes `/opt/rustup`, `/opt/cargo`, `/usr/local/bin` tools, and
`/home/runner` (actions-runner) onto the host, installs the ephemeral-runner
@@ -216,13 +216,15 @@ def github_api(token: str, method: str, path: str, body: dict | None = None) ->
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
return json.loads(response.read().decode("utf-8"))
payload = response.read()
return json.loads(payload.decode("utf-8")) if payload else {}
except urllib.error.HTTPError as error:
# GitHub's error body says exactly why (e.g. "Must have admin rights to
# Repository" for a PAT without the required scope); surface it instead
# of a bare "HTTP Error 403".
# of a bare "HTTP Error 403". This must remain catchable by teardown so
# a runner deregistration failure does not prevent ECS cleanup.
body = error.read().decode("utf-8", "replace")
raise SystemExit(
raise RuntimeError(
f"GitHub API {method} {path} failed: HTTP {error.code}: {body}\n"
"The token comes from the GH_PERSONAL_ACCESS_TOKEN secret; it needs "
"'repo' scope (classic PAT) or 'Administration: write' on the "
@@ -144,11 +144,11 @@ def delete_instance(client, instance_id: str, region_id: str | None = None) -> b
def deregister_runner(token: str, repo: str, runner_name: str) -> bool:
runner = provision.find_runner_by_name(token, repo, runner_name)
if runner is None:
print(f"Runner {runner_name} is not registered", flush=True)
return True
try:
runner = provision.find_runner_by_name(token, repo, runner_name)
if runner is None:
print(f"Runner {runner_name} is not registered", flush=True)
return True
provision.github_api(token, "DELETE", f"/repos/{repo}/actions/runners/{runner['id']}")
print(f"Deregistered runner {runner_name} (id {runner['id']})", flush=True)
return True