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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +21 -2
M internal/host/resolve.go +13 -2
| @@ -5,6 +5,7 @@ package host | |||
| 5 | 5 | import ( | |
| 6 | 6 | "context" | |
| 7 | 7 | "encoding/json" | |
| 8 | + | "errors" | |
| 8 | 9 | "fmt" | |
| 9 | 10 | "strings" | |
| 10 | 11 | "unicode/utf8" | |
| @@ -213,15 +214,25 @@ func (j *jobRun) callResolve(ctx context.Context, uses []string) ([]*execpb.Reso | |||
| 213 | 214 | j.abandon(err) | |
| 214 | 215 | return fail("the server ended the job") | |
| 215 | 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 | 219 | j.log.Warn().Err(err).Int("attempt", attempt).Msg("ResolveActions failed") | |
| 219 | 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 | 236 | // resolveResponse converts the answer of the server. The token of an archive is | |
| 226 | 237 | // a secret that the sandbox sends in an Authorization header, so it only goes | |
| 227 | 238 | // 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 is | 236 | // 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 goes | 237 | // 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 this | 238 | // 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) { | |||
| 75 | 75 | if r := resolveJob(c).resolveActions(context.Background(), &execpb.ResolveRequest{Uses: []string{"a/b@v1"}}); !strings.Contains(r.Error, "invalid_argument") { | |
| 76 | 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 | 86 | c = &resolveStub{resp: &ermite.ResolveActionsResponse{}} | |
| 79 | 87 | if r := resolveJob(c).resolveActions(context.Background(), &execpb.ResolveRequest{Uses: []string{"a/b@v1"}}); r.Error == "" { | |
| 80 | 88 | 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) | ||