detached 4421b49

fix(host): keep the runner's connection failures out of the job log

commit
4421b4925ad0694ac3b409682a080fcec8a5c034
author
brice · · 2026-10-17 19:45 UTC
parent
21a5df7
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
refs
fix/resolve-actions-message
2 files · +40 -9
M internal/host/resolve.go +4 -1
@@ -225,9 +225,12 @@ func (j *jobRun) callResolve(ctx context.Context, uses []string) ([]*execpb.Reso
225225
226226 // serverReason is the code of a failed call, followed by the message of the
227227 // server when it gave one, so the job log names what could not be resolved.
228+// Only a message the server sent goes in the job log, which the readers of the
229+// repository see: a failure of the runner's own connection (DNS, TLS, an
230+// address) stays in the runner's log.
228231 func serverReason(err error) string {
229232 var ce *connect.Error
230- if errors.As(err, &ce) && ce.Message() != "" {
233+ if errors.As(err, &ce) && connect.IsWireError(ce) && ce.Message() != "" {
231234 return fmt.Sprintf("%s: %s", ce.Code(), ce.Message())
232235 }
233236 return connect.CodeOf(err).String()
@@ -225,9 +225,12 @@ func (j *jobRun) callResolve(ctx context.Context, uses []string) ([]*execpb.Reso
225 225
226 // serverReason is the code of a failed call, followed by the message of the226 // serverReason is the code of a failed call, followed by the message of the
227 // server when it gave one, so the job log names what could not be resolved.227 // server when it gave one, so the job log names what could not be resolved.
228+// Only a message the server sent goes in the job log, which the readers of the
229+// repository see: a failure of the runner's own connection (DNS, TLS, an
230+// address) stays in the runner's log.
228 func serverReason(err error) string {231 func serverReason(err error) string {
229 var ce *connect.Error232 var ce *connect.Error
230- if errors.As(err, &ce) && ce.Message() != "" {233+ if errors.As(err, &ce) && connect.IsWireError(ce) && ce.Message() != "" {
231 return fmt.Sprintf("%s: %s", ce.Code(), ce.Message())234 return fmt.Sprintf("%s: %s", ce.Code(), ce.Message())
232 }235 }
233 return connect.CodeOf(err).String()236 return connect.CodeOf(err).String()
M internal/host/resolve_test.go +36 -8
@@ -5,7 +5,10 @@ package host
55 import (
66 "context"
77 "encoding/json"
8+ "errors"
89 "fmt"
10+ "net/http"
11+ "net/http/httptest"
912 "strings"
1013 "testing"
1114 "time"
@@ -14,6 +17,7 @@ import (
1417 "github.com/bnema/zerowrap"
1518
1619 ermite "bnema.dev/ermite-api/gen/ermite"
20+ "bnema.dev/ermite-api/gen/ermite/ermiteconnect"
1721
1822 "bnema.dev/ermite-runner/internal/execstream/execpb"
1923 )
@@ -75,14 +79,6 @@ func TestResolveActionsFailures(t *testing.T) {
7579 if r := resolveJob(c).resolveActions(context.Background(), &execpb.ResolveRequest{Uses: []string{"a/b@v1"}}); !strings.Contains(r.Error, "invalid_argument") {
7680 t.Errorf("%+v", r)
7781 }
78- c = &resolveStub{err: connect.NewError(connect.CodeUnavailable, fmt.Errorf("resolving `actions/checkout@v4`: github.com did not answer"))}
79- r := resolveJob(c).resolveActions(context.Background(), &execpb.ResolveRequest{Uses: []string{"actions/checkout@v4"}})
80- if want := "resolving actions failed: unavailable: resolving `actions/checkout@v4`: github.com did not answer"; r.Error != want {
81- t.Errorf("the job log must name the action: %q", r.Error)
82- }
83- if c.calls != 3 {
84- t.Errorf("an unavailable server is asked 3 times, got %d", c.calls)
85- }
8682 c = &resolveStub{resp: &ermite.ResolveActionsResponse{}}
8783 if r := resolveJob(c).resolveActions(context.Background(), &execpb.ResolveRequest{Uses: []string{"a/b@v1"}}); r.Error == "" {
8884 t.Errorf("a short answer must fail: %+v", r)
@@ -311,3 +307,35 @@ func TestResolveResponseTokenOriginIPv6(t *testing.T) {
311307 }
312308 }
313309 }
310+
311+type unavailableServer struct {
312+ ermiteconnect.UnimplementedRunnerServiceHandler
313+}
314+
315+func (unavailableServer) ResolveActions(context.Context, *connect.Request[ermite.ResolveActionsRequest]) (*connect.Response[ermite.ResolveActionsResponse], error) {
316+ return nil, connect.NewError(connect.CodeUnavailable, errors.New("resolving `actions/checkout@v4`: github.com did not answer"))
317+}
318+
319+// The job log gives the reason the server sent, not the runner's own
320+// connection failures.
321+func TestResolveActionsServerReason(t *testing.T) {
322+ path, h := ermiteconnect.NewRunnerServiceHandler(unavailableServer{})
323+ mux := http.NewServeMux()
324+ mux.Handle(path, h)
325+ srv := httptest.NewServer(mux)
326+ t.Cleanup(srv.Close)
327+ uses := &execpb.ResolveRequest{Uses: []string{"actions/checkout@v4"}}
328+
329+ r := resolveJob(ermiteconnect.NewRunnerServiceClient(srv.Client(), srv.URL)).resolveActions(context.Background(), uses)
330+ if want := "resolving actions failed: unavailable: resolving `actions/checkout@v4`: github.com did not answer"; r.Error != want {
331+ t.Errorf("the job log must name the action: %q", r.Error)
332+ }
333+
334+ closed := httptest.NewServer(mux)
335+ url := closed.URL
336+ closed.Close()
337+ r = resolveJob(ermiteconnect.NewRunnerServiceClient(http.DefaultClient, url)).resolveActions(context.Background(), uses)
338+ if r.Error != "resolving actions failed: unavailable" {
339+ t.Errorf("a connection failure of the runner stays out of the job log: %q", r.Error)
340+ }
341+}
@@ -5,7 +5,10 @@ package host
5 import (5 import (
6 "context"6 "context"
7 "encoding/json"7 "encoding/json"
8+ "errors"
8 "fmt"9 "fmt"
10+ "net/http"
11+ "net/http/httptest"
9 "strings"12 "strings"
10 "testing"13 "testing"
11 "time"14 "time"
@@ -14,6 +17,7 @@ import (
14 "github.com/bnema/zerowrap"17 "github.com/bnema/zerowrap"
15 18
16 ermite "bnema.dev/ermite-api/gen/ermite"19 ermite "bnema.dev/ermite-api/gen/ermite"
20+ "bnema.dev/ermite-api/gen/ermite/ermiteconnect"
17 21
18 "bnema.dev/ermite-runner/internal/execstream/execpb"22 "bnema.dev/ermite-runner/internal/execstream/execpb"
19 )23 )
@@ -75,14 +79,6 @@ func TestResolveActionsFailures(t *testing.T) {
75 if r := resolveJob(c).resolveActions(context.Background(), &execpb.ResolveRequest{Uses: []string{"a/b@v1"}}); !strings.Contains(r.Error, "invalid_argument") {79 if r := resolveJob(c).resolveActions(context.Background(), &execpb.ResolveRequest{Uses: []string{"a/b@v1"}}); !strings.Contains(r.Error, "invalid_argument") {
76 t.Errorf("%+v", r)80 t.Errorf("%+v", r)
77 }81 }
78- c = &resolveStub{err: connect.NewError(connect.CodeUnavailable, fmt.Errorf("resolving `actions/checkout@v4`: github.com did not answer"))}
79- r := resolveJob(c).resolveActions(context.Background(), &execpb.ResolveRequest{Uses: []string{"actions/checkout@v4"}})
80- if want := "resolving actions failed: unavailable: resolving `actions/checkout@v4`: github.com did not answer"; r.Error != want {
81- t.Errorf("the job log must name the action: %q", r.Error)
82- }
83- if c.calls != 3 {
84- t.Errorf("an unavailable server is asked 3 times, got %d", c.calls)
85- }
86 c = &resolveStub{resp: &ermite.ResolveActionsResponse{}}82 c = &resolveStub{resp: &ermite.ResolveActionsResponse{}}
87 if r := resolveJob(c).resolveActions(context.Background(), &execpb.ResolveRequest{Uses: []string{"a/b@v1"}}); r.Error == "" {83 if r := resolveJob(c).resolveActions(context.Background(), &execpb.ResolveRequest{Uses: []string{"a/b@v1"}}); r.Error == "" {
88 t.Errorf("a short answer must fail: %+v", r)84 t.Errorf("a short answer must fail: %+v", r)
@@ -311,3 +307,35 @@ func TestResolveResponseTokenOriginIPv6(t *testing.T) {
311 }307 }
312 }308 }
313 }309 }
310+
311+type unavailableServer struct {
312+ ermiteconnect.UnimplementedRunnerServiceHandler
313+}
314+
315+func (unavailableServer) ResolveActions(context.Context, *connect.Request[ermite.ResolveActionsRequest]) (*connect.Response[ermite.ResolveActionsResponse], error) {
316+ return nil, connect.NewError(connect.CodeUnavailable, errors.New("resolving `actions/checkout@v4`: github.com did not answer"))
317+}
318+
319+// The job log gives the reason the server sent, not the runner's own
320+// connection failures.
321+func TestResolveActionsServerReason(t *testing.T) {
322+ path, h := ermiteconnect.NewRunnerServiceHandler(unavailableServer{})
323+ mux := http.NewServeMux()
324+ mux.Handle(path, h)
325+ srv := httptest.NewServer(mux)
326+ t.Cleanup(srv.Close)
327+ uses := &execpb.ResolveRequest{Uses: []string{"actions/checkout@v4"}}
328+
329+ r := resolveJob(ermiteconnect.NewRunnerServiceClient(srv.Client(), srv.URL)).resolveActions(context.Background(), uses)
330+ if want := "resolving actions failed: unavailable: resolving `actions/checkout@v4`: github.com did not answer"; r.Error != want {
331+ t.Errorf("the job log must name the action: %q", r.Error)
332+ }
333+
334+ closed := httptest.NewServer(mux)
335+ url := closed.URL
336+ closed.Close()
337+ r = resolveJob(ermiteconnect.NewRunnerServiceClient(http.DefaultClient, url)).resolveActions(context.Background(), uses)
338+ if r.Error != "resolving actions failed: unavailable" {
339+ t.Errorf("a connection failure of the runner stays out of the job log: %q", r.Error)
340+ }
341+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 4

Tags 11

↑↓ moveenter openesc close