detached 21a5df7

fix(host): show the server's reason when resolving actions fails

The job log said only `resolving actions failed: unavailable`; it now adds the
message of the server, which names the action github.com did not answer for.
commit
21a5df7e82b9a3a44f6b6f3db943f07ff1d72f6e
author
brice · · 2026-10-17 19:43 UTC
parent
257b319
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +21 -2
M internal/host/resolve.go +13 -2
@@ -5,6 +5,7 @@ package host
55 import (
66 "context"
77 "encoding/json"
8+ "errors"
89 "fmt"
910 "strings"
1011 "unicode/utf8"
@@ -213,15 +214,25 @@ func (j *jobRun) callResolve(ctx context.Context, uses []string) ([]*execpb.Reso
213214 j.abandon(err)
214215 return fail("the server ended the job")
215216 case kindPermanent:
216- return fail("resolving actions failed: %s", connect.CodeOf(err))
217+ return fail("resolving actions failed: %s", serverReason(err))
217218 }
218219 j.log.Warn().Err(err).Int("attempt", attempt).Msg("ResolveActions failed")
219220 if attempt >= 3 || !sleep(ctx, b.next()) {
220- return fail("resolving actions failed: %s", connect.CodeOf(err))
221+ return fail("resolving actions failed: %s", serverReason(err))
221222 }
222223 }
223224 }
224225
226+// 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.
228+func serverReason(err error) string {
229+ var ce *connect.Error
230+ if errors.As(err, &ce) && ce.Message() != "" {
231+ return fmt.Sprintf("%s: %s", ce.Code(), ce.Message())
232+ }
233+ return connect.CodeOf(err).String()
234+}
235+
225236 // resolveResponse converts the answer of the server. The token of an archive is
226237 // a secret that the sandbox sends in an Authorization header, so it only goes
227238 // with an archive of the ermite source whose tarball URL is on an origin this
@@ -5,6 +5,7 @@ package host
5 import (5 import (
6 "context"6 "context"
7 "encoding/json"7 "encoding/json"
8+ "errors"
8 "fmt"9 "fmt"
9 "strings"10 "strings"
10 "unicode/utf8"11 "unicode/utf8"
@@ -213,15 +214,25 @@ func (j *jobRun) callResolve(ctx context.Context, uses []string) ([]*execpb.Reso
213 j.abandon(err)214 j.abandon(err)
214 return fail("the server ended the job")215 return fail("the server ended the job")
215 case kindPermanent:216 case kindPermanent:
216- return fail("resolving actions failed: %s", connect.CodeOf(err))217+ return fail("resolving actions failed: %s", serverReason(err))
217 }218 }
218 j.log.Warn().Err(err).Int("attempt", attempt).Msg("ResolveActions failed")219 j.log.Warn().Err(err).Int("attempt", attempt).Msg("ResolveActions failed")
219 if attempt >= 3 || !sleep(ctx, b.next()) {220 if attempt >= 3 || !sleep(ctx, b.next()) {
220- return fail("resolving actions failed: %s", connect.CodeOf(err))221+ return fail("resolving actions failed: %s", serverReason(err))
221 }222 }
222 }223 }
223 }224 }
224 225
226+// 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.
228+func serverReason(err error) string {
229+ var ce *connect.Error
230+ if errors.As(err, &ce) && ce.Message() != "" {
231+ return fmt.Sprintf("%s: %s", ce.Code(), ce.Message())
232+ }
233+ return connect.CodeOf(err).String()
234+}
235+
225 // resolveResponse converts the answer of the server. The token of an archive is236 // resolveResponse converts the answer of the server. The token of an archive is
226 // a secret that the sandbox sends in an Authorization header, so it only goes237 // a secret that the sandbox sends in an Authorization header, so it only goes
227 // with an archive of the ermite source whose tarball URL is on an origin this238 // with an archive of the ermite source whose tarball URL is on an origin this
M internal/host/resolve_test.go +8 -0
@@ -75,6 +75,14 @@ func TestResolveActionsFailures(t *testing.T) {
7575 if r := resolveJob(c).resolveActions(context.Background(), &execpb.ResolveRequest{Uses: []string{"a/b@v1"}}); !strings.Contains(r.Error, "invalid_argument") {
7676 t.Errorf("%+v", r)
7777 }
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+ }
7886 c = &resolveStub{resp: &ermite.ResolveActionsResponse{}}
7987 if r := resolveJob(c).resolveActions(context.Background(), &execpb.ResolveRequest{Uses: []string{"a/b@v1"}}); r.Error == "" {
8088 t.Errorf("a short answer must fail: %+v", r)
@@ -75,6 +75,14 @@ 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") {75 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)76 t.Errorf("%+v", r)
77 }77 }
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+ }
78 c = &resolveStub{resp: &ermite.ResolveActionsResponse{}}86 c = &resolveStub{resp: &ermite.ResolveActionsResponse{}}
79 if r := resolveJob(c).resolveActions(context.Background(), &execpb.ResolveRequest{Uses: []string{"a/b@v1"}}); r.Error == "" {87 if r := resolveJob(c).resolveActions(context.Background(), &execpb.ResolveRequest{Uses: []string{"a/b@v1"}}); r.Error == "" {
80 t.Errorf("a short answer must fail: %+v", r)88 t.Errorf("a short answer must fail: %+v", r)

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