Skip to content

reuse:stop warm restart: wait loop sees runner online (elapsed_s=0) but no runner ever serves the label — job queued forever, stop-runner never fires #67

Description

@dm1tr1yvovk

Summary

On a reuse: stop warm restart, mode: start (v4.0.1, 65fbe4f) declared the runner registered and exited successfully, but no runner for the new label ever picked up work. The dependent job sat in queued for 3+ hours (GitHub waits up to 24h for a self-hosted label), which also means the consumer workflow's stop job — gated on the acceptance job reaching a terminal state — never fired: the pooled instance was left running and holding the whitelisted sandbox EIP until manual intervention.

Evidence

Failing run (terraform-provider-namecheap CI, pull_request): https://github.com/namecheap/terraform-provider-namecheap/actions/runs/29019773728

  • Start self-hosted EC2 runner (job 86124175784) — success, 2026-07-09T13:02–13:05 UTC:
    13:04:37 Pre-attached eipalloc-1796f61b to stopped warm-pool instance i-049890a74e3bf0c4d.
    13:05:25 Checking every 10s for bootstrap failures and runner registration
    13:05:26 {"step":"wait_for_runner","mode":"start","outcome":"online","elapsed_s":0}
    13:05:26 {"step":"start","mode":"start","label":"xsvha","instance_ids":["i-049890a74e3bf0c4d"],"instance_type":"t3.medium","subnet_id":"subnet-01c4ff5a","outcome":"registered"}
    
  • Acceptance test (job 86124838100, runs-on: xsvha) — queued at 13:06:20, never picked up (3h+ at time of filing).
  • GET /repos/namecheap/terraform-provider-namecheap/actions/runners at ~15:50 UTC returned an empty list — no runner with label xsvha was registered at all by then. Either the registration the wait loop observed vanished within the first minute, or it observed something stale.

Contrast: successful run ~45 min earlier, same instance: https://github.com/namecheap/terraform-provider-namecheap/actions/runs/29017428944

  • Identical warm-restart path, label 3xo8q, same i-049890a74e3bf0c4d, same {"step":"wait_for_runner","outcome":"online","elapsed_s":0} — but there the runner did take the job (12:18–12:20). Its mode: stop then logged:
    {"step":"remove_runner","mode":"stop","label":"3xo8q","skipped":true,"reason":"not_found"}
    
    i.e. the ephemeral runner had already deregistered after its single job — expected, but it shows the wait loop's success signal is indistinguishable between a healthy run and this failure.

Analysis

src/gh.js isOnline(label) returns true on a single instantaneous runner.status === 'online' check, and the wait loop in src/wait.js treats the first such observation as terminal success. On a warm restart that leaves two gaps:

  1. No freshness guarantee. Success at elapsed_s: 0 means the very first poll matched. Nothing ties the observed registration to this boot of the instance — a registration that immediately drops (runner process exits after config.sh/on first connect, EIP/network hiccup, stale record not yet purged by GitHub) still counts as success.
  2. No liveness follow-through. The action exits green the same second; if the runner dies before the dependent job is scheduled (~54s later here), nothing detects it. The consumer is left with a forever-queued job and — because typical stop jobs need the runner-consuming job — a leaked running instance holding the EIP.

This is the same general area as the v4.0.0/v4.0.1 reuse: stop registration fixes (#62, #64, #65, #66), so it looks like a remaining race rather than a regression.

Suggested fixes

  1. Require a fresh registration: record the runner set (or GET /actions/runners ids) for the label before StartInstances, and only accept a runner whose id/registration is new relative to that snapshot — or require the poll loop to observe the registration appear (absent → online), never accept a first-poll instant match on warm restarts.
  2. Confirm stability, not an instant: require N consecutive online polls (e.g. 2–3 over 20–30s) before declaring success, so a registration that flaps offline within seconds fails the start step instead of the downstream job queueing forever.
  3. Diagnostics on this path: when mode: start succeeded but the runner disappears, there is currently zero instance-side evidence in the workflow logs. Extending the Bootstrap diagnostics: phone-home status tags, fast-fail, console-output capture, cleanup on failed start #41-style bootstrap diagnostics (console-output capture / phone-home tag) to cover the warm-restart re-registration would make the next occurrence debuggable.
  4. Docs: recommend consumers add a post-start verification (poll /actions/runners for the label from the start job) and/or a reaper rule for runs whose label job stays queued with no online runner, since a leaked running instance also keeps holding the EIP that the next run's pre-attach step expects to find on a stopped instance.

Environment

  • Action: namecheap/ec2-github-runner@65fbe4f0c1181c60d4df2bd3b33b8893e21fbb43 (v4.0.1)
  • Consumer workflow: terraform-provider-namecheap .github/workflows/ci.yml (reuse: stop, reuse-pool-tag: sandbox-acceptance, t3.medium, EIP pre-attach before start)
  • Instance: i-049890a74e3bf0c4d (warm pool, stopped at 12:20 UTC by the prior run, restarted 13:04 UTC)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions