Skip to content

Commit 19f445a

Browse files
authored
Fix ephemeral runner job exit and cleanup (#37)
* fix: run ephemeral github runners once * fix: drain accepted runner delete responses
1 parent b8fedce commit 19f445a

4 files changed

Lines changed: 105 additions & 5 deletions

File tree

cmd/github-actions-runner-job/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,7 @@ func (d *runnerDriver) configureRunner(ctx context.Context, spec internal.Epheme
313313
}
314314

315315
func (d *runnerDriver) runRunner(ctx context.Context) error {
316-
return runCommand(ctx, filepath.Join(d.runnerDir, "run.sh"), d.runnerDir)
316+
return runCommand(ctx, filepath.Join(d.runnerDir, "run.sh"), d.runnerDir, "--once")
317317
}
318318

319319
func (d *runnerDriver) runnerID() int64 {

cmd/github-actions-runner-job/main_test.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ func TestT915CommandRunsDynamicProviderEnvelopeThroughSidecarAndRunner(t *testin
6464

6565
runnerDir := t.TempDir()
6666
writeExecutable(t, filepath.Join(runnerDir, "config.sh"), "#!/bin/sh\nprintf '%s\\n' \"$@\" > \"$GITHUB_ACTIONS_RUNNER_JOB_TEST_DIR/config.args\"\nprintf '{\"agentId\":42}\\n' > .runner\n")
67-
writeExecutable(t, filepath.Join(runnerDir, "run.sh"), "#!/bin/sh\nprintf 'runner executed\\n' > \"$GITHUB_ACTIONS_RUNNER_JOB_TEST_DIR/run.log\"\n")
67+
writeExecutable(t, filepath.Join(runnerDir, "run.sh"), "#!/bin/sh\nprintf '%s\\n' \"$@\" > \"$GITHUB_ACTIONS_RUNNER_JOB_TEST_DIR/run.args\"\nprintf 'runner executed\\n' > \"$GITHUB_ACTIONS_RUNNER_JOB_TEST_DIR/run.log\"\n")
6868
workspace := t.TempDir()
6969
t.Chdir(workspace)
7070
t.Setenv("COMPUTE_GITHUB_RUNNER_PROVIDER_URL", sidecar.URL)
@@ -115,6 +115,9 @@ func TestT915CommandRunsDynamicProviderEnvelopeThroughSidecarAndRunner(t *testin
115115
if got := readFile(t, filepath.Join(workspace, "run.log")); !strings.Contains(got, "runner executed") {
116116
t.Fatalf("run script did not execute: %q", got)
117117
}
118+
if got := readFile(t, filepath.Join(workspace, "run.args")); strings.TrimSpace(got) != "--once" {
119+
t.Fatalf("run args: got %q want --once", got)
120+
}
118121
var result struct {
119122
Artifacts []string `json:"artifacts"`
120123
}

internal/module_runner_provider.go

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ func (c *httpGitHubRunnerClient) RemoveRunner(ctx context.Context, owner, repo s
9090
return errors.New("runner_id must be positive")
9191
}
9292
endpoint := fmt.Sprintf("%s/repos/%s/%s/actions/runners/%d", c.baseURL, url.PathEscape(owner), url.PathEscape(repo), runnerID)
93-
return c.do(ctx, http.MethodDelete, endpoint, nil, token, http.StatusNoContent, nil)
93+
return c.doRunnerDelete(ctx, endpoint, token)
9494
}
9595

9696
func (c *httpGitHubRunnerClient) OrgRegistrationToken(ctx context.Context, organization, token string) (GitHubRunnerRegistrationToken, error) {
@@ -110,7 +110,7 @@ func (c *httpGitHubRunnerClient) RemoveOrgRunner(ctx context.Context, organizati
110110
return errors.New("runner_id must be positive")
111111
}
112112
endpoint := fmt.Sprintf("%s/orgs/%s/actions/runners/%d", c.baseURL, url.PathEscape(organization), runnerID)
113-
return c.do(ctx, http.MethodDelete, endpoint, nil, token, http.StatusNoContent, nil)
113+
return c.doRunnerDelete(ctx, endpoint, token)
114114
}
115115

116116
func (c *httpGitHubRunnerClient) PreflightOrg(ctx context.Context, req GitHubRunnerProviderPreflightRequest, token string) (GitHubRunnerProviderPreflight, error) {
@@ -190,6 +190,15 @@ func (c *httpGitHubRunnerClient) do(ctx context.Context, method, endpoint string
190190
}
191191

192192
func (c *httpGitHubRunnerClient) doRaw(ctx context.Context, method, endpoint string, body any, token string, wantStatus int, out any) (http.Header, error) {
193+
return c.doRawAllowed(ctx, method, endpoint, body, token, []int{wantStatus}, out)
194+
}
195+
196+
func (c *httpGitHubRunnerClient) doRunnerDelete(ctx context.Context, endpoint, token string) error {
197+
_, err := c.doRawAllowed(ctx, http.MethodDelete, endpoint, nil, token, []int{http.StatusNoContent, http.StatusNotFound}, nil)
198+
return err
199+
}
200+
201+
func (c *httpGitHubRunnerClient) doRawAllowed(ctx context.Context, method, endpoint string, body any, token string, wantStatuses []int, out any) (http.Header, error) {
193202
if strings.TrimSpace(token) == "" {
194203
return nil, errors.New("github token is required")
195204
}
@@ -221,11 +230,19 @@ func (c *httpGitHubRunnerClient) doRaw(ctx context.Context, method, endpoint str
221230
return nil, fmt.Errorf("github runner request failed: %w", err)
222231
}
223232
defer resp.Body.Close()
224-
if resp.StatusCode != wantStatus {
233+
ok := false
234+
for _, status := range wantStatuses {
235+
if resp.StatusCode == status {
236+
ok = true
237+
break
238+
}
239+
}
240+
if !ok {
225241
limited, _ := io.ReadAll(io.LimitReader(resp.Body, 2048))
226242
return nil, fmt.Errorf("github runner request returned %s: %s", resp.Status, strings.TrimSpace(string(limited)))
227243
}
228244
if out == nil {
245+
_, _ = io.Copy(io.Discard, resp.Body)
229246
return resp.Header, nil
230247
}
231248
if err := json.NewDecoder(resp.Body).Decode(out); err != nil {

internal/runner_provider_test.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,61 @@ func TestT915_GitHubRunnerClientDispatchesWorkflow(t *testing.T) {
133133
}
134134
}
135135

136+
func TestT915_GitHubRunnerClientTreatsMissingRunnerAsRemoved(t *testing.T) {
137+
var paths []string
138+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
139+
if r.Method != http.MethodDelete {
140+
t.Fatalf("method: got %q want DELETE", r.Method)
141+
}
142+
paths = append(paths, r.URL.Path)
143+
http.Error(w, `{"message":"Not Found"}`, http.StatusNotFound)
144+
}))
145+
defer server.Close()
146+
147+
client := &httpGitHubRunnerClient{baseURL: server.URL, httpClient: server.Client()}
148+
if err := client.RemoveRunner(context.Background(), "GoCodeAlone", "workflow-compute", 42, "github-token"); err != nil {
149+
t.Fatalf("remove repo runner should ignore already-missing runner: %v", err)
150+
}
151+
if err := client.RemoveOrgRunner(context.Background(), "GoCodeAlone", 43, "github-token"); err != nil {
152+
t.Fatalf("remove org runner should ignore already-missing runner: %v", err)
153+
}
154+
want := []string{
155+
"/repos/GoCodeAlone/workflow-compute/actions/runners/42",
156+
"/orgs/GoCodeAlone/actions/runners/43",
157+
}
158+
if strings.Join(paths, "\n") != strings.Join(want, "\n") {
159+
t.Fatalf("delete paths:\ngot:\n%s\nwant:\n%s", strings.Join(paths, "\n"), strings.Join(want, "\n"))
160+
}
161+
}
162+
163+
func TestT915_GitHubRunnerClientDrainsAcceptedDeleteResponseBody(t *testing.T) {
164+
body := &trackingReadCloser{reader: strings.NewReader(`{"message":"Not Found"}`)}
165+
client := &httpGitHubRunnerClient{
166+
baseURL: "https://api.github.invalid",
167+
httpClient: &http.Client{Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
168+
if r.Method != http.MethodDelete {
169+
t.Fatalf("method: got %q want DELETE", r.Method)
170+
}
171+
return &http.Response{
172+
StatusCode: http.StatusNotFound,
173+
Status: "404 Not Found",
174+
Header: make(http.Header),
175+
Body: body,
176+
Request: r,
177+
}, nil
178+
})},
179+
}
180+
if err := client.RemoveOrgRunner(context.Background(), "GoCodeAlone", 43, "github-token"); err != nil {
181+
t.Fatalf("remove org runner: %v", err)
182+
}
183+
if !body.read {
184+
t.Fatal("accepted delete response body was not drained")
185+
}
186+
if !body.closed {
187+
t.Fatal("accepted delete response body was not closed")
188+
}
189+
}
190+
136191
func TestT41_GitHubRunnerProviderModuleRejectsUnknownConfig(t *testing.T) {
137192
_, err := newGitHubRunnerProviderModule("provider", map[string]any{
138193
"token": "github-token",
@@ -542,6 +597,31 @@ type fakeRunnerClient struct {
542597
dispatchedInputs map[string]string
543598
}
544599

600+
type roundTripFunc func(*http.Request) (*http.Response, error)
601+
602+
func (f roundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) {
603+
return f(r)
604+
}
605+
606+
type trackingReadCloser struct {
607+
reader *strings.Reader
608+
read bool
609+
closed bool
610+
}
611+
612+
func (b *trackingReadCloser) Read(p []byte) (int, error) {
613+
n, err := b.reader.Read(p)
614+
if n > 0 || err == io.EOF {
615+
b.read = true
616+
}
617+
return n, err
618+
}
619+
620+
func (b *trackingReadCloser) Close() error {
621+
b.closed = true
622+
return nil
623+
}
624+
545625
func (f *fakeRunnerClient) RegistrationToken(_ context.Context, owner, repo, _ string) (GitHubRunnerRegistrationToken, error) {
546626
f.registrationRepository = owner + "/" + repo
547627
return f.token, nil

0 commit comments

Comments
 (0)