detached 1541ed7

feat(ci): add actions, annotations and summaries to the CI protocol

Job.github_token, RunnerService.ResolveActions and UploadSummary, StepUpdate.outcome, Annotation on UpdateJob and CompleteJob, RunService.ListAnnotations and GetStepSummaries, Step.outcome, and the ci.actions, ci.annotations and ci.summaries features.
commit
1541ed78879d9a6ad1497253f775c91d0d81bfb6
author
brice · · 2026-10-13 09:12 UTC
parent
4db3b8b
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
10 files · +2054 -252
M README.md +2 -2
@@ -22,8 +22,8 @@ Handlers are mounted under `/-/api/` by the server. Errors are Connect codes.
2222 | `MergeRequestService` | `CreateMergeRequest`, `ListMergeRequests`, `GetMergeRequest` (commits, diff, comments), `AddComment`, `ResolveComment`, `CloseMergeRequest`, `MergeMergeRequest`, `MarkRead` |
2323 | `SessionService` | `CreateLoginLink`, `SignOutEverywhere` (owner only) |
2424 | `EventService` | `Watch` (streamed, resumable) |
25-| `RunnerService` | `RegisterRunner`, `FetchJob` (long poll), `UpdateJob`, `AppendLog` (offset-idempotent), `Heartbeat`, `WatchJob` (cancel requests, streamed), `CompleteJob` (idempotent); for CI runners |
26-| `RunService` | `ListRuns`, `GetRun`, `GetJob`, `GetJobLog` (streamed, follow), `CancelRun`, `RerunRun`, `ListChecks`, `ListRunners`, `RemoveRunner` |
25+| `RunnerService` | `RegisterRunner`, `FetchJob` (long poll), `UpdateJob` (steps, annotations), `AppendLog` (offset-idempotent), `Heartbeat`, `WatchJob` (cancel requests, streamed), `CompleteJob` (idempotent), `ResolveActions` (where to download the `uses:` actions), `UploadSummary` (job summary of a step); for CI runners |
26+| `RunService` | `ListRuns`, `GetRun`, `GetJob`, `GetJobLog` (streamed, follow), `ListAnnotations`, `GetStepSummaries`, `CancelRun`, `RerunRun`, `ListChecks`, `ListRunners`, `RemoveRunner` |
2727 | `ServerService` | `GetVersion` (server version and feature list), `GetSigningKey` (commit signing key) |
2828
2929 Tokens carry scopes only; presets such as `cli` or `agent` are a client concern.
@@ -22,8 +22,8 @@ Handlers are mounted under `/-/api/` by the server. Errors are Connect codes.
22 | `MergeRequestService` | `CreateMergeRequest`, `ListMergeRequests`, `GetMergeRequest` (commits, diff, comments), `AddComment`, `ResolveComment`, `CloseMergeRequest`, `MergeMergeRequest`, `MarkRead` |22 | `MergeRequestService` | `CreateMergeRequest`, `ListMergeRequests`, `GetMergeRequest` (commits, diff, comments), `AddComment`, `ResolveComment`, `CloseMergeRequest`, `MergeMergeRequest`, `MarkRead` |
23 | `SessionService` | `CreateLoginLink`, `SignOutEverywhere` (owner only) |23 | `SessionService` | `CreateLoginLink`, `SignOutEverywhere` (owner only) |
24 | `EventService` | `Watch` (streamed, resumable) |24 | `EventService` | `Watch` (streamed, resumable) |
25-| `RunnerService` | `RegisterRunner`, `FetchJob` (long poll), `UpdateJob`, `AppendLog` (offset-idempotent), `Heartbeat`, `WatchJob` (cancel requests, streamed), `CompleteJob` (idempotent); for CI runners |25+| `RunnerService` | `RegisterRunner`, `FetchJob` (long poll), `UpdateJob` (steps, annotations), `AppendLog` (offset-idempotent), `Heartbeat`, `WatchJob` (cancel requests, streamed), `CompleteJob` (idempotent), `ResolveActions` (where to download the `uses:` actions), `UploadSummary` (job summary of a step); for CI runners |
26-| `RunService` | `ListRuns`, `GetRun`, `GetJob`, `GetJobLog` (streamed, follow), `CancelRun`, `RerunRun`, `ListChecks`, `ListRunners`, `RemoveRunner` |26+| `RunService` | `ListRuns`, `GetRun`, `GetJob`, `GetJobLog` (streamed, follow), `ListAnnotations`, `GetStepSummaries`, `CancelRun`, `RerunRun`, `ListChecks`, `ListRunners`, `RemoveRunner` |
27 | `ServerService` | `GetVersion` (server version and feature list), `GetSigningKey` (commit signing key) |27 | `ServerService` | `GetVersion` (server version and feature list), `GetSigningKey` (commit signing key) |
28 28
29 Tokens carry scopes only; presets such as `cli` or `agent` are a client concern.29 Tokens carry scopes only; presets such as `cli` or `agent` are a client concern.
M gen/ermite/ermiteconnect/run.connect.go +93 -9
@@ -41,6 +41,12 @@ const (
4141 RunServiceGetRunProcedure = "/ermite.RunService/GetRun"
4242 // RunServiceGetJobProcedure is the fully-qualified name of the RunService's GetJob RPC.
4343 RunServiceGetJobProcedure = "/ermite.RunService/GetJob"
44+ // RunServiceListAnnotationsProcedure is the fully-qualified name of the RunService's
45+ // ListAnnotations RPC.
46+ RunServiceListAnnotationsProcedure = "/ermite.RunService/ListAnnotations"
47+ // RunServiceGetStepSummariesProcedure is the fully-qualified name of the RunService's
48+ // GetStepSummaries RPC.
49+ RunServiceGetStepSummariesProcedure = "/ermite.RunService/GetStepSummaries"
4450 // RunServiceGetJobLogProcedure is the fully-qualified name of the RunService's GetJobLog RPC.
4551 RunServiceGetJobLogProcedure = "/ermite.RunService/GetJobLog"
4652 // RunServiceCancelRunProcedure is the fully-qualified name of the RunService's CancelRun RPC.
@@ -70,6 +76,21 @@ type RunServiceClient interface {
7076 // GetJob returns one job with its steps. Fails with not_found if the job
7177 // or the attempt does not exist.
7278 GetJob(context.Context, *connect.Request[ermite.GetJobRequest]) (*connect.Response[ermite.GetJobResponse], error)
79+ // ListAnnotations lists the annotations of a run attempt, optionally of one
80+ // job, ordered by job (as in GetRun), step number, then the order in which
81+ // the step produced them. Needs the same access as GetRun. Returns at most
82+ // 1,000 annotations (truncated tells). A run without annotations gives an
83+ // empty list. Only servers with the feature "ci.annotations" implement it
84+ // (unimplemented otherwise). Fails with not_found if the run, the attempt or
85+ // the job does not exist, or if the job is not a job of the run.
86+ ListAnnotations(context.Context, *connect.Request[ermite.ListAnnotationsRequest]) (*connect.Response[ermite.ListAnnotationsResponse], error)
87+ // GetStepSummaries returns the job summaries (the Markdown written to
88+ // GITHUB_STEP_SUMMARY) of the steps of a job that have one, ordered by step
89+ // number. Needs the same access as GetJob. Only servers with the feature
90+ // "ci.summaries" implement it (unimplemented otherwise). A job without
91+ // summaries gives an empty list. Fails with not_found if the job or the
92+ // attempt does not exist.
93+ GetStepSummaries(context.Context, *connect.Request[ermite.GetStepSummariesRequest]) (*connect.Response[ermite.GetStepSummariesResponse], error)
7394 // GetJobLog streams the lines of the log of a job, from a line number. The
7495 // server sends them in order in messages of at most 1,000 lines and 64 KiB of
7596 // text each.
@@ -148,6 +169,18 @@ func NewRunServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...
148169 connect.WithSchema(runServiceMethods.ByName("GetJob")),
149170 connect.WithClientOptions(opts...),
150171 ),
172+ listAnnotations: connect.NewClient[ermite.ListAnnotationsRequest, ermite.ListAnnotationsResponse](
173+ httpClient,
174+ baseURL+RunServiceListAnnotationsProcedure,
175+ connect.WithSchema(runServiceMethods.ByName("ListAnnotations")),
176+ connect.WithClientOptions(opts...),
177+ ),
178+ getStepSummaries: connect.NewClient[ermite.GetStepSummariesRequest, ermite.GetStepSummariesResponse](
179+ httpClient,
180+ baseURL+RunServiceGetStepSummariesProcedure,
181+ connect.WithSchema(runServiceMethods.ByName("GetStepSummaries")),
182+ connect.WithClientOptions(opts...),
183+ ),
151184 getJobLog: connect.NewClient[ermite.GetJobLogRequest, ermite.GetJobLogResponse](
152185 httpClient,
153186 baseURL+RunServiceGetJobLogProcedure,
@@ -189,15 +222,17 @@ func NewRunServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...
189222
190223 // runServiceClient implements RunServiceClient.
191224 type runServiceClient struct {
192- listRuns *connect.Client[ermite.ListRunsRequest, ermite.ListRunsResponse]
193- getRun *connect.Client[ermite.GetRunRequest, ermite.GetRunResponse]
194- getJob *connect.Client[ermite.GetJobRequest, ermite.GetJobResponse]
195- getJobLog *connect.Client[ermite.GetJobLogRequest, ermite.GetJobLogResponse]
196- cancelRun *connect.Client[ermite.CancelRunRequest, ermite.CancelRunResponse]
197- rerunRun *connect.Client[ermite.RerunRunRequest, ermite.RerunRunResponse]
198- listChecks *connect.Client[ermite.ListChecksRequest, ermite.ListChecksResponse]
199- listRunners *connect.Client[ermite.ListRunnersRequest, ermite.ListRunnersResponse]
200- removeRunner *connect.Client[ermite.RemoveRunnerRequest, ermite.RemoveRunnerResponse]
225+ listRuns *connect.Client[ermite.ListRunsRequest, ermite.ListRunsResponse]
226+ getRun *connect.Client[ermite.GetRunRequest, ermite.GetRunResponse]
227+ getJob *connect.Client[ermite.GetJobRequest, ermite.GetJobResponse]
228+ listAnnotations *connect.Client[ermite.ListAnnotationsRequest, ermite.ListAnnotationsResponse]
229+ getStepSummaries *connect.Client[ermite.GetStepSummariesRequest, ermite.GetStepSummariesResponse]
230+ getJobLog *connect.Client[ermite.GetJobLogRequest, ermite.GetJobLogResponse]
231+ cancelRun *connect.Client[ermite.CancelRunRequest, ermite.CancelRunResponse]
232+ rerunRun *connect.Client[ermite.RerunRunRequest, ermite.RerunRunResponse]
233+ listChecks *connect.Client[ermite.ListChecksRequest, ermite.ListChecksResponse]
234+ listRunners *connect.Client[ermite.ListRunnersRequest, ermite.ListRunnersResponse]
235+ removeRunner *connect.Client[ermite.RemoveRunnerRequest, ermite.RemoveRunnerResponse]
201236 }
202237
203238 // ListRuns calls ermite.RunService.ListRuns.
@@ -215,6 +250,16 @@ func (c *runServiceClient) GetJob(ctx context.Context, req *connect.Request[ermi
215250 return c.getJob.CallUnary(ctx, req)
216251 }
217252
253+// ListAnnotations calls ermite.RunService.ListAnnotations.
254+func (c *runServiceClient) ListAnnotations(ctx context.Context, req *connect.Request[ermite.ListAnnotationsRequest]) (*connect.Response[ermite.ListAnnotationsResponse], error) {
255+ return c.listAnnotations.CallUnary(ctx, req)
256+}
257+
258+// GetStepSummaries calls ermite.RunService.GetStepSummaries.
259+func (c *runServiceClient) GetStepSummaries(ctx context.Context, req *connect.Request[ermite.GetStepSummariesRequest]) (*connect.Response[ermite.GetStepSummariesResponse], error) {
260+ return c.getStepSummaries.CallUnary(ctx, req)
261+}
262+
218263 // GetJobLog calls ermite.RunService.GetJobLog.
219264 func (c *runServiceClient) GetJobLog(ctx context.Context, req *connect.Request[ermite.GetJobLogRequest]) (*connect.ServerStreamForClient[ermite.GetJobLogResponse], error) {
220265 return c.getJobLog.CallServerStream(ctx, req)
@@ -260,6 +305,21 @@ type RunServiceHandler interface {
260305 // GetJob returns one job with its steps. Fails with not_found if the job
261306 // or the attempt does not exist.
262307 GetJob(context.Context, *connect.Request[ermite.GetJobRequest]) (*connect.Response[ermite.GetJobResponse], error)
308+ // ListAnnotations lists the annotations of a run attempt, optionally of one
309+ // job, ordered by job (as in GetRun), step number, then the order in which
310+ // the step produced them. Needs the same access as GetRun. Returns at most
311+ // 1,000 annotations (truncated tells). A run without annotations gives an
312+ // empty list. Only servers with the feature "ci.annotations" implement it
313+ // (unimplemented otherwise). Fails with not_found if the run, the attempt or
314+ // the job does not exist, or if the job is not a job of the run.
315+ ListAnnotations(context.Context, *connect.Request[ermite.ListAnnotationsRequest]) (*connect.Response[ermite.ListAnnotationsResponse], error)
316+ // GetStepSummaries returns the job summaries (the Markdown written to
317+ // GITHUB_STEP_SUMMARY) of the steps of a job that have one, ordered by step
318+ // number. Needs the same access as GetJob. Only servers with the feature
319+ // "ci.summaries" implement it (unimplemented otherwise). A job without
320+ // summaries gives an empty list. Fails with not_found if the job or the
321+ // attempt does not exist.
322+ GetStepSummaries(context.Context, *connect.Request[ermite.GetStepSummariesRequest]) (*connect.Response[ermite.GetStepSummariesResponse], error)
263323 // GetJobLog streams the lines of the log of a job, from a line number. The
264324 // server sends them in order in messages of at most 1,000 lines and 64 KiB of
265325 // text each.
@@ -334,6 +394,18 @@ func NewRunServiceHandler(svc RunServiceHandler, opts ...connect.HandlerOption)
334394 connect.WithSchema(runServiceMethods.ByName("GetJob")),
335395 connect.WithHandlerOptions(opts...),
336396 )
397+ runServiceListAnnotationsHandler := connect.NewUnaryHandler(
398+ RunServiceListAnnotationsProcedure,
399+ svc.ListAnnotations,
400+ connect.WithSchema(runServiceMethods.ByName("ListAnnotations")),
401+ connect.WithHandlerOptions(opts...),
402+ )
403+ runServiceGetStepSummariesHandler := connect.NewUnaryHandler(
404+ RunServiceGetStepSummariesProcedure,
405+ svc.GetStepSummaries,
406+ connect.WithSchema(runServiceMethods.ByName("GetStepSummaries")),
407+ connect.WithHandlerOptions(opts...),
408+ )
337409 runServiceGetJobLogHandler := connect.NewServerStreamHandler(
338410 RunServiceGetJobLogProcedure,
339411 svc.GetJobLog,
@@ -378,6 +450,10 @@ func NewRunServiceHandler(svc RunServiceHandler, opts ...connect.HandlerOption)
378450 runServiceGetRunHandler.ServeHTTP(w, r)
379451 case RunServiceGetJobProcedure:
380452 runServiceGetJobHandler.ServeHTTP(w, r)
453+ case RunServiceListAnnotationsProcedure:
454+ runServiceListAnnotationsHandler.ServeHTTP(w, r)
455+ case RunServiceGetStepSummariesProcedure:
456+ runServiceGetStepSummariesHandler.ServeHTTP(w, r)
381457 case RunServiceGetJobLogProcedure:
382458 runServiceGetJobLogHandler.ServeHTTP(w, r)
383459 case RunServiceCancelRunProcedure:
@@ -411,6 +487,14 @@ func (UnimplementedRunServiceHandler) GetJob(context.Context, *connect.Request[e
411487 return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunService.GetJob is not implemented"))
412488 }
413489
490+func (UnimplementedRunServiceHandler) ListAnnotations(context.Context, *connect.Request[ermite.ListAnnotationsRequest]) (*connect.Response[ermite.ListAnnotationsResponse], error) {
491+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunService.ListAnnotations is not implemented"))
492+}
493+
494+func (UnimplementedRunServiceHandler) GetStepSummaries(context.Context, *connect.Request[ermite.GetStepSummariesRequest]) (*connect.Response[ermite.GetStepSummariesResponse], error) {
495+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunService.GetStepSummaries is not implemented"))
496+}
497+
414498 func (UnimplementedRunServiceHandler) GetJobLog(context.Context, *connect.Request[ermite.GetJobLogRequest], *connect.ServerStream[ermite.GetJobLogResponse]) error {
415499 return connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunService.GetJobLog is not implemented"))
416500 }
@@ -41,6 +41,12 @@ const (
41 RunServiceGetRunProcedure = "/ermite.RunService/GetRun"41 RunServiceGetRunProcedure = "/ermite.RunService/GetRun"
42 // RunServiceGetJobProcedure is the fully-qualified name of the RunService's GetJob RPC.42 // RunServiceGetJobProcedure is the fully-qualified name of the RunService's GetJob RPC.
43 RunServiceGetJobProcedure = "/ermite.RunService/GetJob"43 RunServiceGetJobProcedure = "/ermite.RunService/GetJob"
44+ // RunServiceListAnnotationsProcedure is the fully-qualified name of the RunService's
45+ // ListAnnotations RPC.
46+ RunServiceListAnnotationsProcedure = "/ermite.RunService/ListAnnotations"
47+ // RunServiceGetStepSummariesProcedure is the fully-qualified name of the RunService's
48+ // GetStepSummaries RPC.
49+ RunServiceGetStepSummariesProcedure = "/ermite.RunService/GetStepSummaries"
44 // RunServiceGetJobLogProcedure is the fully-qualified name of the RunService's GetJobLog RPC.50 // RunServiceGetJobLogProcedure is the fully-qualified name of the RunService's GetJobLog RPC.
45 RunServiceGetJobLogProcedure = "/ermite.RunService/GetJobLog"51 RunServiceGetJobLogProcedure = "/ermite.RunService/GetJobLog"
46 // RunServiceCancelRunProcedure is the fully-qualified name of the RunService's CancelRun RPC.52 // RunServiceCancelRunProcedure is the fully-qualified name of the RunService's CancelRun RPC.
@@ -70,6 +76,21 @@ type RunServiceClient interface {
70 // GetJob returns one job with its steps. Fails with not_found if the job76 // GetJob returns one job with its steps. Fails with not_found if the job
71 // or the attempt does not exist.77 // or the attempt does not exist.
72 GetJob(context.Context, *connect.Request[ermite.GetJobRequest]) (*connect.Response[ermite.GetJobResponse], error)78 GetJob(context.Context, *connect.Request[ermite.GetJobRequest]) (*connect.Response[ermite.GetJobResponse], error)
79+ // ListAnnotations lists the annotations of a run attempt, optionally of one
80+ // job, ordered by job (as in GetRun), step number, then the order in which
81+ // the step produced them. Needs the same access as GetRun. Returns at most
82+ // 1,000 annotations (truncated tells). A run without annotations gives an
83+ // empty list. Only servers with the feature "ci.annotations" implement it
84+ // (unimplemented otherwise). Fails with not_found if the run, the attempt or
85+ // the job does not exist, or if the job is not a job of the run.
86+ ListAnnotations(context.Context, *connect.Request[ermite.ListAnnotationsRequest]) (*connect.Response[ermite.ListAnnotationsResponse], error)
87+ // GetStepSummaries returns the job summaries (the Markdown written to
88+ // GITHUB_STEP_SUMMARY) of the steps of a job that have one, ordered by step
89+ // number. Needs the same access as GetJob. Only servers with the feature
90+ // "ci.summaries" implement it (unimplemented otherwise). A job without
91+ // summaries gives an empty list. Fails with not_found if the job or the
92+ // attempt does not exist.
93+ GetStepSummaries(context.Context, *connect.Request[ermite.GetStepSummariesRequest]) (*connect.Response[ermite.GetStepSummariesResponse], error)
73 // GetJobLog streams the lines of the log of a job, from a line number. The94 // GetJobLog streams the lines of the log of a job, from a line number. The
74 // server sends them in order in messages of at most 1,000 lines and 64 KiB of95 // server sends them in order in messages of at most 1,000 lines and 64 KiB of
75 // text each.96 // text each.
@@ -148,6 +169,18 @@ func NewRunServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...
148 connect.WithSchema(runServiceMethods.ByName("GetJob")),169 connect.WithSchema(runServiceMethods.ByName("GetJob")),
149 connect.WithClientOptions(opts...),170 connect.WithClientOptions(opts...),
150 ),171 ),
172+ listAnnotations: connect.NewClient[ermite.ListAnnotationsRequest, ermite.ListAnnotationsResponse](
173+ httpClient,
174+ baseURL+RunServiceListAnnotationsProcedure,
175+ connect.WithSchema(runServiceMethods.ByName("ListAnnotations")),
176+ connect.WithClientOptions(opts...),
177+ ),
178+ getStepSummaries: connect.NewClient[ermite.GetStepSummariesRequest, ermite.GetStepSummariesResponse](
179+ httpClient,
180+ baseURL+RunServiceGetStepSummariesProcedure,
181+ connect.WithSchema(runServiceMethods.ByName("GetStepSummaries")),
182+ connect.WithClientOptions(opts...),
183+ ),
151 getJobLog: connect.NewClient[ermite.GetJobLogRequest, ermite.GetJobLogResponse](184 getJobLog: connect.NewClient[ermite.GetJobLogRequest, ermite.GetJobLogResponse](
152 httpClient,185 httpClient,
153 baseURL+RunServiceGetJobLogProcedure,186 baseURL+RunServiceGetJobLogProcedure,
@@ -189,15 +222,17 @@ func NewRunServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...
189 222
190 // runServiceClient implements RunServiceClient.223 // runServiceClient implements RunServiceClient.
191 type runServiceClient struct {224 type runServiceClient struct {
192- listRuns *connect.Client[ermite.ListRunsRequest, ermite.ListRunsResponse]225+ listRuns *connect.Client[ermite.ListRunsRequest, ermite.ListRunsResponse]
193- getRun *connect.Client[ermite.GetRunRequest, ermite.GetRunResponse]226+ getRun *connect.Client[ermite.GetRunRequest, ermite.GetRunResponse]
194- getJob *connect.Client[ermite.GetJobRequest, ermite.GetJobResponse]227+ getJob *connect.Client[ermite.GetJobRequest, ermite.GetJobResponse]
195- getJobLog *connect.Client[ermite.GetJobLogRequest, ermite.GetJobLogResponse]228+ listAnnotations *connect.Client[ermite.ListAnnotationsRequest, ermite.ListAnnotationsResponse]
196- cancelRun *connect.Client[ermite.CancelRunRequest, ermite.CancelRunResponse]229+ getStepSummaries *connect.Client[ermite.GetStepSummariesRequest, ermite.GetStepSummariesResponse]
197- rerunRun *connect.Client[ermite.RerunRunRequest, ermite.RerunRunResponse]230+ getJobLog *connect.Client[ermite.GetJobLogRequest, ermite.GetJobLogResponse]
198- listChecks *connect.Client[ermite.ListChecksRequest, ermite.ListChecksResponse]231+ cancelRun *connect.Client[ermite.CancelRunRequest, ermite.CancelRunResponse]
199- listRunners *connect.Client[ermite.ListRunnersRequest, ermite.ListRunnersResponse]232+ rerunRun *connect.Client[ermite.RerunRunRequest, ermite.RerunRunResponse]
200- removeRunner *connect.Client[ermite.RemoveRunnerRequest, ermite.RemoveRunnerResponse]233+ listChecks *connect.Client[ermite.ListChecksRequest, ermite.ListChecksResponse]
234+ listRunners *connect.Client[ermite.ListRunnersRequest, ermite.ListRunnersResponse]
235+ removeRunner *connect.Client[ermite.RemoveRunnerRequest, ermite.RemoveRunnerResponse]
201 }236 }
202 237
203 // ListRuns calls ermite.RunService.ListRuns.238 // ListRuns calls ermite.RunService.ListRuns.
@@ -215,6 +250,16 @@ func (c *runServiceClient) GetJob(ctx context.Context, req *connect.Request[ermi
215 return c.getJob.CallUnary(ctx, req)250 return c.getJob.CallUnary(ctx, req)
216 }251 }
217 252
253+// ListAnnotations calls ermite.RunService.ListAnnotations.
254+func (c *runServiceClient) ListAnnotations(ctx context.Context, req *connect.Request[ermite.ListAnnotationsRequest]) (*connect.Response[ermite.ListAnnotationsResponse], error) {
255+ return c.listAnnotations.CallUnary(ctx, req)
256+}
257+
258+// GetStepSummaries calls ermite.RunService.GetStepSummaries.
259+func (c *runServiceClient) GetStepSummaries(ctx context.Context, req *connect.Request[ermite.GetStepSummariesRequest]) (*connect.Response[ermite.GetStepSummariesResponse], error) {
260+ return c.getStepSummaries.CallUnary(ctx, req)
261+}
262+
218 // GetJobLog calls ermite.RunService.GetJobLog.263 // GetJobLog calls ermite.RunService.GetJobLog.
219 func (c *runServiceClient) GetJobLog(ctx context.Context, req *connect.Request[ermite.GetJobLogRequest]) (*connect.ServerStreamForClient[ermite.GetJobLogResponse], error) {264 func (c *runServiceClient) GetJobLog(ctx context.Context, req *connect.Request[ermite.GetJobLogRequest]) (*connect.ServerStreamForClient[ermite.GetJobLogResponse], error) {
220 return c.getJobLog.CallServerStream(ctx, req)265 return c.getJobLog.CallServerStream(ctx, req)
@@ -260,6 +305,21 @@ type RunServiceHandler interface {
260 // GetJob returns one job with its steps. Fails with not_found if the job305 // GetJob returns one job with its steps. Fails with not_found if the job
261 // or the attempt does not exist.306 // or the attempt does not exist.
262 GetJob(context.Context, *connect.Request[ermite.GetJobRequest]) (*connect.Response[ermite.GetJobResponse], error)307 GetJob(context.Context, *connect.Request[ermite.GetJobRequest]) (*connect.Response[ermite.GetJobResponse], error)
308+ // ListAnnotations lists the annotations of a run attempt, optionally of one
309+ // job, ordered by job (as in GetRun), step number, then the order in which
310+ // the step produced them. Needs the same access as GetRun. Returns at most
311+ // 1,000 annotations (truncated tells). A run without annotations gives an
312+ // empty list. Only servers with the feature "ci.annotations" implement it
313+ // (unimplemented otherwise). Fails with not_found if the run, the attempt or
314+ // the job does not exist, or if the job is not a job of the run.
315+ ListAnnotations(context.Context, *connect.Request[ermite.ListAnnotationsRequest]) (*connect.Response[ermite.ListAnnotationsResponse], error)
316+ // GetStepSummaries returns the job summaries (the Markdown written to
317+ // GITHUB_STEP_SUMMARY) of the steps of a job that have one, ordered by step
318+ // number. Needs the same access as GetJob. Only servers with the feature
319+ // "ci.summaries" implement it (unimplemented otherwise). A job without
320+ // summaries gives an empty list. Fails with not_found if the job or the
321+ // attempt does not exist.
322+ GetStepSummaries(context.Context, *connect.Request[ermite.GetStepSummariesRequest]) (*connect.Response[ermite.GetStepSummariesResponse], error)
263 // GetJobLog streams the lines of the log of a job, from a line number. The323 // GetJobLog streams the lines of the log of a job, from a line number. The
264 // server sends them in order in messages of at most 1,000 lines and 64 KiB of324 // server sends them in order in messages of at most 1,000 lines and 64 KiB of
265 // text each.325 // text each.
@@ -334,6 +394,18 @@ func NewRunServiceHandler(svc RunServiceHandler, opts ...connect.HandlerOption)
334 connect.WithSchema(runServiceMethods.ByName("GetJob")),394 connect.WithSchema(runServiceMethods.ByName("GetJob")),
335 connect.WithHandlerOptions(opts...),395 connect.WithHandlerOptions(opts...),
336 )396 )
397+ runServiceListAnnotationsHandler := connect.NewUnaryHandler(
398+ RunServiceListAnnotationsProcedure,
399+ svc.ListAnnotations,
400+ connect.WithSchema(runServiceMethods.ByName("ListAnnotations")),
401+ connect.WithHandlerOptions(opts...),
402+ )
403+ runServiceGetStepSummariesHandler := connect.NewUnaryHandler(
404+ RunServiceGetStepSummariesProcedure,
405+ svc.GetStepSummaries,
406+ connect.WithSchema(runServiceMethods.ByName("GetStepSummaries")),
407+ connect.WithHandlerOptions(opts...),
408+ )
337 runServiceGetJobLogHandler := connect.NewServerStreamHandler(409 runServiceGetJobLogHandler := connect.NewServerStreamHandler(
338 RunServiceGetJobLogProcedure,410 RunServiceGetJobLogProcedure,
339 svc.GetJobLog,411 svc.GetJobLog,
@@ -378,6 +450,10 @@ func NewRunServiceHandler(svc RunServiceHandler, opts ...connect.HandlerOption)
378 runServiceGetRunHandler.ServeHTTP(w, r)450 runServiceGetRunHandler.ServeHTTP(w, r)
379 case RunServiceGetJobProcedure:451 case RunServiceGetJobProcedure:
380 runServiceGetJobHandler.ServeHTTP(w, r)452 runServiceGetJobHandler.ServeHTTP(w, r)
453+ case RunServiceListAnnotationsProcedure:
454+ runServiceListAnnotationsHandler.ServeHTTP(w, r)
455+ case RunServiceGetStepSummariesProcedure:
456+ runServiceGetStepSummariesHandler.ServeHTTP(w, r)
381 case RunServiceGetJobLogProcedure:457 case RunServiceGetJobLogProcedure:
382 runServiceGetJobLogHandler.ServeHTTP(w, r)458 runServiceGetJobLogHandler.ServeHTTP(w, r)
383 case RunServiceCancelRunProcedure:459 case RunServiceCancelRunProcedure:
@@ -411,6 +487,14 @@ func (UnimplementedRunServiceHandler) GetJob(context.Context, *connect.Request[e
411 return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunService.GetJob is not implemented"))487 return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunService.GetJob is not implemented"))
412 }488 }
413 489
490+func (UnimplementedRunServiceHandler) ListAnnotations(context.Context, *connect.Request[ermite.ListAnnotationsRequest]) (*connect.Response[ermite.ListAnnotationsResponse], error) {
491+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunService.ListAnnotations is not implemented"))
492+}
493+
494+func (UnimplementedRunServiceHandler) GetStepSummaries(context.Context, *connect.Request[ermite.GetStepSummariesRequest]) (*connect.Response[ermite.GetStepSummariesResponse], error) {
495+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunService.GetStepSummaries is not implemented"))
496+}
497+
414 func (UnimplementedRunServiceHandler) GetJobLog(context.Context, *connect.Request[ermite.GetJobLogRequest], *connect.ServerStream[ermite.GetJobLogResponse]) error {498 func (UnimplementedRunServiceHandler) GetJobLog(context.Context, *connect.Request[ermite.GetJobLogRequest], *connect.ServerStream[ermite.GetJobLogResponse]) error {
415 return connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunService.GetJobLog is not implemented"))499 return connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunService.GetJobLog is not implemented"))
416 }500 }
M gen/ermite/ermiteconnect/runner.connect.go +166 -12
@@ -51,6 +51,12 @@ const (
5151 // RunnerServiceCompleteJobProcedure is the fully-qualified name of the RunnerService's CompleteJob
5252 // RPC.
5353 RunnerServiceCompleteJobProcedure = "/ermite.RunnerService/CompleteJob"
54+ // RunnerServiceResolveActionsProcedure is the fully-qualified name of the RunnerService's
55+ // ResolveActions RPC.
56+ RunnerServiceResolveActionsProcedure = "/ermite.RunnerService/ResolveActions"
57+ // RunnerServiceUploadSummaryProcedure is the fully-qualified name of the RunnerService's
58+ // UploadSummary RPC.
59+ RunnerServiceUploadSummaryProcedure = "/ermite.RunnerService/UploadSummary"
5460 )
5561
5662 // RunnerServiceClient is a client for the ermite.RunnerService service.
@@ -105,10 +111,16 @@ type RunnerServiceClient interface {
105111 // identical call changes nothing, so a runner may retry after a network
106112 // error. To end the job use CompleteJob.
107113 //
108- // Fails with invalid_argument if a rule is broken, with failed_precondition
109- // if a step goes back (a COMPLETED step cannot change, a step cannot go from
110- // IN_PROGRESS to PENDING) and with failed_precondition and a JobEnded detail
111- // if the job has ended.
114+ // Annotations are applied with the steps, in the same atomic step: for each
115+ // step_number present in annotations, the annotations of that step stored
116+ // so far are replaced by the ones of the request, so a retry never
117+ // duplicates them. A step number absent from the request keeps its
118+ // annotations.
119+ //
120+ // Fails with invalid_argument if a rule is broken (including the annotation
121+ // limits), with failed_precondition if a step goes back (a COMPLETED step
122+ // cannot change, a step cannot go from IN_PROGRESS to PENDING) and with
123+ // failed_precondition and a JobEnded detail if the job has ended.
112124 UpdateJob(context.Context, *connect.Request[ermite.UpdateJobRequest]) (*connect.Response[ermite.UpdateJobResponse], error)
113125 // AppendLog appends lines to the log of a job. Needs the job token.
114126 //
@@ -213,9 +225,53 @@ type RunnerServiceClient interface {
213225 // ends are completed by the server as CI_CONCLUSION_CANCELLED (or SKIPPED when
214226 // they never started and the job did not end by cancellation).
215227 //
216- // Fails with invalid_argument if a rule is broken (including the output
217- // limits) and failed_precondition if a step goes back, as in UpdateJob.
228+ // Annotations are applied as in UpdateJob, together with the conclusion.
229+ // Summaries are not part of this call: a runner sends them with
230+ // UploadSummary before it.
231+ //
232+ // Fails with invalid_argument if a rule is broken (including the output and
233+ // annotation limits) and failed_precondition if a step goes back, as in
234+ // UpdateJob.
218235 CompleteJob(context.Context, *connect.Request[ermite.CompleteJobRequest]) (*connect.Response[ermite.CompleteJobResponse], error)
236+ // ResolveActions turns the "uses" of workflow and action steps into what the
237+ // runner needs to download the actions, as GitHub's action download info
238+ // does. Needs the job token; the job_id must be the one of the token. It
239+ // may be called as often as needed while the job runs (the runner calls it
240+ // once per level of composite actions, see the runner's depth limit of 9).
241+ // The runner then downloads each archive itself, from the sandbox, with the
242+ // token of the ResolvedAction when there is one.
243+ //
244+ // The response has one ResolvedAction per ActionRef, in the same order. A
245+ // reference that cannot be resolved (unknown repository, tag or branch, a
246+ // repository without access, a "uses" that GitHub itself would reject) does
247+ // not fail the call: its ResolvedAction has only ref and error, where error
248+ // is the message GitHub gives for the same failure, which the runner prints
249+ // as is. Only a failure of the call itself (credential, ended job, bad
250+ // request, server error) is an RPC error; the runner then fails the job.
251+ //
252+ // Resolution is stable for a run: the same ref resolves to the same sha for
253+ // every job and every call of one attempt of the run. A re-run attempt
254+ // resolves again.
255+ //
256+ // Fails with unauthenticated or permission_denied as the other job RPCs,
257+ // with failed_precondition and a JobEnded detail if the job has ended, and
258+ // with invalid_argument if there are more than 100 refs or if a "uses" is a
259+ // local action ("./path") or a Docker image ("docker://"), which the runner
260+ // handles without the server.
261+ ResolveActions(context.Context, *connect.Request[ermite.ResolveActionsRequest]) (*connect.Response[ermite.ResolveActionsResponse], error)
262+ // UploadSummary stores the job summary of one step: the Markdown that the
263+ // step wrote to the file named by GITHUB_STEP_SUMMARY. Needs the job token.
264+ // The runner masks it before sending it, and sends it after the step ended
265+ // and before CompleteJob.
266+ //
267+ // It is idempotent: the summary of a step is replaced by each call, so a
268+ // retry changes nothing, and an empty markdown removes the summary of the
269+ // step. The Markdown is at most 1 MiB (1,048,576 bytes) and valid UTF-8;
270+ // clients set a message size limit above it.
271+ //
272+ // Fails with invalid_argument if a rule is broken (size, UTF-8, step number)
273+ // and with failed_precondition and a JobEnded detail if the job has ended.
274+ UploadSummary(context.Context, *connect.Request[ermite.UploadSummaryRequest]) (*connect.Response[ermite.UploadSummaryResponse], error)
219275 }
220276
221277 // NewRunnerServiceClient constructs a client for the ermite.RunnerService service. By default, it
@@ -271,6 +327,18 @@ func NewRunnerServiceClient(httpClient connect.HTTPClient, baseURL string, opts
271327 connect.WithSchema(runnerServiceMethods.ByName("CompleteJob")),
272328 connect.WithClientOptions(opts...),
273329 ),
330+ resolveActions: connect.NewClient[ermite.ResolveActionsRequest, ermite.ResolveActionsResponse](
331+ httpClient,
332+ baseURL+RunnerServiceResolveActionsProcedure,
333+ connect.WithSchema(runnerServiceMethods.ByName("ResolveActions")),
334+ connect.WithClientOptions(opts...),
335+ ),
336+ uploadSummary: connect.NewClient[ermite.UploadSummaryRequest, ermite.UploadSummaryResponse](
337+ httpClient,
338+ baseURL+RunnerServiceUploadSummaryProcedure,
339+ connect.WithSchema(runnerServiceMethods.ByName("UploadSummary")),
340+ connect.WithClientOptions(opts...),
341+ ),
274342 }
275343 }
276344
@@ -283,6 +351,8 @@ type runnerServiceClient struct {
283351 heartbeat *connect.Client[ermite.HeartbeatRequest, ermite.HeartbeatResponse]
284352 watchJob *connect.Client[ermite.WatchJobRequest, ermite.WatchJobResponse]
285353 completeJob *connect.Client[ermite.CompleteJobRequest, ermite.CompleteJobResponse]
354+ resolveActions *connect.Client[ermite.ResolveActionsRequest, ermite.ResolveActionsResponse]
355+ uploadSummary *connect.Client[ermite.UploadSummaryRequest, ermite.UploadSummaryResponse]
286356 }
287357
288358 // RegisterRunner calls ermite.RunnerService.RegisterRunner.
@@ -320,6 +390,16 @@ func (c *runnerServiceClient) CompleteJob(ctx context.Context, req *connect.Requ
320390 return c.completeJob.CallUnary(ctx, req)
321391 }
322392
393+// ResolveActions calls ermite.RunnerService.ResolveActions.
394+func (c *runnerServiceClient) ResolveActions(ctx context.Context, req *connect.Request[ermite.ResolveActionsRequest]) (*connect.Response[ermite.ResolveActionsResponse], error) {
395+ return c.resolveActions.CallUnary(ctx, req)
396+}
397+
398+// UploadSummary calls ermite.RunnerService.UploadSummary.
399+func (c *runnerServiceClient) UploadSummary(ctx context.Context, req *connect.Request[ermite.UploadSummaryRequest]) (*connect.Response[ermite.UploadSummaryResponse], error) {
400+ return c.uploadSummary.CallUnary(ctx, req)
401+}
402+
323403 // RunnerServiceHandler is an implementation of the ermite.RunnerService service.
324404 type RunnerServiceHandler interface {
325405 // RegisterRunner registers a runner and returns its runner token, which is
@@ -372,10 +452,16 @@ type RunnerServiceHandler interface {
372452 // identical call changes nothing, so a runner may retry after a network
373453 // error. To end the job use CompleteJob.
374454 //
375- // Fails with invalid_argument if a rule is broken, with failed_precondition
376- // if a step goes back (a COMPLETED step cannot change, a step cannot go from
377- // IN_PROGRESS to PENDING) and with failed_precondition and a JobEnded detail
378- // if the job has ended.
455+ // Annotations are applied with the steps, in the same atomic step: for each
456+ // step_number present in annotations, the annotations of that step stored
457+ // so far are replaced by the ones of the request, so a retry never
458+ // duplicates them. A step number absent from the request keeps its
459+ // annotations.
460+ //
461+ // Fails with invalid_argument if a rule is broken (including the annotation
462+ // limits), with failed_precondition if a step goes back (a COMPLETED step
463+ // cannot change, a step cannot go from IN_PROGRESS to PENDING) and with
464+ // failed_precondition and a JobEnded detail if the job has ended.
379465 UpdateJob(context.Context, *connect.Request[ermite.UpdateJobRequest]) (*connect.Response[ermite.UpdateJobResponse], error)
380466 // AppendLog appends lines to the log of a job. Needs the job token.
381467 //
@@ -480,9 +566,53 @@ type RunnerServiceHandler interface {
480566 // ends are completed by the server as CI_CONCLUSION_CANCELLED (or SKIPPED when
481567 // they never started and the job did not end by cancellation).
482568 //
483- // Fails with invalid_argument if a rule is broken (including the output
484- // limits) and failed_precondition if a step goes back, as in UpdateJob.
569+ // Annotations are applied as in UpdateJob, together with the conclusion.
570+ // Summaries are not part of this call: a runner sends them with
571+ // UploadSummary before it.
572+ //
573+ // Fails with invalid_argument if a rule is broken (including the output and
574+ // annotation limits) and failed_precondition if a step goes back, as in
575+ // UpdateJob.
485576 CompleteJob(context.Context, *connect.Request[ermite.CompleteJobRequest]) (*connect.Response[ermite.CompleteJobResponse], error)
577+ // ResolveActions turns the "uses" of workflow and action steps into what the
578+ // runner needs to download the actions, as GitHub's action download info
579+ // does. Needs the job token; the job_id must be the one of the token. It
580+ // may be called as often as needed while the job runs (the runner calls it
581+ // once per level of composite actions, see the runner's depth limit of 9).
582+ // The runner then downloads each archive itself, from the sandbox, with the
583+ // token of the ResolvedAction when there is one.
584+ //
585+ // The response has one ResolvedAction per ActionRef, in the same order. A
586+ // reference that cannot be resolved (unknown repository, tag or branch, a
587+ // repository without access, a "uses" that GitHub itself would reject) does
588+ // not fail the call: its ResolvedAction has only ref and error, where error
589+ // is the message GitHub gives for the same failure, which the runner prints
590+ // as is. Only a failure of the call itself (credential, ended job, bad
591+ // request, server error) is an RPC error; the runner then fails the job.
592+ //
593+ // Resolution is stable for a run: the same ref resolves to the same sha for
594+ // every job and every call of one attempt of the run. A re-run attempt
595+ // resolves again.
596+ //
597+ // Fails with unauthenticated or permission_denied as the other job RPCs,
598+ // with failed_precondition and a JobEnded detail if the job has ended, and
599+ // with invalid_argument if there are more than 100 refs or if a "uses" is a
600+ // local action ("./path") or a Docker image ("docker://"), which the runner
601+ // handles without the server.
602+ ResolveActions(context.Context, *connect.Request[ermite.ResolveActionsRequest]) (*connect.Response[ermite.ResolveActionsResponse], error)
603+ // UploadSummary stores the job summary of one step: the Markdown that the
604+ // step wrote to the file named by GITHUB_STEP_SUMMARY. Needs the job token.
605+ // The runner masks it before sending it, and sends it after the step ended
606+ // and before CompleteJob.
607+ //
608+ // It is idempotent: the summary of a step is replaced by each call, so a
609+ // retry changes nothing, and an empty markdown removes the summary of the
610+ // step. The Markdown is at most 1 MiB (1,048,576 bytes) and valid UTF-8;
611+ // clients set a message size limit above it.
612+ //
613+ // Fails with invalid_argument if a rule is broken (size, UTF-8, step number)
614+ // and with failed_precondition and a JobEnded detail if the job has ended.
615+ UploadSummary(context.Context, *connect.Request[ermite.UploadSummaryRequest]) (*connect.Response[ermite.UploadSummaryResponse], error)
486616 }
487617
488618 // NewRunnerServiceHandler builds an HTTP handler from the service implementation. It returns the
@@ -534,6 +664,18 @@ func NewRunnerServiceHandler(svc RunnerServiceHandler, opts ...connect.HandlerOp
534664 connect.WithSchema(runnerServiceMethods.ByName("CompleteJob")),
535665 connect.WithHandlerOptions(opts...),
536666 )
667+ runnerServiceResolveActionsHandler := connect.NewUnaryHandler(
668+ RunnerServiceResolveActionsProcedure,
669+ svc.ResolveActions,
670+ connect.WithSchema(runnerServiceMethods.ByName("ResolveActions")),
671+ connect.WithHandlerOptions(opts...),
672+ )
673+ runnerServiceUploadSummaryHandler := connect.NewUnaryHandler(
674+ RunnerServiceUploadSummaryProcedure,
675+ svc.UploadSummary,
676+ connect.WithSchema(runnerServiceMethods.ByName("UploadSummary")),
677+ connect.WithHandlerOptions(opts...),
678+ )
537679 return "/ermite.RunnerService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
538680 switch r.URL.Path {
539681 case RunnerServiceRegisterRunnerProcedure:
@@ -550,6 +692,10 @@ func NewRunnerServiceHandler(svc RunnerServiceHandler, opts ...connect.HandlerOp
550692 runnerServiceWatchJobHandler.ServeHTTP(w, r)
551693 case RunnerServiceCompleteJobProcedure:
552694 runnerServiceCompleteJobHandler.ServeHTTP(w, r)
695+ case RunnerServiceResolveActionsProcedure:
696+ runnerServiceResolveActionsHandler.ServeHTTP(w, r)
697+ case RunnerServiceUploadSummaryProcedure:
698+ runnerServiceUploadSummaryHandler.ServeHTTP(w, r)
553699 default:
554700 http.NotFound(w, r)
555701 }
@@ -586,3 +732,11 @@ func (UnimplementedRunnerServiceHandler) WatchJob(context.Context, *connect.Requ
586732 func (UnimplementedRunnerServiceHandler) CompleteJob(context.Context, *connect.Request[ermite.CompleteJobRequest]) (*connect.Response[ermite.CompleteJobResponse], error) {
587733 return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunnerService.CompleteJob is not implemented"))
588734 }
735+
736+func (UnimplementedRunnerServiceHandler) ResolveActions(context.Context, *connect.Request[ermite.ResolveActionsRequest]) (*connect.Response[ermite.ResolveActionsResponse], error) {
737+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunnerService.ResolveActions is not implemented"))
738+}
739+
740+func (UnimplementedRunnerServiceHandler) UploadSummary(context.Context, *connect.Request[ermite.UploadSummaryRequest]) (*connect.Response[ermite.UploadSummaryResponse], error) {
741+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunnerService.UploadSummary is not implemented"))
742+}
@@ -51,6 +51,12 @@ const (
51 // RunnerServiceCompleteJobProcedure is the fully-qualified name of the RunnerService's CompleteJob51 // RunnerServiceCompleteJobProcedure is the fully-qualified name of the RunnerService's CompleteJob
52 // RPC.52 // RPC.
53 RunnerServiceCompleteJobProcedure = "/ermite.RunnerService/CompleteJob"53 RunnerServiceCompleteJobProcedure = "/ermite.RunnerService/CompleteJob"
54+ // RunnerServiceResolveActionsProcedure is the fully-qualified name of the RunnerService's
55+ // ResolveActions RPC.
56+ RunnerServiceResolveActionsProcedure = "/ermite.RunnerService/ResolveActions"
57+ // RunnerServiceUploadSummaryProcedure is the fully-qualified name of the RunnerService's
58+ // UploadSummary RPC.
59+ RunnerServiceUploadSummaryProcedure = "/ermite.RunnerService/UploadSummary"
54 )60 )
55 61
56 // RunnerServiceClient is a client for the ermite.RunnerService service.62 // RunnerServiceClient is a client for the ermite.RunnerService service.
@@ -105,10 +111,16 @@ type RunnerServiceClient interface {
105 // identical call changes nothing, so a runner may retry after a network111 // identical call changes nothing, so a runner may retry after a network
106 // error. To end the job use CompleteJob.112 // error. To end the job use CompleteJob.
107 //113 //
108- // Fails with invalid_argument if a rule is broken, with failed_precondition114+ // Annotations are applied with the steps, in the same atomic step: for each
109- // if a step goes back (a COMPLETED step cannot change, a step cannot go from115+ // step_number present in annotations, the annotations of that step stored
110- // IN_PROGRESS to PENDING) and with failed_precondition and a JobEnded detail116+ // so far are replaced by the ones of the request, so a retry never
111- // if the job has ended.117+ // duplicates them. A step number absent from the request keeps its
118+ // annotations.
119+ //
120+ // Fails with invalid_argument if a rule is broken (including the annotation
121+ // limits), with failed_precondition if a step goes back (a COMPLETED step
122+ // cannot change, a step cannot go from IN_PROGRESS to PENDING) and with
123+ // failed_precondition and a JobEnded detail if the job has ended.
112 UpdateJob(context.Context, *connect.Request[ermite.UpdateJobRequest]) (*connect.Response[ermite.UpdateJobResponse], error)124 UpdateJob(context.Context, *connect.Request[ermite.UpdateJobRequest]) (*connect.Response[ermite.UpdateJobResponse], error)
113 // AppendLog appends lines to the log of a job. Needs the job token.125 // AppendLog appends lines to the log of a job. Needs the job token.
114 //126 //
@@ -213,9 +225,53 @@ type RunnerServiceClient interface {
213 // ends are completed by the server as CI_CONCLUSION_CANCELLED (or SKIPPED when225 // ends are completed by the server as CI_CONCLUSION_CANCELLED (or SKIPPED when
214 // they never started and the job did not end by cancellation).226 // they never started and the job did not end by cancellation).
215 //227 //
216- // Fails with invalid_argument if a rule is broken (including the output228+ // Annotations are applied as in UpdateJob, together with the conclusion.
217- // limits) and failed_precondition if a step goes back, as in UpdateJob.229+ // Summaries are not part of this call: a runner sends them with
230+ // UploadSummary before it.
231+ //
232+ // Fails with invalid_argument if a rule is broken (including the output and
233+ // annotation limits) and failed_precondition if a step goes back, as in
234+ // UpdateJob.
218 CompleteJob(context.Context, *connect.Request[ermite.CompleteJobRequest]) (*connect.Response[ermite.CompleteJobResponse], error)235 CompleteJob(context.Context, *connect.Request[ermite.CompleteJobRequest]) (*connect.Response[ermite.CompleteJobResponse], error)
236+ // ResolveActions turns the "uses" of workflow and action steps into what the
237+ // runner needs to download the actions, as GitHub's action download info
238+ // does. Needs the job token; the job_id must be the one of the token. It
239+ // may be called as often as needed while the job runs (the runner calls it
240+ // once per level of composite actions, see the runner's depth limit of 9).
241+ // The runner then downloads each archive itself, from the sandbox, with the
242+ // token of the ResolvedAction when there is one.
243+ //
244+ // The response has one ResolvedAction per ActionRef, in the same order. A
245+ // reference that cannot be resolved (unknown repository, tag or branch, a
246+ // repository without access, a "uses" that GitHub itself would reject) does
247+ // not fail the call: its ResolvedAction has only ref and error, where error
248+ // is the message GitHub gives for the same failure, which the runner prints
249+ // as is. Only a failure of the call itself (credential, ended job, bad
250+ // request, server error) is an RPC error; the runner then fails the job.
251+ //
252+ // Resolution is stable for a run: the same ref resolves to the same sha for
253+ // every job and every call of one attempt of the run. A re-run attempt
254+ // resolves again.
255+ //
256+ // Fails with unauthenticated or permission_denied as the other job RPCs,
257+ // with failed_precondition and a JobEnded detail if the job has ended, and
258+ // with invalid_argument if there are more than 100 refs or if a "uses" is a
259+ // local action ("./path") or a Docker image ("docker://"), which the runner
260+ // handles without the server.
261+ ResolveActions(context.Context, *connect.Request[ermite.ResolveActionsRequest]) (*connect.Response[ermite.ResolveActionsResponse], error)
262+ // UploadSummary stores the job summary of one step: the Markdown that the
263+ // step wrote to the file named by GITHUB_STEP_SUMMARY. Needs the job token.
264+ // The runner masks it before sending it, and sends it after the step ended
265+ // and before CompleteJob.
266+ //
267+ // It is idempotent: the summary of a step is replaced by each call, so a
268+ // retry changes nothing, and an empty markdown removes the summary of the
269+ // step. The Markdown is at most 1 MiB (1,048,576 bytes) and valid UTF-8;
270+ // clients set a message size limit above it.
271+ //
272+ // Fails with invalid_argument if a rule is broken (size, UTF-8, step number)
273+ // and with failed_precondition and a JobEnded detail if the job has ended.
274+ UploadSummary(context.Context, *connect.Request[ermite.UploadSummaryRequest]) (*connect.Response[ermite.UploadSummaryResponse], error)
219 }275 }
220 276
221 // NewRunnerServiceClient constructs a client for the ermite.RunnerService service. By default, it277 // NewRunnerServiceClient constructs a client for the ermite.RunnerService service. By default, it
@@ -271,6 +327,18 @@ func NewRunnerServiceClient(httpClient connect.HTTPClient, baseURL string, opts
271 connect.WithSchema(runnerServiceMethods.ByName("CompleteJob")),327 connect.WithSchema(runnerServiceMethods.ByName("CompleteJob")),
272 connect.WithClientOptions(opts...),328 connect.WithClientOptions(opts...),
273 ),329 ),
330+ resolveActions: connect.NewClient[ermite.ResolveActionsRequest, ermite.ResolveActionsResponse](
331+ httpClient,
332+ baseURL+RunnerServiceResolveActionsProcedure,
333+ connect.WithSchema(runnerServiceMethods.ByName("ResolveActions")),
334+ connect.WithClientOptions(opts...),
335+ ),
336+ uploadSummary: connect.NewClient[ermite.UploadSummaryRequest, ermite.UploadSummaryResponse](
337+ httpClient,
338+ baseURL+RunnerServiceUploadSummaryProcedure,
339+ connect.WithSchema(runnerServiceMethods.ByName("UploadSummary")),
340+ connect.WithClientOptions(opts...),
341+ ),
274 }342 }
275 }343 }
276 344
@@ -283,6 +351,8 @@ type runnerServiceClient struct {
283 heartbeat *connect.Client[ermite.HeartbeatRequest, ermite.HeartbeatResponse]351 heartbeat *connect.Client[ermite.HeartbeatRequest, ermite.HeartbeatResponse]
284 watchJob *connect.Client[ermite.WatchJobRequest, ermite.WatchJobResponse]352 watchJob *connect.Client[ermite.WatchJobRequest, ermite.WatchJobResponse]
285 completeJob *connect.Client[ermite.CompleteJobRequest, ermite.CompleteJobResponse]353 completeJob *connect.Client[ermite.CompleteJobRequest, ermite.CompleteJobResponse]
354+ resolveActions *connect.Client[ermite.ResolveActionsRequest, ermite.ResolveActionsResponse]
355+ uploadSummary *connect.Client[ermite.UploadSummaryRequest, ermite.UploadSummaryResponse]
286 }356 }
287 357
288 // RegisterRunner calls ermite.RunnerService.RegisterRunner.358 // RegisterRunner calls ermite.RunnerService.RegisterRunner.
@@ -320,6 +390,16 @@ func (c *runnerServiceClient) CompleteJob(ctx context.Context, req *connect.Requ
320 return c.completeJob.CallUnary(ctx, req)390 return c.completeJob.CallUnary(ctx, req)
321 }391 }
322 392
393+// ResolveActions calls ermite.RunnerService.ResolveActions.
394+func (c *runnerServiceClient) ResolveActions(ctx context.Context, req *connect.Request[ermite.ResolveActionsRequest]) (*connect.Response[ermite.ResolveActionsResponse], error) {
395+ return c.resolveActions.CallUnary(ctx, req)
396+}
397+
398+// UploadSummary calls ermite.RunnerService.UploadSummary.
399+func (c *runnerServiceClient) UploadSummary(ctx context.Context, req *connect.Request[ermite.UploadSummaryRequest]) (*connect.Response[ermite.UploadSummaryResponse], error) {
400+ return c.uploadSummary.CallUnary(ctx, req)
401+}
402+
323 // RunnerServiceHandler is an implementation of the ermite.RunnerService service.403 // RunnerServiceHandler is an implementation of the ermite.RunnerService service.
324 type RunnerServiceHandler interface {404 type RunnerServiceHandler interface {
325 // RegisterRunner registers a runner and returns its runner token, which is405 // RegisterRunner registers a runner and returns its runner token, which is
@@ -372,10 +452,16 @@ type RunnerServiceHandler interface {
372 // identical call changes nothing, so a runner may retry after a network452 // identical call changes nothing, so a runner may retry after a network
373 // error. To end the job use CompleteJob.453 // error. To end the job use CompleteJob.
374 //454 //
375- // Fails with invalid_argument if a rule is broken, with failed_precondition455+ // Annotations are applied with the steps, in the same atomic step: for each
376- // if a step goes back (a COMPLETED step cannot change, a step cannot go from456+ // step_number present in annotations, the annotations of that step stored
377- // IN_PROGRESS to PENDING) and with failed_precondition and a JobEnded detail457+ // so far are replaced by the ones of the request, so a retry never
378- // if the job has ended.458+ // duplicates them. A step number absent from the request keeps its
459+ // annotations.
460+ //
461+ // Fails with invalid_argument if a rule is broken (including the annotation
462+ // limits), with failed_precondition if a step goes back (a COMPLETED step
463+ // cannot change, a step cannot go from IN_PROGRESS to PENDING) and with
464+ // failed_precondition and a JobEnded detail if the job has ended.
379 UpdateJob(context.Context, *connect.Request[ermite.UpdateJobRequest]) (*connect.Response[ermite.UpdateJobResponse], error)465 UpdateJob(context.Context, *connect.Request[ermite.UpdateJobRequest]) (*connect.Response[ermite.UpdateJobResponse], error)
380 // AppendLog appends lines to the log of a job. Needs the job token.466 // AppendLog appends lines to the log of a job. Needs the job token.
381 //467 //
@@ -480,9 +566,53 @@ type RunnerServiceHandler interface {
480 // ends are completed by the server as CI_CONCLUSION_CANCELLED (or SKIPPED when566 // ends are completed by the server as CI_CONCLUSION_CANCELLED (or SKIPPED when
481 // they never started and the job did not end by cancellation).567 // they never started and the job did not end by cancellation).
482 //568 //
483- // Fails with invalid_argument if a rule is broken (including the output569+ // Annotations are applied as in UpdateJob, together with the conclusion.
484- // limits) and failed_precondition if a step goes back, as in UpdateJob.570+ // Summaries are not part of this call: a runner sends them with
571+ // UploadSummary before it.
572+ //
573+ // Fails with invalid_argument if a rule is broken (including the output and
574+ // annotation limits) and failed_precondition if a step goes back, as in
575+ // UpdateJob.
485 CompleteJob(context.Context, *connect.Request[ermite.CompleteJobRequest]) (*connect.Response[ermite.CompleteJobResponse], error)576 CompleteJob(context.Context, *connect.Request[ermite.CompleteJobRequest]) (*connect.Response[ermite.CompleteJobResponse], error)
577+ // ResolveActions turns the "uses" of workflow and action steps into what the
578+ // runner needs to download the actions, as GitHub's action download info
579+ // does. Needs the job token; the job_id must be the one of the token. It
580+ // may be called as often as needed while the job runs (the runner calls it
581+ // once per level of composite actions, see the runner's depth limit of 9).
582+ // The runner then downloads each archive itself, from the sandbox, with the
583+ // token of the ResolvedAction when there is one.
584+ //
585+ // The response has one ResolvedAction per ActionRef, in the same order. A
586+ // reference that cannot be resolved (unknown repository, tag or branch, a
587+ // repository without access, a "uses" that GitHub itself would reject) does
588+ // not fail the call: its ResolvedAction has only ref and error, where error
589+ // is the message GitHub gives for the same failure, which the runner prints
590+ // as is. Only a failure of the call itself (credential, ended job, bad
591+ // request, server error) is an RPC error; the runner then fails the job.
592+ //
593+ // Resolution is stable for a run: the same ref resolves to the same sha for
594+ // every job and every call of one attempt of the run. A re-run attempt
595+ // resolves again.
596+ //
597+ // Fails with unauthenticated or permission_denied as the other job RPCs,
598+ // with failed_precondition and a JobEnded detail if the job has ended, and
599+ // with invalid_argument if there are more than 100 refs or if a "uses" is a
600+ // local action ("./path") or a Docker image ("docker://"), which the runner
601+ // handles without the server.
602+ ResolveActions(context.Context, *connect.Request[ermite.ResolveActionsRequest]) (*connect.Response[ermite.ResolveActionsResponse], error)
603+ // UploadSummary stores the job summary of one step: the Markdown that the
604+ // step wrote to the file named by GITHUB_STEP_SUMMARY. Needs the job token.
605+ // The runner masks it before sending it, and sends it after the step ended
606+ // and before CompleteJob.
607+ //
608+ // It is idempotent: the summary of a step is replaced by each call, so a
609+ // retry changes nothing, and an empty markdown removes the summary of the
610+ // step. The Markdown is at most 1 MiB (1,048,576 bytes) and valid UTF-8;
611+ // clients set a message size limit above it.
612+ //
613+ // Fails with invalid_argument if a rule is broken (size, UTF-8, step number)
614+ // and with failed_precondition and a JobEnded detail if the job has ended.
615+ UploadSummary(context.Context, *connect.Request[ermite.UploadSummaryRequest]) (*connect.Response[ermite.UploadSummaryResponse], error)
486 }616 }
487 617
488 // NewRunnerServiceHandler builds an HTTP handler from the service implementation. It returns the618 // NewRunnerServiceHandler builds an HTTP handler from the service implementation. It returns the
@@ -534,6 +664,18 @@ func NewRunnerServiceHandler(svc RunnerServiceHandler, opts ...connect.HandlerOp
534 connect.WithSchema(runnerServiceMethods.ByName("CompleteJob")),664 connect.WithSchema(runnerServiceMethods.ByName("CompleteJob")),
535 connect.WithHandlerOptions(opts...),665 connect.WithHandlerOptions(opts...),
536 )666 )
667+ runnerServiceResolveActionsHandler := connect.NewUnaryHandler(
668+ RunnerServiceResolveActionsProcedure,
669+ svc.ResolveActions,
670+ connect.WithSchema(runnerServiceMethods.ByName("ResolveActions")),
671+ connect.WithHandlerOptions(opts...),
672+ )
673+ runnerServiceUploadSummaryHandler := connect.NewUnaryHandler(
674+ RunnerServiceUploadSummaryProcedure,
675+ svc.UploadSummary,
676+ connect.WithSchema(runnerServiceMethods.ByName("UploadSummary")),
677+ connect.WithHandlerOptions(opts...),
678+ )
537 return "/ermite.RunnerService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {679 return "/ermite.RunnerService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
538 switch r.URL.Path {680 switch r.URL.Path {
539 case RunnerServiceRegisterRunnerProcedure:681 case RunnerServiceRegisterRunnerProcedure:
@@ -550,6 +692,10 @@ func NewRunnerServiceHandler(svc RunnerServiceHandler, opts ...connect.HandlerOp
550 runnerServiceWatchJobHandler.ServeHTTP(w, r)692 runnerServiceWatchJobHandler.ServeHTTP(w, r)
551 case RunnerServiceCompleteJobProcedure:693 case RunnerServiceCompleteJobProcedure:
552 runnerServiceCompleteJobHandler.ServeHTTP(w, r)694 runnerServiceCompleteJobHandler.ServeHTTP(w, r)
695+ case RunnerServiceResolveActionsProcedure:
696+ runnerServiceResolveActionsHandler.ServeHTTP(w, r)
697+ case RunnerServiceUploadSummaryProcedure:
698+ runnerServiceUploadSummaryHandler.ServeHTTP(w, r)
553 default:699 default:
554 http.NotFound(w, r)700 http.NotFound(w, r)
555 }701 }
@@ -586,3 +732,11 @@ func (UnimplementedRunnerServiceHandler) WatchJob(context.Context, *connect.Requ
586 func (UnimplementedRunnerServiceHandler) CompleteJob(context.Context, *connect.Request[ermite.CompleteJobRequest]) (*connect.Response[ermite.CompleteJobResponse], error) {732 func (UnimplementedRunnerServiceHandler) CompleteJob(context.Context, *connect.Request[ermite.CompleteJobRequest]) (*connect.Response[ermite.CompleteJobResponse], error) {
587 return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunnerService.CompleteJob is not implemented"))733 return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunnerService.CompleteJob is not implemented"))
588 }734 }
735+
736+func (UnimplementedRunnerServiceHandler) ResolveActions(context.Context, *connect.Request[ermite.ResolveActionsRequest]) (*connect.Response[ermite.ResolveActionsResponse], error) {
737+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunnerService.ResolveActions is not implemented"))
738+}
739+
740+func (UnimplementedRunnerServiceHandler) UploadSummary(context.Context, *connect.Request[ermite.UploadSummaryRequest]) (*connect.Response[ermite.UploadSummaryResponse], error) {
741+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunnerService.UploadSummary is not implemented"))
742+}
M gen/ermite/run.pb.go +537 -132 generated file: collapsed
M gen/ermite/runner.pb.go +829 -78 generated file: collapsed
M gen/ermite/server.pb.go +5 -1
@@ -75,7 +75,11 @@ type GetVersionResponse struct {
7575 // identifiers are "ci.runs" (RunService runs, jobs, logs, checks, and the
7676 // run_updated and job_updated events), "ci.runs.filters" (ListRuns honours
7777 // its filters) and "ci.runners" (RunnerService, and RunService.ListRunners
78- // and RemoveRunner).
78+ // and RemoveRunner). The CI execution identifiers are "ci.actions"
79+ // (RunnerService.ResolveActions and Job.github_token), "ci.annotations"
80+ // (annotations in UpdateJob and CompleteJob, and RunService.ListAnnotations)
81+ // and "ci.summaries" (RunnerService.UploadSummary and
82+ // RunService.GetStepSummaries).
7983 Features []string `protobuf:"bytes,2,rep,name=features,proto3" json:"features,omitempty"`
8084 unknownFields protoimpl.UnknownFields
8185 sizeCache protoimpl.SizeCache
@@ -75,7 +75,11 @@ type GetVersionResponse struct {
75 // identifiers are "ci.runs" (RunService runs, jobs, logs, checks, and the75 // identifiers are "ci.runs" (RunService runs, jobs, logs, checks, and the
76 // run_updated and job_updated events), "ci.runs.filters" (ListRuns honours76 // run_updated and job_updated events), "ci.runs.filters" (ListRuns honours
77 // its filters) and "ci.runners" (RunnerService, and RunService.ListRunners77 // its filters) and "ci.runners" (RunnerService, and RunService.ListRunners
78- // and RemoveRunner).78+ // and RemoveRunner). The CI execution identifiers are "ci.actions"
79+ // (RunnerService.ResolveActions and Job.github_token), "ci.annotations"
80+ // (annotations in UpdateJob and CompleteJob, and RunService.ListAnnotations)
81+ // and "ci.summaries" (RunnerService.UploadSummary and
82+ // RunService.GetStepSummaries).
79 Features []string `protobuf:"bytes,2,rep,name=features,proto3" json:"features,omitempty"`83 Features []string `protobuf:"bytes,2,rep,name=features,proto3" json:"features,omitempty"`
80 unknownFields protoimpl.UnknownFields84 unknownFields protoimpl.UnknownFields
81 sizeCache protoimpl.SizeCache85 sizeCache protoimpl.SizeCache
M gen_test.go +100 -0
@@ -54,10 +54,14 @@ func TestProcedures(t *testing.T) {
5454 {ermiteconnect.RunnerServiceHeartbeatProcedure, "/ermite.RunnerService/Heartbeat"},
5555 {ermiteconnect.RunnerServiceWatchJobProcedure, "/ermite.RunnerService/WatchJob"},
5656 {ermiteconnect.RunnerServiceCompleteJobProcedure, "/ermite.RunnerService/CompleteJob"},
57+ {ermiteconnect.RunnerServiceResolveActionsProcedure, "/ermite.RunnerService/ResolveActions"},
58+ {ermiteconnect.RunnerServiceUploadSummaryProcedure, "/ermite.RunnerService/UploadSummary"},
5759 {ermiteconnect.RunServiceListRunsProcedure, "/ermite.RunService/ListRuns"},
5860 {ermiteconnect.RunServiceGetRunProcedure, "/ermite.RunService/GetRun"},
5961 {ermiteconnect.RunServiceGetJobProcedure, "/ermite.RunService/GetJob"},
6062 {ermiteconnect.RunServiceGetJobLogProcedure, "/ermite.RunService/GetJobLog"},
63+ {ermiteconnect.RunServiceListAnnotationsProcedure, "/ermite.RunService/ListAnnotations"},
64+ {ermiteconnect.RunServiceGetStepSummariesProcedure, "/ermite.RunService/GetStepSummaries"},
6165 {ermiteconnect.RunServiceCancelRunProcedure, "/ermite.RunService/CancelRun"},
6266 {ermiteconnect.RunServiceRerunRunProcedure, "/ermite.RunService/RerunRun"},
6367 {ermiteconnect.RunServiceListChecksProcedure, "/ermite.RunService/ListChecks"},
@@ -120,6 +124,13 @@ func TestEnumNumbers(t *testing.T) {
120124 {"CANCEL_REASON_USER", int32(ermite.CancelReason_CANCEL_REASON_USER), 1},
121125 {"CANCEL_REASON_FAIL_FAST", int32(ermite.CancelReason_CANCEL_REASON_FAIL_FAST), 2},
122126 {"CANCEL_REASON_TIMEOUT", int32(ermite.CancelReason_CANCEL_REASON_TIMEOUT), 3},
127+ {"ANNOTATION_LEVEL_UNSPECIFIED", int32(ermite.AnnotationLevel_ANNOTATION_LEVEL_UNSPECIFIED), 0},
128+ {"ANNOTATION_LEVEL_NOTICE", int32(ermite.AnnotationLevel_ANNOTATION_LEVEL_NOTICE), 1},
129+ {"ANNOTATION_LEVEL_WARNING", int32(ermite.AnnotationLevel_ANNOTATION_LEVEL_WARNING), 2},
130+ {"ANNOTATION_LEVEL_FAILURE", int32(ermite.AnnotationLevel_ANNOTATION_LEVEL_FAILURE), 3},
131+ {"ACTION_SOURCE_UNSPECIFIED", int32(ermite.ActionSource_ACTION_SOURCE_UNSPECIFIED), 0},
132+ {"ACTION_SOURCE_GITHUB", int32(ermite.ActionSource_ACTION_SOURCE_GITHUB), 1},
133+ {"ACTION_SOURCE_ERMITE", int32(ermite.ActionSource_ACTION_SOURCE_ERMITE), 2},
123134 }
124135 for _, e := range enums {
125136 if e.got != e.want {
@@ -350,3 +361,92 @@ func TestRunNameField(t *testing.T) {
350361 t.Errorf("round trip: name %q, workflow_name %q", out.GetName(), out.GetWorkflowName())
351362 }
352363 }
364+
365+// TestCI2FieldNumbers pins the numbers of the fields added for CI-2.
366+func TestCI2FieldNumbers(t *testing.T) {
367+ tests := []struct {
368+ msg proto.Message
369+ fields map[string]int
370+ }{
371+ {&ermite.Job{}, map[string]int{"github_token": 15}},
372+ {&ermite.UpdateJobRequest{}, map[string]int{"annotations": 3}},
373+ {&ermite.CompleteJobRequest{}, map[string]int{"annotations": 6}},
374+ {&ermite.StepUpdate{}, map[string]int{"outcome": 7}},
375+ {&ermite.Step{}, map[string]int{"outcome": 9}},
376+ {&ermite.Annotation{}, map[string]int{
377+ "level": 1, "title": 2, "message": 3, "file": 4, "start_line": 5,
378+ "end_line": 6, "start_column": 7, "end_column": 8, "step_number": 9,
379+ }},
380+ {&ermite.ActionRef{}, map[string]int{"uses": 1}},
381+ {&ermite.ResolveActionsRequest{}, map[string]int{"job_id": 1, "refs": 2}},
382+ {&ermite.ResolveActionsResponse{}, map[string]int{"actions": 1}},
383+ {&ermite.ResolvedAction{}, map[string]int{
384+ "ref": 1, "name_with_owner": 2, "path": 3, "resolved_sha": 4,
385+ "tarball_url": 5, "token": 6, "source": 7, "error": 8,
386+ }},
387+ {&ermite.UploadSummaryRequest{}, map[string]int{"job_id": 1, "step_number": 2, "markdown": 3}},
388+ {&ermite.ListAnnotationsRequest{}, map[string]int{"repo": 1, "run_id": 2, "attempt": 3, "job_id": 4}},
389+ {&ermite.ListAnnotationsResponse{}, map[string]int{"annotations": 1, "truncated": 2}},
390+ {&ermite.JobAnnotation{}, map[string]int{"job_id": 1, "annotation": 2}},
391+ {&ermite.GetStepSummariesRequest{}, map[string]int{"repo": 1, "job_id": 2, "attempt": 3}},
392+ {&ermite.GetStepSummariesResponse{}, map[string]int{"summaries": 1}},
393+ {&ermite.StepSummary{}, map[string]int{"step_number": 1, "markdown": 2}},
394+ }
395+ for _, tt := range tests {
396+ md := tt.msg.ProtoReflect().Descriptor()
397+ for name, num := range tt.fields {
398+ if fd := md.Fields().ByName(protoreflect.Name(name)); fd == nil || int(fd.Number()) != num {
399+ t.Errorf("%s.%s: %v, want number %d", md.Name(), name, fd, num)
400+ }
401+ }
402+ }
403+}
404+
405+// TestCI2RoundTrip checks that the CI-2 messages survive the wire.
406+func TestCI2RoundTrip(t *testing.T) {
407+ msgs := []proto.Message{
408+ &ermite.Job{JobId: "j1", GithubToken: "ermitegh_x"},
409+ &ermite.UpdateJobRequest{
410+ JobId: "j1",
411+ Steps: []*ermite.StepUpdate{{
412+ Number: 2, State: ermite.StepState_STEP_STATE_COMPLETED,
413+ Conclusion: ermite.CiConclusion_CI_CONCLUSION_SUCCESS,
414+ Outcome: ermite.CiConclusion_CI_CONCLUSION_FAILURE,
415+ }},
416+ Annotations: []*ermite.Annotation{{
417+ Level: ermite.AnnotationLevel_ANNOTATION_LEVEL_WARNING, Title: "t", Message: "m",
418+ File: "a.go", StartLine: 1, EndLine: 2, StartColumn: 3, EndColumn: 4, StepNumber: 2,
419+ }},
420+ },
421+ &ermite.CompleteJobRequest{
422+ JobId: "j1",
423+ Conclusion: ermite.CiConclusion_CI_CONCLUSION_SUCCESS,
424+ Annotations: []*ermite.Annotation{{Level: ermite.AnnotationLevel_ANNOTATION_LEVEL_FAILURE, Message: "m", StepNumber: 3}},
425+ },
426+ &ermite.ResolveActionsRequest{JobId: "j1", Refs: []*ermite.ActionRef{{Uses: "actions/checkout@v4"}, {Uses: "o/r/p@main"}}},
427+ &ermite.ResolveActionsResponse{Actions: []*ermite.ResolvedAction{
428+ {
429+ Ref: "o/r/p@main", NameWithOwner: "o/r", Path: "p", ResolvedSha: "0123456789012345678901234567890123456789",
430+ TarballUrl: "https://example.test/o/r.tar.gz", Token: "t", Source: ermite.ActionSource_ACTION_SOURCE_ERMITE,
431+ },
432+ {Ref: "x/y@z", Error: "Unable to resolve action `x/y@z`, unable to find version `z`"},
433+ }},
434+ &ermite.UploadSummaryRequest{JobId: "j1", StepNumber: 2, Markdown: "# hi"},
435+ &ermite.ListAnnotationsResponse{Annotations: []*ermite.JobAnnotation{{JobId: "j1", Annotation: &ermite.Annotation{Message: "m"}}}, Truncated: true},
436+ &ermite.GetStepSummariesResponse{Summaries: []*ermite.StepSummary{{StepNumber: 2, Markdown: "# hi"}}},
437+ &ermite.RunJob{Id: "j1", Steps: []*ermite.Step{{Number: 2, Outcome: ermite.CiConclusion_CI_CONCLUSION_FAILURE}}},
438+ }
439+ for _, in := range msgs {
440+ b, err := proto.Marshal(in)
441+ if err != nil {
442+ t.Fatal(err)
443+ }
444+ out := in.ProtoReflect().New().Interface()
445+ if err := proto.Unmarshal(b, out); err != nil {
446+ t.Fatal(err)
447+ }
448+ if !proto.Equal(in, out) {
449+ t.Errorf("%T: round trip mismatch: %v != %v", in, out, in)
450+ }
451+ }
452+}
@@ -54,10 +54,14 @@ func TestProcedures(t *testing.T) {
54 {ermiteconnect.RunnerServiceHeartbeatProcedure, "/ermite.RunnerService/Heartbeat"},54 {ermiteconnect.RunnerServiceHeartbeatProcedure, "/ermite.RunnerService/Heartbeat"},
55 {ermiteconnect.RunnerServiceWatchJobProcedure, "/ermite.RunnerService/WatchJob"},55 {ermiteconnect.RunnerServiceWatchJobProcedure, "/ermite.RunnerService/WatchJob"},
56 {ermiteconnect.RunnerServiceCompleteJobProcedure, "/ermite.RunnerService/CompleteJob"},56 {ermiteconnect.RunnerServiceCompleteJobProcedure, "/ermite.RunnerService/CompleteJob"},
57+ {ermiteconnect.RunnerServiceResolveActionsProcedure, "/ermite.RunnerService/ResolveActions"},
58+ {ermiteconnect.RunnerServiceUploadSummaryProcedure, "/ermite.RunnerService/UploadSummary"},
57 {ermiteconnect.RunServiceListRunsProcedure, "/ermite.RunService/ListRuns"},59 {ermiteconnect.RunServiceListRunsProcedure, "/ermite.RunService/ListRuns"},
58 {ermiteconnect.RunServiceGetRunProcedure, "/ermite.RunService/GetRun"},60 {ermiteconnect.RunServiceGetRunProcedure, "/ermite.RunService/GetRun"},
59 {ermiteconnect.RunServiceGetJobProcedure, "/ermite.RunService/GetJob"},61 {ermiteconnect.RunServiceGetJobProcedure, "/ermite.RunService/GetJob"},
60 {ermiteconnect.RunServiceGetJobLogProcedure, "/ermite.RunService/GetJobLog"},62 {ermiteconnect.RunServiceGetJobLogProcedure, "/ermite.RunService/GetJobLog"},
63+ {ermiteconnect.RunServiceListAnnotationsProcedure, "/ermite.RunService/ListAnnotations"},
64+ {ermiteconnect.RunServiceGetStepSummariesProcedure, "/ermite.RunService/GetStepSummaries"},
61 {ermiteconnect.RunServiceCancelRunProcedure, "/ermite.RunService/CancelRun"},65 {ermiteconnect.RunServiceCancelRunProcedure, "/ermite.RunService/CancelRun"},
62 {ermiteconnect.RunServiceRerunRunProcedure, "/ermite.RunService/RerunRun"},66 {ermiteconnect.RunServiceRerunRunProcedure, "/ermite.RunService/RerunRun"},
63 {ermiteconnect.RunServiceListChecksProcedure, "/ermite.RunService/ListChecks"},67 {ermiteconnect.RunServiceListChecksProcedure, "/ermite.RunService/ListChecks"},
@@ -120,6 +124,13 @@ func TestEnumNumbers(t *testing.T) {
120 {"CANCEL_REASON_USER", int32(ermite.CancelReason_CANCEL_REASON_USER), 1},124 {"CANCEL_REASON_USER", int32(ermite.CancelReason_CANCEL_REASON_USER), 1},
121 {"CANCEL_REASON_FAIL_FAST", int32(ermite.CancelReason_CANCEL_REASON_FAIL_FAST), 2},125 {"CANCEL_REASON_FAIL_FAST", int32(ermite.CancelReason_CANCEL_REASON_FAIL_FAST), 2},
122 {"CANCEL_REASON_TIMEOUT", int32(ermite.CancelReason_CANCEL_REASON_TIMEOUT), 3},126 {"CANCEL_REASON_TIMEOUT", int32(ermite.CancelReason_CANCEL_REASON_TIMEOUT), 3},
127+ {"ANNOTATION_LEVEL_UNSPECIFIED", int32(ermite.AnnotationLevel_ANNOTATION_LEVEL_UNSPECIFIED), 0},
128+ {"ANNOTATION_LEVEL_NOTICE", int32(ermite.AnnotationLevel_ANNOTATION_LEVEL_NOTICE), 1},
129+ {"ANNOTATION_LEVEL_WARNING", int32(ermite.AnnotationLevel_ANNOTATION_LEVEL_WARNING), 2},
130+ {"ANNOTATION_LEVEL_FAILURE", int32(ermite.AnnotationLevel_ANNOTATION_LEVEL_FAILURE), 3},
131+ {"ACTION_SOURCE_UNSPECIFIED", int32(ermite.ActionSource_ACTION_SOURCE_UNSPECIFIED), 0},
132+ {"ACTION_SOURCE_GITHUB", int32(ermite.ActionSource_ACTION_SOURCE_GITHUB), 1},
133+ {"ACTION_SOURCE_ERMITE", int32(ermite.ActionSource_ACTION_SOURCE_ERMITE), 2},
123 }134 }
124 for _, e := range enums {135 for _, e := range enums {
125 if e.got != e.want {136 if e.got != e.want {
@@ -350,3 +361,92 @@ func TestRunNameField(t *testing.T) {
350 t.Errorf("round trip: name %q, workflow_name %q", out.GetName(), out.GetWorkflowName())361 t.Errorf("round trip: name %q, workflow_name %q", out.GetName(), out.GetWorkflowName())
351 }362 }
352 }363 }
364+
365+// TestCI2FieldNumbers pins the numbers of the fields added for CI-2.
366+func TestCI2FieldNumbers(t *testing.T) {
367+ tests := []struct {
368+ msg proto.Message
369+ fields map[string]int
370+ }{
371+ {&ermite.Job{}, map[string]int{"github_token": 15}},
372+ {&ermite.UpdateJobRequest{}, map[string]int{"annotations": 3}},
373+ {&ermite.CompleteJobRequest{}, map[string]int{"annotations": 6}},
374+ {&ermite.StepUpdate{}, map[string]int{"outcome": 7}},
375+ {&ermite.Step{}, map[string]int{"outcome": 9}},
376+ {&ermite.Annotation{}, map[string]int{
377+ "level": 1, "title": 2, "message": 3, "file": 4, "start_line": 5,
378+ "end_line": 6, "start_column": 7, "end_column": 8, "step_number": 9,
379+ }},
380+ {&ermite.ActionRef{}, map[string]int{"uses": 1}},
381+ {&ermite.ResolveActionsRequest{}, map[string]int{"job_id": 1, "refs": 2}},
382+ {&ermite.ResolveActionsResponse{}, map[string]int{"actions": 1}},
383+ {&ermite.ResolvedAction{}, map[string]int{
384+ "ref": 1, "name_with_owner": 2, "path": 3, "resolved_sha": 4,
385+ "tarball_url": 5, "token": 6, "source": 7, "error": 8,
386+ }},
387+ {&ermite.UploadSummaryRequest{}, map[string]int{"job_id": 1, "step_number": 2, "markdown": 3}},
388+ {&ermite.ListAnnotationsRequest{}, map[string]int{"repo": 1, "run_id": 2, "attempt": 3, "job_id": 4}},
389+ {&ermite.ListAnnotationsResponse{}, map[string]int{"annotations": 1, "truncated": 2}},
390+ {&ermite.JobAnnotation{}, map[string]int{"job_id": 1, "annotation": 2}},
391+ {&ermite.GetStepSummariesRequest{}, map[string]int{"repo": 1, "job_id": 2, "attempt": 3}},
392+ {&ermite.GetStepSummariesResponse{}, map[string]int{"summaries": 1}},
393+ {&ermite.StepSummary{}, map[string]int{"step_number": 1, "markdown": 2}},
394+ }
395+ for _, tt := range tests {
396+ md := tt.msg.ProtoReflect().Descriptor()
397+ for name, num := range tt.fields {
398+ if fd := md.Fields().ByName(protoreflect.Name(name)); fd == nil || int(fd.Number()) != num {
399+ t.Errorf("%s.%s: %v, want number %d", md.Name(), name, fd, num)
400+ }
401+ }
402+ }
403+}
404+
405+// TestCI2RoundTrip checks that the CI-2 messages survive the wire.
406+func TestCI2RoundTrip(t *testing.T) {
407+ msgs := []proto.Message{
408+ &ermite.Job{JobId: "j1", GithubToken: "ermitegh_x"},
409+ &ermite.UpdateJobRequest{
410+ JobId: "j1",
411+ Steps: []*ermite.StepUpdate{{
412+ Number: 2, State: ermite.StepState_STEP_STATE_COMPLETED,
413+ Conclusion: ermite.CiConclusion_CI_CONCLUSION_SUCCESS,
414+ Outcome: ermite.CiConclusion_CI_CONCLUSION_FAILURE,
415+ }},
416+ Annotations: []*ermite.Annotation{{
417+ Level: ermite.AnnotationLevel_ANNOTATION_LEVEL_WARNING, Title: "t", Message: "m",
418+ File: "a.go", StartLine: 1, EndLine: 2, StartColumn: 3, EndColumn: 4, StepNumber: 2,
419+ }},
420+ },
421+ &ermite.CompleteJobRequest{
422+ JobId: "j1",
423+ Conclusion: ermite.CiConclusion_CI_CONCLUSION_SUCCESS,
424+ Annotations: []*ermite.Annotation{{Level: ermite.AnnotationLevel_ANNOTATION_LEVEL_FAILURE, Message: "m", StepNumber: 3}},
425+ },
426+ &ermite.ResolveActionsRequest{JobId: "j1", Refs: []*ermite.ActionRef{{Uses: "actions/checkout@v4"}, {Uses: "o/r/p@main"}}},
427+ &ermite.ResolveActionsResponse{Actions: []*ermite.ResolvedAction{
428+ {
429+ Ref: "o/r/p@main", NameWithOwner: "o/r", Path: "p", ResolvedSha: "0123456789012345678901234567890123456789",
430+ TarballUrl: "https://example.test/o/r.tar.gz", Token: "t", Source: ermite.ActionSource_ACTION_SOURCE_ERMITE,
431+ },
432+ {Ref: "x/y@z", Error: "Unable to resolve action `x/y@z`, unable to find version `z`"},
433+ }},
434+ &ermite.UploadSummaryRequest{JobId: "j1", StepNumber: 2, Markdown: "# hi"},
435+ &ermite.ListAnnotationsResponse{Annotations: []*ermite.JobAnnotation{{JobId: "j1", Annotation: &ermite.Annotation{Message: "m"}}}, Truncated: true},
436+ &ermite.GetStepSummariesResponse{Summaries: []*ermite.StepSummary{{StepNumber: 2, Markdown: "# hi"}}},
437+ &ermite.RunJob{Id: "j1", Steps: []*ermite.Step{{Number: 2, Outcome: ermite.CiConclusion_CI_CONCLUSION_FAILURE}}},
438+ }
439+ for _, in := range msgs {
440+ b, err := proto.Marshal(in)
441+ if err != nil {
442+ t.Fatal(err)
443+ }
444+ out := in.ProtoReflect().New().Interface()
445+ if err := proto.Unmarshal(b, out); err != nil {
446+ t.Fatal(err)
447+ }
448+ if !proto.Equal(in, out) {
449+ t.Errorf("%T: round trip mismatch: %v != %v", in, out, in)
450+ }
451+ }
452+}
M proto/ermite/run.proto +84 -5
@@ -172,6 +172,11 @@ message Step {
172172 // IN_PROGRESS step it is the number of lines held at the time of the
173173 // call and grows; for a step without a line it equals log_start.
174174 int64 log_end = 8;
175+ // Outcome is how the step ended before continue-on-error was applied, as
176+ // StepUpdate.outcome: it differs from conclusion only for a failed step with
177+ // continue-on-error. Unset until state is COMPLETED, and for a step stored
178+ // by a server or runner before this field existed (read it as conclusion).
179+ CiConclusion outcome = 9;
175180 }
176181
177182 // Check is the result of one job on one commit, the unit shown next to a
@@ -238,9 +243,9 @@ message Runner {
238243 // RunService reads and controls CI runs, like `gh run` does on GitHub. It is for
239244 // owners and their tools; runners use RunnerService.
240245 //
241-// Runs, jobs, checks and logs belong to a repository, and reading them needs
242-// the same access as reading it: nothing for a public repository, the
243-// repo:read scope for a private one. A repository that does not exist or that
246+// Runs, jobs, checks, logs, annotations and summaries belong to a repository,
247+// and reading them needs the same access as reading it: nothing for a public
248+// repository, the repo:read scope for a private one. A repository that does not exist or that
244249 // the caller cannot read gives not_found; so does a run or a job that belongs
245250 // to another repository than the one named (ids are global but are always
246251 // checked against the repository). Cancelling and re-running need the ci:write
@@ -255,10 +260,13 @@ message Runner {
255260 // page_token is the next_page_token of a previous response for the same
256261 // request (other fields must be unchanged).
257262 //
258-// Servers that implement the runs side (every RPC but ListRunners and
259-// RemoveRunner) list "ci.runs" in GetVersionResponse.features; those that
263+// Servers that implement the runs side (every RPC but ListRunners,
264+// RemoveRunner, ListAnnotations and GetStepSummaries) list "ci.runs" in
265+// GetVersionResponse.features; those that
260266 // implement the runners side list "ci.runners", as RunnerService does. A server
261267 // that honours the filters of ListRunsRequest also lists "ci.runs.filters".
268+// ListAnnotations needs "ci.annotations" and GetStepSummaries "ci.summaries",
269+// which are also the features of the runner side of each.
262270 service RunService {
263271 // ListRuns lists the runs of a repository, highest id (newest) first. Each
264272 // run is described by its latest attempt. The filters narrow the result; a
@@ -273,6 +281,21 @@ service RunService {
273281 // GetJob returns one job with its steps. Fails with not_found if the job
274282 // or the attempt does not exist.
275283 rpc GetJob(GetJobRequest) returns (GetJobResponse);
284+ // ListAnnotations lists the annotations of a run attempt, optionally of one
285+ // job, ordered by job (as in GetRun), step number, then the order in which
286+ // the step produced them. Needs the same access as GetRun. Returns at most
287+ // 1,000 annotations (truncated tells). A run without annotations gives an
288+ // empty list. Only servers with the feature "ci.annotations" implement it
289+ // (unimplemented otherwise). Fails with not_found if the run, the attempt or
290+ // the job does not exist, or if the job is not a job of the run.
291+ rpc ListAnnotations(ListAnnotationsRequest) returns (ListAnnotationsResponse);
292+ // GetStepSummaries returns the job summaries (the Markdown written to
293+ // GITHUB_STEP_SUMMARY) of the steps of a job that have one, ordered by step
294+ // number. Needs the same access as GetJob. Only servers with the feature
295+ // "ci.summaries" implement it (unimplemented otherwise). A job without
296+ // summaries gives an empty list. Fails with not_found if the job or the
297+ // attempt does not exist.
298+ rpc GetStepSummaries(GetStepSummariesRequest) returns (GetStepSummariesResponse);
276299 // GetJobLog streams the lines of the log of a job, from a line number. The
277300 // server sends them in order in messages of at most 1,000 lines and 64 KiB of
278301 // text each.
@@ -393,6 +416,62 @@ message GetJobResponse {
393416 RunJob job = 1;
394417 }
395418
419+// ListAnnotationsRequest selects the annotations of a run.
420+message ListAnnotationsRequest {
421+ // Repo is the repository name.
422+ string repo = 1;
423+ // RunId is the Run.id.
424+ int64 run_id = 2;
425+ // Attempt is the attempt to read. 0 means the latest.
426+ int32 attempt = 3;
427+ // JobId restricts the result to one job (RunJob.id). Empty means every job
428+ // of the run.
429+ string job_id = 4;
430+}
431+
432+// ListAnnotationsResponse holds the annotations of a run.
433+message ListAnnotationsResponse {
434+ // Annotations are the annotations, at most 1,000.
435+ repeated JobAnnotation annotations = 1;
436+ // Truncated is true when there are more annotations than annotations holds.
437+ bool truncated = 2;
438+}
439+
440+// JobAnnotation is an annotation with the job it belongs to.
441+message JobAnnotation {
442+ // JobId is the RunJob.id of the job.
443+ string job_id = 1;
444+ // Annotation is the annotation as the runner sent it, after the server
445+ // checked it. Its step_number is the step of the job.
446+ Annotation annotation = 2;
447+}
448+
449+// GetStepSummariesRequest selects a job.
450+message GetStepSummariesRequest {
451+ // Repo is the repository name.
452+ string repo = 1;
453+ // JobId is the RunJob.id.
454+ string job_id = 2;
455+ // Attempt is the RunJob.attempt to read. 0 means the latest.
456+ int32 attempt = 3;
457+}
458+
459+// GetStepSummariesResponse holds the summaries of a job.
460+message GetStepSummariesResponse {
461+ // Summaries are the summaries of the steps that have one, ordered by step
462+ // number.
463+ repeated StepSummary summaries = 1;
464+}
465+
466+// StepSummary is the job summary of one step.
467+message StepSummary {
468+ // StepNumber is the number of the step (see Step.number).
469+ int32 step_number = 1;
470+ // Markdown is the summary as uploaded, at most 1 MiB. Clients render it
471+ // safely: it is untrusted content (raw HTML and script URLs must not run).
472+ string markdown = 2;
473+}
474+
396475 // GetJobLogRequest selects the lines to stream.
397476 message GetJobLogRequest {
398477 // Repo is the repository name.
@@ -172,6 +172,11 @@ message Step {
172 // IN_PROGRESS step it is the number of lines held at the time of the172 // IN_PROGRESS step it is the number of lines held at the time of the
173 // call and grows; for a step without a line it equals log_start.173 // call and grows; for a step without a line it equals log_start.
174 int64 log_end = 8;174 int64 log_end = 8;
175+ // Outcome is how the step ended before continue-on-error was applied, as
176+ // StepUpdate.outcome: it differs from conclusion only for a failed step with
177+ // continue-on-error. Unset until state is COMPLETED, and for a step stored
178+ // by a server or runner before this field existed (read it as conclusion).
179+ CiConclusion outcome = 9;
175 }180 }
176 181
177 // Check is the result of one job on one commit, the unit shown next to a182 // Check is the result of one job on one commit, the unit shown next to a
@@ -238,9 +243,9 @@ message Runner {
238 // RunService reads and controls CI runs, like `gh run` does on GitHub. It is for243 // RunService reads and controls CI runs, like `gh run` does on GitHub. It is for
239 // owners and their tools; runners use RunnerService.244 // owners and their tools; runners use RunnerService.
240 //245 //
241-// Runs, jobs, checks and logs belong to a repository, and reading them needs246+// Runs, jobs, checks, logs, annotations and summaries belong to a repository,
242-// the same access as reading it: nothing for a public repository, the247+// and reading them needs the same access as reading it: nothing for a public
243-// repo:read scope for a private one. A repository that does not exist or that248+// repository, the repo:read scope for a private one. A repository that does not exist or that
244 // the caller cannot read gives not_found; so does a run or a job that belongs249 // the caller cannot read gives not_found; so does a run or a job that belongs
245 // to another repository than the one named (ids are global but are always250 // to another repository than the one named (ids are global but are always
246 // checked against the repository). Cancelling and re-running need the ci:write251 // checked against the repository). Cancelling and re-running need the ci:write
@@ -255,10 +260,13 @@ message Runner {
255 // page_token is the next_page_token of a previous response for the same260 // page_token is the next_page_token of a previous response for the same
256 // request (other fields must be unchanged).261 // request (other fields must be unchanged).
257 //262 //
258-// Servers that implement the runs side (every RPC but ListRunners and263+// Servers that implement the runs side (every RPC but ListRunners,
259-// RemoveRunner) list "ci.runs" in GetVersionResponse.features; those that264+// RemoveRunner, ListAnnotations and GetStepSummaries) list "ci.runs" in
265+// GetVersionResponse.features; those that
260 // implement the runners side list "ci.runners", as RunnerService does. A server266 // implement the runners side list "ci.runners", as RunnerService does. A server
261 // that honours the filters of ListRunsRequest also lists "ci.runs.filters".267 // that honours the filters of ListRunsRequest also lists "ci.runs.filters".
268+// ListAnnotations needs "ci.annotations" and GetStepSummaries "ci.summaries",
269+// which are also the features of the runner side of each.
262 service RunService {270 service RunService {
263 // ListRuns lists the runs of a repository, highest id (newest) first. Each271 // ListRuns lists the runs of a repository, highest id (newest) first. Each
264 // run is described by its latest attempt. The filters narrow the result; a272 // run is described by its latest attempt. The filters narrow the result; a
@@ -273,6 +281,21 @@ service RunService {
273 // GetJob returns one job with its steps. Fails with not_found if the job281 // GetJob returns one job with its steps. Fails with not_found if the job
274 // or the attempt does not exist.282 // or the attempt does not exist.
275 rpc GetJob(GetJobRequest) returns (GetJobResponse);283 rpc GetJob(GetJobRequest) returns (GetJobResponse);
284+ // ListAnnotations lists the annotations of a run attempt, optionally of one
285+ // job, ordered by job (as in GetRun), step number, then the order in which
286+ // the step produced them. Needs the same access as GetRun. Returns at most
287+ // 1,000 annotations (truncated tells). A run without annotations gives an
288+ // empty list. Only servers with the feature "ci.annotations" implement it
289+ // (unimplemented otherwise). Fails with not_found if the run, the attempt or
290+ // the job does not exist, or if the job is not a job of the run.
291+ rpc ListAnnotations(ListAnnotationsRequest) returns (ListAnnotationsResponse);
292+ // GetStepSummaries returns the job summaries (the Markdown written to
293+ // GITHUB_STEP_SUMMARY) of the steps of a job that have one, ordered by step
294+ // number. Needs the same access as GetJob. Only servers with the feature
295+ // "ci.summaries" implement it (unimplemented otherwise). A job without
296+ // summaries gives an empty list. Fails with not_found if the job or the
297+ // attempt does not exist.
298+ rpc GetStepSummaries(GetStepSummariesRequest) returns (GetStepSummariesResponse);
276 // GetJobLog streams the lines of the log of a job, from a line number. The299 // GetJobLog streams the lines of the log of a job, from a line number. The
277 // server sends them in order in messages of at most 1,000 lines and 64 KiB of300 // server sends them in order in messages of at most 1,000 lines and 64 KiB of
278 // text each.301 // text each.
@@ -393,6 +416,62 @@ message GetJobResponse {
393 RunJob job = 1;416 RunJob job = 1;
394 }417 }
395 418
419+// ListAnnotationsRequest selects the annotations of a run.
420+message ListAnnotationsRequest {
421+ // Repo is the repository name.
422+ string repo = 1;
423+ // RunId is the Run.id.
424+ int64 run_id = 2;
425+ // Attempt is the attempt to read. 0 means the latest.
426+ int32 attempt = 3;
427+ // JobId restricts the result to one job (RunJob.id). Empty means every job
428+ // of the run.
429+ string job_id = 4;
430+}
431+
432+// ListAnnotationsResponse holds the annotations of a run.
433+message ListAnnotationsResponse {
434+ // Annotations are the annotations, at most 1,000.
435+ repeated JobAnnotation annotations = 1;
436+ // Truncated is true when there are more annotations than annotations holds.
437+ bool truncated = 2;
438+}
439+
440+// JobAnnotation is an annotation with the job it belongs to.
441+message JobAnnotation {
442+ // JobId is the RunJob.id of the job.
443+ string job_id = 1;
444+ // Annotation is the annotation as the runner sent it, after the server
445+ // checked it. Its step_number is the step of the job.
446+ Annotation annotation = 2;
447+}
448+
449+// GetStepSummariesRequest selects a job.
450+message GetStepSummariesRequest {
451+ // Repo is the repository name.
452+ string repo = 1;
453+ // JobId is the RunJob.id.
454+ string job_id = 2;
455+ // Attempt is the RunJob.attempt to read. 0 means the latest.
456+ int32 attempt = 3;
457+}
458+
459+// GetStepSummariesResponse holds the summaries of a job.
460+message GetStepSummariesResponse {
461+ // Summaries are the summaries of the steps that have one, ordered by step
462+ // number.
463+ repeated StepSummary summaries = 1;
464+}
465+
466+// StepSummary is the job summary of one step.
467+message StepSummary {
468+ // StepNumber is the number of the step (see Step.number).
469+ int32 step_number = 1;
470+ // Markdown is the summary as uploaded, at most 1 MiB. Clients render it
471+ // safely: it is untrusted content (raw HTML and script URLs must not run).
472+ string markdown = 2;
473+}
474+
396 // GetJobLogRequest selects the lines to stream.475 // GetJobLogRequest selects the lines to stream.
397 message GetJobLogRequest {476 message GetJobLogRequest {
398 // Repo is the repository name.477 // Repo is the repository name.
M proto/ermite/runner.proto +233 -12
@@ -93,6 +93,32 @@ enum CancelReason {
9393 CANCEL_REASON_TIMEOUT = 3;
9494 }
9595
96+// AnnotationLevel is the severity of an annotation, with the values of
97+// GitHub's check run annotation levels ("notice", "warning", "failure"). The
98+// workflow commands ::notice, ::warning and ::error produce NOTICE, WARNING and
99+// FAILURE.
100+enum AnnotationLevel {
101+ // Not set. Rejected in requests, never returned by the server.
102+ ANNOTATION_LEVEL_UNSPECIFIED = 0;
103+ // Information: ::notice.
104+ ANNOTATION_LEVEL_NOTICE = 1;
105+ // A problem that does not fail the job by itself: ::warning.
106+ ANNOTATION_LEVEL_WARNING = 2;
107+ // An error: ::error.
108+ ANNOTATION_LEVEL_FAILURE = 3;
109+}
110+
111+// ActionSource says where the server found an action.
112+enum ActionSource {
113+ // Not set. Only in a ResolvedAction that carries an error.
114+ ACTION_SOURCE_UNSPECIFIED = 0;
115+ // github.com: the archive is downloaded from GitHub, without a token.
116+ ACTION_SOURCE_GITHUB = 1;
117+ // A repository of this server, which wins over a github.com repository of
118+ // the same name (as GitHub Enterprise Server's own actions do).
119+ ACTION_SOURCE_ERMITE = 2;
120+}
121+
96122 // RunnerService is what a CI runner (ermite-runner) calls to get jobs and
97123 // report on them. It is not for people: owners use RunService.
98124 //
@@ -104,8 +130,8 @@ enum CancelReason {
104130 // - FetchJob: the runner token returned by RegisterRunner. It identifies the
105131 // runner and only works for FetchJob. It is "ermiterun_" followed by 43
106132 // base64url characters (32 random bytes, no padding).
107-// - UpdateJob, AppendLog, Heartbeat, WatchJob, CompleteJob: the job token of
108-// the Job that FetchJob returned. It is valid for that one job and that
133+// - UpdateJob, AppendLog, Heartbeat, WatchJob, CompleteJob, ResolveActions,
134+// UploadSummary: the job token of the Job that FetchJob returned. It is valid for that one job and that
109135 // one assignment. It is "ermitejob_" followed by 43 base64url characters
110136 // (32 random bytes, no padding). The job_id of the request must be the
111137 // one of the token.
@@ -120,8 +146,8 @@ enum CancelReason {
120146 //
121147 // The end of a job. A job ends when its conclusion is set, by CompleteJob or
122148 // by the server (cancellation not honoured, lost runner, log limit, queue
123-// timeout). From then on UpdateJob, AppendLog, Heartbeat and WatchJob answer
124-// failed_precondition with a JobEnded error detail (a JobEnded message in the
149+// timeout). From then on UpdateJob, AppendLog, Heartbeat, WatchJob,
150+// ResolveActions and UploadSummary answer failed_precondition with a JobEnded error detail (a JobEnded message in the
125151 // Connect error details) that gives the conclusion; the runner stops the job
126152 // and sends nothing more. The job token is still recognised for 24 hours after
127153 // the end, so that these calls can say so and so that CompleteJob, the only
@@ -132,14 +158,22 @@ enum CancelReason {
132158 // Secrets and trust. Runner tokens and job tokens are secrets: the server
133159 // never logs them and clients must not log them. The job token stays in the
134160 // runner host process; it is never given to the sandbox that runs the job's
135-// code. A runner receives the jobs of every repository, with their workflow
161+// code. The job's repository token (Job.github_token) is another secret: it is
162+// the one the sandbox does get, and it only reads the repository of the run.
163+// A runner receives the jobs of every repository, with their workflow
136164 // files, contexts and (from CI-2) their code, and from CI-3 their secrets: the
137165 // runner:register scope therefore effectively gives read access to the code
138166 // and the secrets of every repository, private ones included. Give it only to
139167 // runners you operate.
140168 //
141169 // Servers that implement this service list "ci.runners" in
142-// GetVersionResponse.features.
170+// GetVersionResponse.features. Those that implement ResolveActions and
171+// Job.github_token also list "ci.actions", those that accept annotations
172+// (UpdateJobRequest.annotations, CompleteJobRequest.annotations) list
173+// "ci.annotations", and those that implement UploadSummary list
174+// "ci.summaries". A server without a feature ignores the fields of that
175+// feature (protobuf drops unknown fields) and answers unimplemented to its
176+// RPCs; a runner that needs one checks GetVersion first.
143177 service RunnerService {
144178 // RegisterRunner registers a runner and returns its runner token, which is
145179 // shown only in this response; the runner keeps it. The caller needs the
@@ -191,10 +225,16 @@ service RunnerService {
191225 // identical call changes nothing, so a runner may retry after a network
192226 // error. To end the job use CompleteJob.
193227 //
194- // Fails with invalid_argument if a rule is broken, with failed_precondition
195- // if a step goes back (a COMPLETED step cannot change, a step cannot go from
196- // IN_PROGRESS to PENDING) and with failed_precondition and a JobEnded detail
197- // if the job has ended.
228+ // Annotations are applied with the steps, in the same atomic step: for each
229+ // step_number present in annotations, the annotations of that step stored
230+ // so far are replaced by the ones of the request, so a retry never
231+ // duplicates them. A step number absent from the request keeps its
232+ // annotations.
233+ //
234+ // Fails with invalid_argument if a rule is broken (including the annotation
235+ // limits), with failed_precondition if a step goes back (a COMPLETED step
236+ // cannot change, a step cannot go from IN_PROGRESS to PENDING) and with
237+ // failed_precondition and a JobEnded detail if the job has ended.
198238 rpc UpdateJob(UpdateJobRequest) returns (UpdateJobResponse);
199239 // AppendLog appends lines to the log of a job. Needs the job token.
200240 //
@@ -299,9 +339,53 @@ service RunnerService {
299339 // ends are completed by the server as CI_CONCLUSION_CANCELLED (or SKIPPED when
300340 // they never started and the job did not end by cancellation).
301341 //
302- // Fails with invalid_argument if a rule is broken (including the output
303- // limits) and failed_precondition if a step goes back, as in UpdateJob.
342+ // Annotations are applied as in UpdateJob, together with the conclusion.
343+ // Summaries are not part of this call: a runner sends them with
344+ // UploadSummary before it.
345+ //
346+ // Fails with invalid_argument if a rule is broken (including the output and
347+ // annotation limits) and failed_precondition if a step goes back, as in
348+ // UpdateJob.
304349 rpc CompleteJob(CompleteJobRequest) returns (CompleteJobResponse);
350+ // ResolveActions turns the "uses" of workflow and action steps into what the
351+ // runner needs to download the actions, as GitHub's action download info
352+ // does. Needs the job token; the job_id must be the one of the token. It
353+ // may be called as often as needed while the job runs (the runner calls it
354+ // once per level of composite actions, see the runner's depth limit of 9).
355+ // The runner then downloads each archive itself, from the sandbox, with the
356+ // token of the ResolvedAction when there is one.
357+ //
358+ // The response has one ResolvedAction per ActionRef, in the same order. A
359+ // reference that cannot be resolved (unknown repository, tag or branch, a
360+ // repository without access, a "uses" that GitHub itself would reject) does
361+ // not fail the call: its ResolvedAction has only ref and error, where error
362+ // is the message GitHub gives for the same failure, which the runner prints
363+ // as is. Only a failure of the call itself (credential, ended job, bad
364+ // request, server error) is an RPC error; the runner then fails the job.
365+ //
366+ // Resolution is stable for a run: the same ref resolves to the same sha for
367+ // every job and every call of one attempt of the run. A re-run attempt
368+ // resolves again.
369+ //
370+ // Fails with unauthenticated or permission_denied as the other job RPCs,
371+ // with failed_precondition and a JobEnded detail if the job has ended, and
372+ // with invalid_argument if there are more than 100 refs or if a "uses" is a
373+ // local action ("./path") or a Docker image ("docker://"), which the runner
374+ // handles without the server.
375+ rpc ResolveActions(ResolveActionsRequest) returns (ResolveActionsResponse);
376+ // UploadSummary stores the job summary of one step: the Markdown that the
377+ // step wrote to the file named by GITHUB_STEP_SUMMARY. Needs the job token.
378+ // The runner masks it before sending it, and sends it after the step ended
379+ // and before CompleteJob.
380+ //
381+ // It is idempotent: the summary of a step is replaced by each call, so a
382+ // retry changes nothing, and an empty markdown removes the summary of the
383+ // step. The Markdown is at most 1 MiB (1,048,576 bytes) and valid UTF-8;
384+ // clients set a message size limit above it.
385+ //
386+ // Fails with invalid_argument if a rule is broken (size, UTF-8, step number)
387+ // and with failed_precondition and a JobEnded detail if the job has ended.
388+ rpc UploadSummary(UploadSummaryRequest) returns (UploadSummaryResponse);
305389 }
306390
307391 // RegisterRunnerRequest describes the runner to register.
@@ -437,6 +521,18 @@ message Job {
437521 // Ref is the full ref name that triggered the run, for example
438522 // "refs/heads/main" or "refs/tags/v1.0.0".
439523 string ref = 14;
524+ // GithubToken is the job repository token, the "github.token" and
525+ // "secrets.GITHUB_TOKEN" of the job: "ermitegh_" and 43 base64url characters
526+ // (32 random bytes, no padding). Unlike job_token it is meant for the code of
527+ // the job: the runner gives it to the sandbox (actions/checkout clones with
528+ // it). It can only read the repository of the run (read-only, whatever
529+ // "permissions:" says), and is valid from the assignment until the job ends
530+ // (it is revoked then, on cancellation and when the job is lost). It is a
531+ // secret: the runner registers it in its masker before anything runs, and
532+ // neither the runner nor the server logs it. It is shown only in this
533+ // response, or in a repeat of the same poll_id; a new assignment of the same
534+ // job has a new one. Empty from servers without the feature "ci.actions".
535+ string github_token = 15;
440536 }
441537
442538 // UpdateJobRequest reports the steps of a running job.
@@ -449,6 +545,12 @@ message UpdateJobRequest {
449545 // implicit "Set up job" is 1, the steps of the workflow follow in order, and
450546 // the implicit "Complete job" is last.
451547 repeated StepUpdate steps = 2;
548+ // Annotations are the annotations of the steps that the call names: for
549+ // each step_number present, all the annotations of that step known so far,
550+ // not only the new ones (see the rules on UpdateJob). At most 10 per level
551+ // and per step; more is invalid_argument. The server ignores them without
552+ // the feature "ci.annotations".
553+ repeated Annotation annotations = 3;
452554 }
453555
454556 // StepUpdate is the state of one step as the runner knows it.
@@ -472,6 +574,14 @@ message StepUpdate {
472574 // CompletedAt is when the step ended, by the clock of the runner. Required
473575 // when state is COMPLETED.
474576 google.protobuf.Timestamp completed_at = 6;
577+ // Outcome is how the step ended before continue-on-error was applied, GitHub's
578+ // "steps.<id>.outcome", whereas conclusion is the result after it
579+ // ("steps.<id>.conclusion"). Same values as conclusion. They differ only for
580+ // a step with continue-on-error that failed: outcome FAILURE, conclusion
581+ // SUCCESS. Set by the runner when state is COMPLETED and UNSPECIFIED
582+ // otherwise; UNSPECIFIED in a COMPLETED step means the same as conclusion
583+ // (a runner that does not know the field).
584+ CiConclusion outcome = 7;
475585 }
476586
477587 // UpdateJobResponse is empty.
@@ -504,6 +614,11 @@ message CompleteJobRequest {
504614 // Steps are the final steps, with the same meaning as UpdateJobRequest.steps,
505615 // applied in the same atomic step as the conclusion.
506616 repeated StepUpdate steps = 5;
617+ // Annotations are those of the steps that changed since the last call, with
618+ // the rules of UpdateJobRequest.annotations (all the annotations of each
619+ // step named). The server ignores them without the feature
620+ // "ci.annotations".
621+ repeated Annotation annotations = 6;
507622 }
508623
509624 // CompleteJobResponse is the stored result of a job.
@@ -620,3 +735,109 @@ message CancelRequest {
620735 // server.
621736 google.protobuf.Timestamp requested_at = 3;
622737 }
738+
739+// Annotation is a message attached to a step, and optionally to a place in a
740+// file of the repository, like an annotation of a GitHub check run. The runner
741+// produces them from the workflow commands ::notice, ::warning and ::error and
742+// from problem matchers, after masking.
743+//
744+// The runner sends at most 10 annotations of each level per step (the ones
745+// beyond only reach the log) and the server rejects more.
746+message Annotation {
747+ // Level is the severity. Required.
748+ AnnotationLevel level = 1;
749+ // Title is the optional title. Empty when not given.
750+ string title = 2;
751+ // Message is the text, at most 4,096 characters (the runner cuts longer
752+ // ones). Required, not empty.
753+ string message = 3;
754+ // File is the path the annotation is about, relative to the repository when
755+ // the runner can tell, else as given. Empty when the annotation is not about
756+ // a file.
757+ string file = 4;
758+ // StartLine is the first line, counted from 1. 0 when not given.
759+ int32 start_line = 5;
760+ // EndLine is the last line, counted from 1, not before start_line. 0 when
761+ // not given.
762+ int32 end_line = 6;
763+ // StartColumn is the first column, counted from 1. 0 when not given.
764+ int32 start_column = 7;
765+ // EndColumn is the last column, counted from 1. 0 when not given.
766+ int32 end_column = 8;
767+ // StepNumber is the number of the step that produced it (see
768+ // StepUpdate.number), from 1 to 1,000. An annotation of a step inside a
769+ // composite action belongs to the step of the workflow that uses the action.
770+ int32 step_number = 9;
771+}
772+
773+// ActionRef is an action to resolve.
774+message ActionRef {
775+ // Uses is the value of "uses" as written, after expression evaluation, for
776+ // example "actions/checkout@v4" or "owner/repo/path/to/action@ref". Required.
777+ string uses = 1;
778+}
779+
780+// ResolveActionsRequest lists the actions to resolve.
781+message ResolveActionsRequest {
782+ // JobId is the Job.job_id. It must be the job of the token.
783+ string job_id = 1;
784+ // Refs are the actions to resolve, at most 100. Duplicates are allowed and
785+ // answered one by one.
786+ repeated ActionRef refs = 2;
787+}
788+
789+// ResolveActionsResponse holds the resolution of each action.
790+message ResolveActionsResponse {
791+ // Actions has one entry per ActionRef of the request, in the same order.
792+ repeated ResolvedAction actions = 1;
793+}
794+
795+// ResolvedAction is where to download one action, or why it cannot be.
796+message ResolvedAction {
797+ // Ref is the ActionRef.uses of the request, unchanged.
798+ string ref = 1;
799+ // NameWithOwner is the repository of the action, "owner/repo", with the
800+ // case of the repository as the source gives it. Empty when error is set.
801+ string name_with_owner = 2;
802+ // Path is the sub-path of the action inside the repository, without leading
803+ // or trailing slash: "path/to/action" for "owner/repo/path/to/action@ref".
804+ // Empty for an action at the root of the repository.
805+ string path = 3;
806+ // ResolvedSha is the full id of the commit the ref points to, 40 lowercase
807+ // hexadecimal characters for SHA-1 (the commit that an annotated tag points
808+ // to, not the tag object). Empty when error is set.
809+ string resolved_sha = 4;
810+ // TarballUrl is where to download the archive of the repository at
811+ // resolved_sha, a gzip-compressed tar with one top-level directory. The
812+ // runner downloads it from the sandbox. Empty when error is set.
813+ string tarball_url = 5;
814+ // Token authenticates the download: the runner sends it as "Authorization:
815+ // Basic" with the user "x-access-token". Empty for ACTION_SOURCE_GITHUB.
816+ // For ACTION_SOURCE_ERMITE it is a read token for that one repository,
817+ // valid until the job ends, so a job can use an action of another private
818+ // repository of the server that its own repository token cannot read. It is
819+ // a secret: the runner masks it, and neither the runner nor the server logs
820+ // it.
821+ string token = 6;
822+ // Source is where the action was found. UNSPECIFIED when error is set.
823+ ActionSource source = 7;
824+ // Error is why the reference could not be resolved, in the words of GitHub
825+ // for the same failure. Empty when the action was resolved, in which case
826+ // the other fields describe it.
827+ string error = 8;
828+}
829+
830+// UploadSummaryRequest carries the job summary of one step.
831+message UploadSummaryRequest {
832+ // JobId is the Job.job_id. It must be the job of the token.
833+ string job_id = 1;
834+ // StepNumber is the number of the step that wrote the summary (see
835+ // StepUpdate.number), from 1 to 1,000.
836+ int32 step_number = 2;
837+ // Markdown is the summary, masked, valid UTF-8, at most 1 MiB (1,048,576
838+ // bytes). Empty removes the summary of the step.
839+ string markdown = 3;
840+}
841+
842+// UploadSummaryResponse is empty.
843+message UploadSummaryResponse {}
@@ -93,6 +93,32 @@ enum CancelReason {
93 CANCEL_REASON_TIMEOUT = 3;93 CANCEL_REASON_TIMEOUT = 3;
94 }94 }
95 95
96+// AnnotationLevel is the severity of an annotation, with the values of
97+// GitHub's check run annotation levels ("notice", "warning", "failure"). The
98+// workflow commands ::notice, ::warning and ::error produce NOTICE, WARNING and
99+// FAILURE.
100+enum AnnotationLevel {
101+ // Not set. Rejected in requests, never returned by the server.
102+ ANNOTATION_LEVEL_UNSPECIFIED = 0;
103+ // Information: ::notice.
104+ ANNOTATION_LEVEL_NOTICE = 1;
105+ // A problem that does not fail the job by itself: ::warning.
106+ ANNOTATION_LEVEL_WARNING = 2;
107+ // An error: ::error.
108+ ANNOTATION_LEVEL_FAILURE = 3;
109+}
110+
111+// ActionSource says where the server found an action.
112+enum ActionSource {
113+ // Not set. Only in a ResolvedAction that carries an error.
114+ ACTION_SOURCE_UNSPECIFIED = 0;
115+ // github.com: the archive is downloaded from GitHub, without a token.
116+ ACTION_SOURCE_GITHUB = 1;
117+ // A repository of this server, which wins over a github.com repository of
118+ // the same name (as GitHub Enterprise Server's own actions do).
119+ ACTION_SOURCE_ERMITE = 2;
120+}
121+
96 // RunnerService is what a CI runner (ermite-runner) calls to get jobs and122 // RunnerService is what a CI runner (ermite-runner) calls to get jobs and
97 // report on them. It is not for people: owners use RunService.123 // report on them. It is not for people: owners use RunService.
98 //124 //
@@ -104,8 +130,8 @@ enum CancelReason {
104 // - FetchJob: the runner token returned by RegisterRunner. It identifies the130 // - FetchJob: the runner token returned by RegisterRunner. It identifies the
105 // runner and only works for FetchJob. It is "ermiterun_" followed by 43131 // runner and only works for FetchJob. It is "ermiterun_" followed by 43
106 // base64url characters (32 random bytes, no padding).132 // base64url characters (32 random bytes, no padding).
107-// - UpdateJob, AppendLog, Heartbeat, WatchJob, CompleteJob: the job token of133+// - UpdateJob, AppendLog, Heartbeat, WatchJob, CompleteJob, ResolveActions,
108-// the Job that FetchJob returned. It is valid for that one job and that134+// UploadSummary: the job token of the Job that FetchJob returned. It is valid for that one job and that
109 // one assignment. It is "ermitejob_" followed by 43 base64url characters135 // one assignment. It is "ermitejob_" followed by 43 base64url characters
110 // (32 random bytes, no padding). The job_id of the request must be the136 // (32 random bytes, no padding). The job_id of the request must be the
111 // one of the token.137 // one of the token.
@@ -120,8 +146,8 @@ enum CancelReason {
120 //146 //
121 // The end of a job. A job ends when its conclusion is set, by CompleteJob or147 // The end of a job. A job ends when its conclusion is set, by CompleteJob or
122 // by the server (cancellation not honoured, lost runner, log limit, queue148 // by the server (cancellation not honoured, lost runner, log limit, queue
123-// timeout). From then on UpdateJob, AppendLog, Heartbeat and WatchJob answer149+// timeout). From then on UpdateJob, AppendLog, Heartbeat, WatchJob,
124-// failed_precondition with a JobEnded error detail (a JobEnded message in the150+// ResolveActions and UploadSummary answer failed_precondition with a JobEnded error detail (a JobEnded message in the
125 // Connect error details) that gives the conclusion; the runner stops the job151 // Connect error details) that gives the conclusion; the runner stops the job
126 // and sends nothing more. The job token is still recognised for 24 hours after152 // and sends nothing more. The job token is still recognised for 24 hours after
127 // the end, so that these calls can say so and so that CompleteJob, the only153 // the end, so that these calls can say so and so that CompleteJob, the only
@@ -132,14 +158,22 @@ enum CancelReason {
132 // Secrets and trust. Runner tokens and job tokens are secrets: the server158 // Secrets and trust. Runner tokens and job tokens are secrets: the server
133 // never logs them and clients must not log them. The job token stays in the159 // never logs them and clients must not log them. The job token stays in the
134 // runner host process; it is never given to the sandbox that runs the job's160 // runner host process; it is never given to the sandbox that runs the job's
135-// code. A runner receives the jobs of every repository, with their workflow161+// code. The job's repository token (Job.github_token) is another secret: it is
162+// the one the sandbox does get, and it only reads the repository of the run.
163+// A runner receives the jobs of every repository, with their workflow
136 // files, contexts and (from CI-2) their code, and from CI-3 their secrets: the164 // files, contexts and (from CI-2) their code, and from CI-3 their secrets: the
137 // runner:register scope therefore effectively gives read access to the code165 // runner:register scope therefore effectively gives read access to the code
138 // and the secrets of every repository, private ones included. Give it only to166 // and the secrets of every repository, private ones included. Give it only to
139 // runners you operate.167 // runners you operate.
140 //168 //
141 // Servers that implement this service list "ci.runners" in169 // Servers that implement this service list "ci.runners" in
142-// GetVersionResponse.features.170+// GetVersionResponse.features. Those that implement ResolveActions and
171+// Job.github_token also list "ci.actions", those that accept annotations
172+// (UpdateJobRequest.annotations, CompleteJobRequest.annotations) list
173+// "ci.annotations", and those that implement UploadSummary list
174+// "ci.summaries". A server without a feature ignores the fields of that
175+// feature (protobuf drops unknown fields) and answers unimplemented to its
176+// RPCs; a runner that needs one checks GetVersion first.
143 service RunnerService {177 service RunnerService {
144 // RegisterRunner registers a runner and returns its runner token, which is178 // RegisterRunner registers a runner and returns its runner token, which is
145 // shown only in this response; the runner keeps it. The caller needs the179 // shown only in this response; the runner keeps it. The caller needs the
@@ -191,10 +225,16 @@ service RunnerService {
191 // identical call changes nothing, so a runner may retry after a network225 // identical call changes nothing, so a runner may retry after a network
192 // error. To end the job use CompleteJob.226 // error. To end the job use CompleteJob.
193 //227 //
194- // Fails with invalid_argument if a rule is broken, with failed_precondition228+ // Annotations are applied with the steps, in the same atomic step: for each
195- // if a step goes back (a COMPLETED step cannot change, a step cannot go from229+ // step_number present in annotations, the annotations of that step stored
196- // IN_PROGRESS to PENDING) and with failed_precondition and a JobEnded detail230+ // so far are replaced by the ones of the request, so a retry never
197- // if the job has ended.231+ // duplicates them. A step number absent from the request keeps its
232+ // annotations.
233+ //
234+ // Fails with invalid_argument if a rule is broken (including the annotation
235+ // limits), with failed_precondition if a step goes back (a COMPLETED step
236+ // cannot change, a step cannot go from IN_PROGRESS to PENDING) and with
237+ // failed_precondition and a JobEnded detail if the job has ended.
198 rpc UpdateJob(UpdateJobRequest) returns (UpdateJobResponse);238 rpc UpdateJob(UpdateJobRequest) returns (UpdateJobResponse);
199 // AppendLog appends lines to the log of a job. Needs the job token.239 // AppendLog appends lines to the log of a job. Needs the job token.
200 //240 //
@@ -299,9 +339,53 @@ service RunnerService {
299 // ends are completed by the server as CI_CONCLUSION_CANCELLED (or SKIPPED when339 // ends are completed by the server as CI_CONCLUSION_CANCELLED (or SKIPPED when
300 // they never started and the job did not end by cancellation).340 // they never started and the job did not end by cancellation).
301 //341 //
302- // Fails with invalid_argument if a rule is broken (including the output342+ // Annotations are applied as in UpdateJob, together with the conclusion.
303- // limits) and failed_precondition if a step goes back, as in UpdateJob.343+ // Summaries are not part of this call: a runner sends them with
344+ // UploadSummary before it.
345+ //
346+ // Fails with invalid_argument if a rule is broken (including the output and
347+ // annotation limits) and failed_precondition if a step goes back, as in
348+ // UpdateJob.
304 rpc CompleteJob(CompleteJobRequest) returns (CompleteJobResponse);349 rpc CompleteJob(CompleteJobRequest) returns (CompleteJobResponse);
350+ // ResolveActions turns the "uses" of workflow and action steps into what the
351+ // runner needs to download the actions, as GitHub's action download info
352+ // does. Needs the job token; the job_id must be the one of the token. It
353+ // may be called as often as needed while the job runs (the runner calls it
354+ // once per level of composite actions, see the runner's depth limit of 9).
355+ // The runner then downloads each archive itself, from the sandbox, with the
356+ // token of the ResolvedAction when there is one.
357+ //
358+ // The response has one ResolvedAction per ActionRef, in the same order. A
359+ // reference that cannot be resolved (unknown repository, tag or branch, a
360+ // repository without access, a "uses" that GitHub itself would reject) does
361+ // not fail the call: its ResolvedAction has only ref and error, where error
362+ // is the message GitHub gives for the same failure, which the runner prints
363+ // as is. Only a failure of the call itself (credential, ended job, bad
364+ // request, server error) is an RPC error; the runner then fails the job.
365+ //
366+ // Resolution is stable for a run: the same ref resolves to the same sha for
367+ // every job and every call of one attempt of the run. A re-run attempt
368+ // resolves again.
369+ //
370+ // Fails with unauthenticated or permission_denied as the other job RPCs,
371+ // with failed_precondition and a JobEnded detail if the job has ended, and
372+ // with invalid_argument if there are more than 100 refs or if a "uses" is a
373+ // local action ("./path") or a Docker image ("docker://"), which the runner
374+ // handles without the server.
375+ rpc ResolveActions(ResolveActionsRequest) returns (ResolveActionsResponse);
376+ // UploadSummary stores the job summary of one step: the Markdown that the
377+ // step wrote to the file named by GITHUB_STEP_SUMMARY. Needs the job token.
378+ // The runner masks it before sending it, and sends it after the step ended
379+ // and before CompleteJob.
380+ //
381+ // It is idempotent: the summary of a step is replaced by each call, so a
382+ // retry changes nothing, and an empty markdown removes the summary of the
383+ // step. The Markdown is at most 1 MiB (1,048,576 bytes) and valid UTF-8;
384+ // clients set a message size limit above it.
385+ //
386+ // Fails with invalid_argument if a rule is broken (size, UTF-8, step number)
387+ // and with failed_precondition and a JobEnded detail if the job has ended.
388+ rpc UploadSummary(UploadSummaryRequest) returns (UploadSummaryResponse);
305 }389 }
306 390
307 // RegisterRunnerRequest describes the runner to register.391 // RegisterRunnerRequest describes the runner to register.
@@ -437,6 +521,18 @@ message Job {
437 // Ref is the full ref name that triggered the run, for example521 // Ref is the full ref name that triggered the run, for example
438 // "refs/heads/main" or "refs/tags/v1.0.0".522 // "refs/heads/main" or "refs/tags/v1.0.0".
439 string ref = 14;523 string ref = 14;
524+ // GithubToken is the job repository token, the "github.token" and
525+ // "secrets.GITHUB_TOKEN" of the job: "ermitegh_" and 43 base64url characters
526+ // (32 random bytes, no padding). Unlike job_token it is meant for the code of
527+ // the job: the runner gives it to the sandbox (actions/checkout clones with
528+ // it). It can only read the repository of the run (read-only, whatever
529+ // "permissions:" says), and is valid from the assignment until the job ends
530+ // (it is revoked then, on cancellation and when the job is lost). It is a
531+ // secret: the runner registers it in its masker before anything runs, and
532+ // neither the runner nor the server logs it. It is shown only in this
533+ // response, or in a repeat of the same poll_id; a new assignment of the same
534+ // job has a new one. Empty from servers without the feature "ci.actions".
535+ string github_token = 15;
440 }536 }
441 537
442 // UpdateJobRequest reports the steps of a running job.538 // UpdateJobRequest reports the steps of a running job.
@@ -449,6 +545,12 @@ message UpdateJobRequest {
449 // implicit "Set up job" is 1, the steps of the workflow follow in order, and545 // implicit "Set up job" is 1, the steps of the workflow follow in order, and
450 // the implicit "Complete job" is last.546 // the implicit "Complete job" is last.
451 repeated StepUpdate steps = 2;547 repeated StepUpdate steps = 2;
548+ // Annotations are the annotations of the steps that the call names: for
549+ // each step_number present, all the annotations of that step known so far,
550+ // not only the new ones (see the rules on UpdateJob). At most 10 per level
551+ // and per step; more is invalid_argument. The server ignores them without
552+ // the feature "ci.annotations".
553+ repeated Annotation annotations = 3;
452 }554 }
453 555
454 // StepUpdate is the state of one step as the runner knows it.556 // StepUpdate is the state of one step as the runner knows it.
@@ -472,6 +574,14 @@ message StepUpdate {
472 // CompletedAt is when the step ended, by the clock of the runner. Required574 // CompletedAt is when the step ended, by the clock of the runner. Required
473 // when state is COMPLETED.575 // when state is COMPLETED.
474 google.protobuf.Timestamp completed_at = 6;576 google.protobuf.Timestamp completed_at = 6;
577+ // Outcome is how the step ended before continue-on-error was applied, GitHub's
578+ // "steps.<id>.outcome", whereas conclusion is the result after it
579+ // ("steps.<id>.conclusion"). Same values as conclusion. They differ only for
580+ // a step with continue-on-error that failed: outcome FAILURE, conclusion
581+ // SUCCESS. Set by the runner when state is COMPLETED and UNSPECIFIED
582+ // otherwise; UNSPECIFIED in a COMPLETED step means the same as conclusion
583+ // (a runner that does not know the field).
584+ CiConclusion outcome = 7;
475 }585 }
476 586
477 // UpdateJobResponse is empty.587 // UpdateJobResponse is empty.
@@ -504,6 +614,11 @@ message CompleteJobRequest {
504 // Steps are the final steps, with the same meaning as UpdateJobRequest.steps,614 // Steps are the final steps, with the same meaning as UpdateJobRequest.steps,
505 // applied in the same atomic step as the conclusion.615 // applied in the same atomic step as the conclusion.
506 repeated StepUpdate steps = 5;616 repeated StepUpdate steps = 5;
617+ // Annotations are those of the steps that changed since the last call, with
618+ // the rules of UpdateJobRequest.annotations (all the annotations of each
619+ // step named). The server ignores them without the feature
620+ // "ci.annotations".
621+ repeated Annotation annotations = 6;
507 }622 }
508 623
509 // CompleteJobResponse is the stored result of a job.624 // CompleteJobResponse is the stored result of a job.
@@ -620,3 +735,109 @@ message CancelRequest {
620 // server.735 // server.
621 google.protobuf.Timestamp requested_at = 3;736 google.protobuf.Timestamp requested_at = 3;
622 }737 }
738+
739+// Annotation is a message attached to a step, and optionally to a place in a
740+// file of the repository, like an annotation of a GitHub check run. The runner
741+// produces them from the workflow commands ::notice, ::warning and ::error and
742+// from problem matchers, after masking.
743+//
744+// The runner sends at most 10 annotations of each level per step (the ones
745+// beyond only reach the log) and the server rejects more.
746+message Annotation {
747+ // Level is the severity. Required.
748+ AnnotationLevel level = 1;
749+ // Title is the optional title. Empty when not given.
750+ string title = 2;
751+ // Message is the text, at most 4,096 characters (the runner cuts longer
752+ // ones). Required, not empty.
753+ string message = 3;
754+ // File is the path the annotation is about, relative to the repository when
755+ // the runner can tell, else as given. Empty when the annotation is not about
756+ // a file.
757+ string file = 4;
758+ // StartLine is the first line, counted from 1. 0 when not given.
759+ int32 start_line = 5;
760+ // EndLine is the last line, counted from 1, not before start_line. 0 when
761+ // not given.
762+ int32 end_line = 6;
763+ // StartColumn is the first column, counted from 1. 0 when not given.
764+ int32 start_column = 7;
765+ // EndColumn is the last column, counted from 1. 0 when not given.
766+ int32 end_column = 8;
767+ // StepNumber is the number of the step that produced it (see
768+ // StepUpdate.number), from 1 to 1,000. An annotation of a step inside a
769+ // composite action belongs to the step of the workflow that uses the action.
770+ int32 step_number = 9;
771+}
772+
773+// ActionRef is an action to resolve.
774+message ActionRef {
775+ // Uses is the value of "uses" as written, after expression evaluation, for
776+ // example "actions/checkout@v4" or "owner/repo/path/to/action@ref". Required.
777+ string uses = 1;
778+}
779+
780+// ResolveActionsRequest lists the actions to resolve.
781+message ResolveActionsRequest {
782+ // JobId is the Job.job_id. It must be the job of the token.
783+ string job_id = 1;
784+ // Refs are the actions to resolve, at most 100. Duplicates are allowed and
785+ // answered one by one.
786+ repeated ActionRef refs = 2;
787+}
788+
789+// ResolveActionsResponse holds the resolution of each action.
790+message ResolveActionsResponse {
791+ // Actions has one entry per ActionRef of the request, in the same order.
792+ repeated ResolvedAction actions = 1;
793+}
794+
795+// ResolvedAction is where to download one action, or why it cannot be.
796+message ResolvedAction {
797+ // Ref is the ActionRef.uses of the request, unchanged.
798+ string ref = 1;
799+ // NameWithOwner is the repository of the action, "owner/repo", with the
800+ // case of the repository as the source gives it. Empty when error is set.
801+ string name_with_owner = 2;
802+ // Path is the sub-path of the action inside the repository, without leading
803+ // or trailing slash: "path/to/action" for "owner/repo/path/to/action@ref".
804+ // Empty for an action at the root of the repository.
805+ string path = 3;
806+ // ResolvedSha is the full id of the commit the ref points to, 40 lowercase
807+ // hexadecimal characters for SHA-1 (the commit that an annotated tag points
808+ // to, not the tag object). Empty when error is set.
809+ string resolved_sha = 4;
810+ // TarballUrl is where to download the archive of the repository at
811+ // resolved_sha, a gzip-compressed tar with one top-level directory. The
812+ // runner downloads it from the sandbox. Empty when error is set.
813+ string tarball_url = 5;
814+ // Token authenticates the download: the runner sends it as "Authorization:
815+ // Basic" with the user "x-access-token". Empty for ACTION_SOURCE_GITHUB.
816+ // For ACTION_SOURCE_ERMITE it is a read token for that one repository,
817+ // valid until the job ends, so a job can use an action of another private
818+ // repository of the server that its own repository token cannot read. It is
819+ // a secret: the runner masks it, and neither the runner nor the server logs
820+ // it.
821+ string token = 6;
822+ // Source is where the action was found. UNSPECIFIED when error is set.
823+ ActionSource source = 7;
824+ // Error is why the reference could not be resolved, in the words of GitHub
825+ // for the same failure. Empty when the action was resolved, in which case
826+ // the other fields describe it.
827+ string error = 8;
828+}
829+
830+// UploadSummaryRequest carries the job summary of one step.
831+message UploadSummaryRequest {
832+ // JobId is the Job.job_id. It must be the job of the token.
833+ string job_id = 1;
834+ // StepNumber is the number of the step that wrote the summary (see
835+ // StepUpdate.number), from 1 to 1,000.
836+ int32 step_number = 2;
837+ // Markdown is the summary, masked, valid UTF-8, at most 1 MiB (1,048,576
838+ // bytes). Empty removes the summary of the step.
839+ string markdown = 3;
840+}
841+
842+// UploadSummaryResponse is empty.
843+message UploadSummaryResponse {}
M proto/ermite/server.proto +5 -1
@@ -35,7 +35,11 @@ message GetVersionResponse {
3535 // identifiers are "ci.runs" (RunService runs, jobs, logs, checks, and the
3636 // run_updated and job_updated events), "ci.runs.filters" (ListRuns honours
3737 // its filters) and "ci.runners" (RunnerService, and RunService.ListRunners
38- // and RemoveRunner).
38+ // and RemoveRunner). The CI execution identifiers are "ci.actions"
39+ // (RunnerService.ResolveActions and Job.github_token), "ci.annotations"
40+ // (annotations in UpdateJob and CompleteJob, and RunService.ListAnnotations)
41+ // and "ci.summaries" (RunnerService.UploadSummary and
42+ // RunService.GetStepSummaries).
3943 repeated string features = 2;
4044 }
4145
@@ -35,7 +35,11 @@ message GetVersionResponse {
35 // identifiers are "ci.runs" (RunService runs, jobs, logs, checks, and the35 // identifiers are "ci.runs" (RunService runs, jobs, logs, checks, and the
36 // run_updated and job_updated events), "ci.runs.filters" (ListRuns honours36 // run_updated and job_updated events), "ci.runs.filters" (ListRuns honours
37 // its filters) and "ci.runners" (RunnerService, and RunService.ListRunners37 // its filters) and "ci.runners" (RunnerService, and RunService.ListRunners
38- // and RemoveRunner).38+ // and RemoveRunner). The CI execution identifiers are "ci.actions"
39+ // (RunnerService.ResolveActions and Job.github_token), "ci.annotations"
40+ // (annotations in UpdateJob and CompleteJob, and RunService.ListAnnotations)
41+ // and "ci.summaries" (RunnerService.UploadSummary and
42+ // RunService.GetStepSummaries).
39 repeated string features = 2;43 repeated string features = 2;
40 }44 }
41 45

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 2

Tags 18

↑↓ moveenter openesc close