Skip to content

Adopt the actions/scaleset SDK - #70

Merged
existemi merged 5 commits into
mainfrom
rl/scaleset-sdk
Aug 20, 2026
Merged

Adopt the actions/scaleset SDK#70
existemi merged 5 commits into
mainfrom
rl/scaleset-sdk

Conversation

@existemi

@existemi existemi commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Description

Replace the hand-rolled GitHub Actions scale set API calls with GitHub's actions/scaleset SDK, the standalone Go client extracted from the Actions Runner Controller.

  • Add pkg/github/scalesetclient, which implements the existing ActionsService interface on top of the SDK, with a compile-time assertion that the two stay in sync
  • Derive the GitHub API URL from GITHUB_URL and log a warning when the now-unused GITHUB_API_URL is set
  • Detect and skip messages that cannot be processed, rather than stalling the message loop
  • Raise the default MAX_RUNNERS to 9000

Testing

  1. Run the integration against an organization and confirm the runner creates its scale set and begins processing messages.
  2. Run a workflow and confirm a VM is provisioned and the job completes.

@existemi
existemi marked this pull request as ready for review August 19, 2026 02:30
@existemi
existemi requested a review from a team as a code owner August 19, 2026 02:30
@ispasov

ispasov commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Two things on the new scalesetclient — both about how we read errors back from the SDK. Nice work overall, the GHES-shaped stub tests are great.

suggestion — pkg/github/scalesetclient/client.go, withStatusCode / statusCodePattern: We can drop the regex here and match on the SDK's typed error instead. actions/scaleset wraps every HTTP failure through wrapResponseErrorType, which attaches a sentinel (BadRequestError, UnauthorizedError, NotFoundError, ConflictError) using %w, and createMessageSession propagates it up the chain with %w as well. So the 409 we care about for session-conflict recovery is reachable directly:

if errors.Is(err, scaleset.ConflictError) { ... }

Parsing status="(\d{3})" out of the formatted error string works today, but it silently breaks the moment upstream changes their error wording — and because the only consumer is createSessionWithRetry's "retry on 409, bail otherwise" logic, a silent break means startup stops recovering from a stale session (the common restart case). errors.Is on the exported sentinel gives the same signal and fails at compile time instead. I checked the SDK and it does not retry the 409 itself, so createSessionWithRetry should stay — it's just the status extraction that can go.

context / nit (keep it, but let's track it) — pkg/github/scalesetclient/client.go, GetMessage / skippableMessage: The strings.Contains(err.Error(), "unsupported message type") match is unfortunately the only handle the SDK gives us — parseRunnerScaleSetMessageResponse returns a plain fmt.Errorf("unsupported message type: %s", ...) with no sentinel and no wrap, so there's nothing typed to match on. Worth keeping the workaround: when the SDK hits an unknown outer message type it returns without acking the message, so the queue head never advances and the listener wedges permanently on it — the re-read-and-skip path is what prevents that. (Unknown inner job-message types are already safe; the SDK's inner switch has an empty default.) Two small asks: (a) a short comment here explaining why we're string-matching, so nobody "cleans it up" later, and (b) an upstream issue on actions/scaleset for a typed error (or SDK-side skip+ack of unknown outer types) — once that lands, this whole block can go.

@existemi

Copy link
Copy Markdown
Contributor Author

Two things on the new scalesetclient — both about how we read errors back from the SDK. Nice work overall, the GHES-shaped stub tests are great.

suggestion — pkg/github/scalesetclient/client.go, withStatusCode / statusCodePattern: We can drop the regex here and match on the SDK's typed error instead. actions/scaleset wraps every HTTP failure through wrapResponseErrorType, which attaches a sentinel (BadRequestError, UnauthorizedError, NotFoundError, ConflictError) using %w, and createMessageSession propagates it up the chain with %w as well. So the 409 we care about for session-conflict recovery is reachable directly:

if errors.Is(err, scaleset.ConflictError) { ... }

Parsing status="(\d{3})" out of the formatted error string works today, but it silently breaks the moment upstream changes their error wording — and because the only consumer is createSessionWithRetry's "retry on 409, bail otherwise" logic, a silent break means startup stops recovering from a stale session (the common restart case). errors.Is on the exported sentinel gives the same signal and fails at compile time instead. I checked the SDK and it does not retry the 409 itself, so createSessionWithRetry should stay — it's just the status extraction that can go.

context / nit (keep it, but let's track it) — pkg/github/scalesetclient/client.go, GetMessage / skippableMessage: The strings.Contains(err.Error(), "unsupported message type") match is unfortunately the only handle the SDK gives us — parseRunnerScaleSetMessageResponse returns a plain fmt.Errorf("unsupported message type: %s", ...) with no sentinel and no wrap, so there's nothing typed to match on. Worth keeping the workaround: when the SDK hits an unknown outer message type it returns without acking the message, so the queue head never advances and the listener wedges permanently on it — the re-read-and-skip path is what prevents that. (Unknown inner job-message types are already safe; the SDK's inner switch has an empty default.) Two small asks: (a) a short comment here explaining why we're string-matching, so nobody "cleans it up" later, and (b) an upstream issue on actions/scaleset for a typed error (or SDK-side skip+ack of unknown outer types) — once that lands, this whole block can go.

Good catches, but this doesn't work against the version we're pinned to. scaleset.ConflictError and wrapResponseErrorType actions/scaleset#86, merged 2026-06-10 into main. The latest published release is v0.4.0 from 2026-05-05, which predates it — that release exports only four sentinels (RunnerNotFoundError, RunnerExistsError, JobStillRunningError, MessageQueueTokenExpiredError) and carries the HTTP status only as text inside the formatted message, so there's nothing to errors.Is against for the 409 yet. This is the same for GetMessage/skippableMessage where v0.4.0 is a bare fmt.Errorf with no sentinel and no wrap.

Do you want to pin against main instead or keep as is and apply these improvements when a v0.5.0 lands upstream?

@existemi
existemi merged commit ff09c00 into main Aug 20, 2026
2 checks passed
@existemi
existemi deleted the rl/scaleset-sdk branch August 20, 2026 20:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants