Skip to content

feat: add shared platform buildkite/githubactions BuildRunner clients#423

Open
roychying wants to merge 18 commits into
mainfrom
chenghan.ying/refactor-buildrunner-extension
Open

feat: add shared platform buildkite/githubactions BuildRunner clients#423
roychying wants to merge 18 commits into
mainfrom
chenghan.ying/refactor-buildrunner-extension

Conversation

@roychying

Copy link
Copy Markdown
Contributor

Summary

  • Extract shared Buildkite and GitHub Actions clients into platform/extension/buildrunner/ and refactor the existing submitqueue adapters to use them.
  • Add stovepipe-side Buildkite and GitHub Actions BuildRunner adapters on top of the shared clients.

Test plan

  • go build ./...
  • go test ./platform/extension/buildrunner/... ./submitqueue/extension/buildrunner/... ./stovepipe/extension/buildrunner/...
  • make gazelle (no drift)

@roychying
roychying requested review from a team, behinddwalls and sbalabanov as code owners July 22, 2026 01:42
// Already terminal — no-op per BuildRunner.Cancel contract.
return nil
default:
return fmt.Errorf("unexpected status %d from cancel", resp.StatusCode)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would be nice to also include the message into the error, typically in a body of a response; can be truncated if needed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done, already included the body in the cancel error

}
defer resp.Body.Close()

respBody, err := io.ReadAll(resp.Body)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this may not properly respond to context cancellation. AI analysis:

Behavior: Vulnerable to blocking/hanging.

Why: io.ReadAll reads from resp.Body until it hits io.EOF. However, once httpClient.Do() successfully receives response headers, the HTTP transport hands the raw stream body to you. Standard response bodies (net/http.bodyEOFSignal or TCP socket readers) do not automatically monitor the request ctx for subsequent reads.

If the server stalls, sends data infinitely slowly, or hangs while streaming the payload after headers have been sent, calling io.ReadAll(resp.Body) will block the goroutine until the connection times out at the network level—even if your ctx was canceled long ago.

The poor man's alternative is buffered read with cancellation checks:
https://github.com/uber/tango/blob/1ed2b86888fc4afc59494d9d97fdd6dc23c2638e/core/storage/ctxreader.go#L25
At least it cancels somewhere in between for long transfers.

@roychying roychying Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good call. dug a bit on this one. Per the stdlib docs, Request's context "controls the entire lifetime of a request and its response: obtaining a connection, sending the request, and reading the response headers and body" , and Client.Timeout (implemented as context cancellation internally) is documented to "interrupt reading of the Response.Body" . So I believe a canceled ctx does unblock an in-progress io.ReadAll(resp.Body) here, not wait for a network-level timeout.

one thing is that nothing sets a deadline on ctx or a Timeout on the http.Client for this path. platform/http.NewClient's doc already calls that out as the caller's job, but no actual service wiring exists yet to enforce it (Buildkite isn't wired into any main.go today).

I prefer to leave that to whichever change wires up a real client, but happy to follow tango's implementation since it's a cheap safe guard. lmk if anything is wrong on my understanding

}

if resp.StatusCode == http.StatusNotFound {
return fmt.Errorf("build not found")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

http helper function do() is not necessarily about build. You may want to define sentinel errors to transform http codes into.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, do() now returns ErrNotFound instead of a hardcoded string.

if !ok || raw == "" {
return meta
}
_ = json.Unmarshal([]byte(raw), &meta)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's not swallow errors without proper visibility
because this is an exported function, probably better design is to expose an error and let caller handle it as they see fit

}

func buildJSONWithEnv(number int, state, webURL string, env map[string]string) []byte {
b, _ := json.Marshal(BuildResponse{Number: number, State: state, WebURL: webURL, Env: env})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

func buildJSONWithEnv(t *testing.T, number int, state, webURL string, env map[string]string) []byte {
  t.Helper()
  b, err := json.Marshal(BuildResponse{Number: number, State: state, WebURL: webURL, Env: env})
  require.NoError(t, err)
  return b
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

capturedMethod = req.Method
capturedBody, _ = io.ReadAll(req.Body)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(buildJSON(42, "scheduled", "https://buildkite.com/test-org/my-pipeline/builds/42"))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

require.NoError()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

)
}

func (c *Client) do(ctx context.Context, method, rawURL string, body []byte, out any) error {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

duplicated code with buildkite impl, can deduce?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure, now both use extracted platform/http.SendRequest

}
if params.Logger == nil {
return nil, fmt.Errorf("logger is required")
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Submitqueue design assumes all wiring to be non-nil, so there is no need to explicitly check it. Panic will do just fine.

@roychying roychying Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, removed those unnecessary check

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also applied the same fix to submitqueue's buildkite and githubaction's NewBuildRunners, which had identical checks.

EnvKeyQueue: r.cfg.QueueName,
}
if len(metadata) > 0 {
metaJSON, _ := json.Marshal(metadata)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

plz do not swallow errors

@roychying roychying Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the metadata marshal error is now propagated. Also fixed the same swallowed-marshal-error pattern in submitqueue's buildkite Trigger


_, err := r.Trigger(context.Background(), "", "github://repo/head/aaa", nil)
require.Error(t, err)
assert.Contains(t, err.Error(), "response missing workflow_run_id")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not assume on error messages - messages are not part of application logic and it makes test unnecessary fragile. AGENTS.md has an instruction to avoid it, surprising it was not picked up.

@roychying roychying Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dropped the message assertion, kept require.Error.

@roychying
roychying requested a review from sbalabanov July 23, 2026 01:38
…ad of hardcoded not-found strings, matching buildkite.
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