detached 49ac5fd

Merge pull request 'feat(ci): add actions, annotations and summaries to the CI protocol' (#9) from feat/ci2-protocol into main

commit
49ac5fd7a78a04c05aa33c86f7dcec5b6a4a58e7
author
bnema · · 2026-10-13 09:16 UTC
merge of
4db3b8b + b94fede · diff against the first parent
signature
SSH UnverifiedSHA256:VOCA5lYARHcbKDZ7GhTV45VmcjWnJ6ZQIBazj17LivA The key is unknown
refs
tag: v0.2.0
10 files · +2129 -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 +101 -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,25 @@ 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. For each job it gives the annotations of the
82+ // attempt in which that job executed (RunJob.attempt), which is lower than
83+ // the attempt of the run for a job kept by a re-run of failed jobs only. Needs the same access as GetRun. Returns at most
84+ // 1,000 annotations (truncated tells). A run without annotations gives an
85+ // empty list. Only servers with the feature "ci.annotations" implement it
86+ // (unimplemented otherwise). Fails with not_found if the run, the attempt or
87+ // the job does not exist, or if the job is not a job of the run.
88+ ListAnnotations(context.Context, *connect.Request[ermite.ListAnnotationsRequest]) (*connect.Response[ermite.ListAnnotationsResponse], error)
89+ // GetStepSummaries returns the job summaries (the Markdown written to
90+ // GITHUB_STEP_SUMMARY) of the steps of a job that have one, ordered by step
91+ // number. Like GitHub, it returns at most 20 summaries per job: the first 20
92+ // steps that have one, in step order; truncated says there are more. Needs
93+ // the same access as GetJob. Only servers with the feature
94+ // "ci.summaries" implement it (unimplemented otherwise). A job without
95+ // summaries gives an empty list. Fails with not_found if the job or the
96+ // attempt does not exist.
97+ GetStepSummaries(context.Context, *connect.Request[ermite.GetStepSummariesRequest]) (*connect.Response[ermite.GetStepSummariesResponse], error)
7398 // GetJobLog streams the lines of the log of a job, from a line number. The
7499 // server sends them in order in messages of at most 1,000 lines and 64 KiB of
75100 // text each.
@@ -148,6 +173,18 @@ func NewRunServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...
148173 connect.WithSchema(runServiceMethods.ByName("GetJob")),
149174 connect.WithClientOptions(opts...),
150175 ),
176+ listAnnotations: connect.NewClient[ermite.ListAnnotationsRequest, ermite.ListAnnotationsResponse](
177+ httpClient,
178+ baseURL+RunServiceListAnnotationsProcedure,
179+ connect.WithSchema(runServiceMethods.ByName("ListAnnotations")),
180+ connect.WithClientOptions(opts...),
181+ ),
182+ getStepSummaries: connect.NewClient[ermite.GetStepSummariesRequest, ermite.GetStepSummariesResponse](
183+ httpClient,
184+ baseURL+RunServiceGetStepSummariesProcedure,
185+ connect.WithSchema(runServiceMethods.ByName("GetStepSummaries")),
186+ connect.WithClientOptions(opts...),
187+ ),
151188 getJobLog: connect.NewClient[ermite.GetJobLogRequest, ermite.GetJobLogResponse](
152189 httpClient,
153190 baseURL+RunServiceGetJobLogProcedure,
@@ -189,15 +226,17 @@ func NewRunServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...
189226
190227 // runServiceClient implements RunServiceClient.
191228 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]
229+ listRuns *connect.Client[ermite.ListRunsRequest, ermite.ListRunsResponse]
230+ getRun *connect.Client[ermite.GetRunRequest, ermite.GetRunResponse]
231+ getJob *connect.Client[ermite.GetJobRequest, ermite.GetJobResponse]
232+ listAnnotations *connect.Client[ermite.ListAnnotationsRequest, ermite.ListAnnotationsResponse]
233+ getStepSummaries *connect.Client[ermite.GetStepSummariesRequest, ermite.GetStepSummariesResponse]
234+ getJobLog *connect.Client[ermite.GetJobLogRequest, ermite.GetJobLogResponse]
235+ cancelRun *connect.Client[ermite.CancelRunRequest, ermite.CancelRunResponse]
236+ rerunRun *connect.Client[ermite.RerunRunRequest, ermite.RerunRunResponse]
237+ listChecks *connect.Client[ermite.ListChecksRequest, ermite.ListChecksResponse]
238+ listRunners *connect.Client[ermite.ListRunnersRequest, ermite.ListRunnersResponse]
239+ removeRunner *connect.Client[ermite.RemoveRunnerRequest, ermite.RemoveRunnerResponse]
201240 }
202241
203242 // ListRuns calls ermite.RunService.ListRuns.
@@ -215,6 +254,16 @@ func (c *runServiceClient) GetJob(ctx context.Context, req *connect.Request[ermi
215254 return c.getJob.CallUnary(ctx, req)
216255 }
217256
257+// ListAnnotations calls ermite.RunService.ListAnnotations.
258+func (c *runServiceClient) ListAnnotations(ctx context.Context, req *connect.Request[ermite.ListAnnotationsRequest]) (*connect.Response[ermite.ListAnnotationsResponse], error) {
259+ return c.listAnnotations.CallUnary(ctx, req)
260+}
261+
262+// GetStepSummaries calls ermite.RunService.GetStepSummaries.
263+func (c *runServiceClient) GetStepSummaries(ctx context.Context, req *connect.Request[ermite.GetStepSummariesRequest]) (*connect.Response[ermite.GetStepSummariesResponse], error) {
264+ return c.getStepSummaries.CallUnary(ctx, req)
265+}
266+
218267 // GetJobLog calls ermite.RunService.GetJobLog.
219268 func (c *runServiceClient) GetJobLog(ctx context.Context, req *connect.Request[ermite.GetJobLogRequest]) (*connect.ServerStreamForClient[ermite.GetJobLogResponse], error) {
220269 return c.getJobLog.CallServerStream(ctx, req)
@@ -260,6 +309,25 @@ type RunServiceHandler interface {
260309 // GetJob returns one job with its steps. Fails with not_found if the job
261310 // or the attempt does not exist.
262311 GetJob(context.Context, *connect.Request[ermite.GetJobRequest]) (*connect.Response[ermite.GetJobResponse], error)
312+ // ListAnnotations lists the annotations of a run attempt, optionally of one
313+ // job, ordered by job (as in GetRun), step number, then the order in which
314+ // the step produced them. For each job it gives the annotations of the
315+ // attempt in which that job executed (RunJob.attempt), which is lower than
316+ // the attempt of the run for a job kept by a re-run of failed jobs only. Needs the same access as GetRun. Returns at most
317+ // 1,000 annotations (truncated tells). A run without annotations gives an
318+ // empty list. Only servers with the feature "ci.annotations" implement it
319+ // (unimplemented otherwise). Fails with not_found if the run, the attempt or
320+ // the job does not exist, or if the job is not a job of the run.
321+ ListAnnotations(context.Context, *connect.Request[ermite.ListAnnotationsRequest]) (*connect.Response[ermite.ListAnnotationsResponse], error)
322+ // GetStepSummaries returns the job summaries (the Markdown written to
323+ // GITHUB_STEP_SUMMARY) of the steps of a job that have one, ordered by step
324+ // number. Like GitHub, it returns at most 20 summaries per job: the first 20
325+ // steps that have one, in step order; truncated says there are more. Needs
326+ // the same access as GetJob. Only servers with the feature
327+ // "ci.summaries" implement it (unimplemented otherwise). A job without
328+ // summaries gives an empty list. Fails with not_found if the job or the
329+ // attempt does not exist.
330+ GetStepSummaries(context.Context, *connect.Request[ermite.GetStepSummariesRequest]) (*connect.Response[ermite.GetStepSummariesResponse], error)
263331 // GetJobLog streams the lines of the log of a job, from a line number. The
264332 // server sends them in order in messages of at most 1,000 lines and 64 KiB of
265333 // text each.
@@ -334,6 +402,18 @@ func NewRunServiceHandler(svc RunServiceHandler, opts ...connect.HandlerOption)
334402 connect.WithSchema(runServiceMethods.ByName("GetJob")),
335403 connect.WithHandlerOptions(opts...),
336404 )
405+ runServiceListAnnotationsHandler := connect.NewUnaryHandler(
406+ RunServiceListAnnotationsProcedure,
407+ svc.ListAnnotations,
408+ connect.WithSchema(runServiceMethods.ByName("ListAnnotations")),
409+ connect.WithHandlerOptions(opts...),
410+ )
411+ runServiceGetStepSummariesHandler := connect.NewUnaryHandler(
412+ RunServiceGetStepSummariesProcedure,
413+ svc.GetStepSummaries,
414+ connect.WithSchema(runServiceMethods.ByName("GetStepSummaries")),
415+ connect.WithHandlerOptions(opts...),
416+ )
337417 runServiceGetJobLogHandler := connect.NewServerStreamHandler(
338418 RunServiceGetJobLogProcedure,
339419 svc.GetJobLog,
@@ -378,6 +458,10 @@ func NewRunServiceHandler(svc RunServiceHandler, opts ...connect.HandlerOption)
378458 runServiceGetRunHandler.ServeHTTP(w, r)
379459 case RunServiceGetJobProcedure:
380460 runServiceGetJobHandler.ServeHTTP(w, r)
461+ case RunServiceListAnnotationsProcedure:
462+ runServiceListAnnotationsHandler.ServeHTTP(w, r)
463+ case RunServiceGetStepSummariesProcedure:
464+ runServiceGetStepSummariesHandler.ServeHTTP(w, r)
381465 case RunServiceGetJobLogProcedure:
382466 runServiceGetJobLogHandler.ServeHTTP(w, r)
383467 case RunServiceCancelRunProcedure:
@@ -411,6 +495,14 @@ func (UnimplementedRunServiceHandler) GetJob(context.Context, *connect.Request[e
411495 return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunService.GetJob is not implemented"))
412496 }
413497
498+func (UnimplementedRunServiceHandler) ListAnnotations(context.Context, *connect.Request[ermite.ListAnnotationsRequest]) (*connect.Response[ermite.ListAnnotationsResponse], error) {
499+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunService.ListAnnotations is not implemented"))
500+}
501+
502+func (UnimplementedRunServiceHandler) GetStepSummaries(context.Context, *connect.Request[ermite.GetStepSummariesRequest]) (*connect.Response[ermite.GetStepSummariesResponse], error) {
503+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunService.GetStepSummaries is not implemented"))
504+}
505+
414506 func (UnimplementedRunServiceHandler) GetJobLog(context.Context, *connect.Request[ermite.GetJobLogRequest], *connect.ServerStream[ermite.GetJobLogResponse]) error {
415507 return connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunService.GetJobLog is not implemented"))
416508 }
@@ -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,25 @@ 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. For each job it gives the annotations of the
82+ // attempt in which that job executed (RunJob.attempt), which is lower than
83+ // the attempt of the run for a job kept by a re-run of failed jobs only. Needs the same access as GetRun. Returns at most
84+ // 1,000 annotations (truncated tells). A run without annotations gives an
85+ // empty list. Only servers with the feature "ci.annotations" implement it
86+ // (unimplemented otherwise). Fails with not_found if the run, the attempt or
87+ // the job does not exist, or if the job is not a job of the run.
88+ ListAnnotations(context.Context, *connect.Request[ermite.ListAnnotationsRequest]) (*connect.Response[ermite.ListAnnotationsResponse], error)
89+ // GetStepSummaries returns the job summaries (the Markdown written to
90+ // GITHUB_STEP_SUMMARY) of the steps of a job that have one, ordered by step
91+ // number. Like GitHub, it returns at most 20 summaries per job: the first 20
92+ // steps that have one, in step order; truncated says there are more. Needs
93+ // the same access as GetJob. Only servers with the feature
94+ // "ci.summaries" implement it (unimplemented otherwise). A job without
95+ // summaries gives an empty list. Fails with not_found if the job or the
96+ // attempt does not exist.
97+ 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. The98 // 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 of99 // server sends them in order in messages of at most 1,000 lines and 64 KiB of
75 // text each.100 // text each.
@@ -148,6 +173,18 @@ func NewRunServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...
148 connect.WithSchema(runServiceMethods.ByName("GetJob")),173 connect.WithSchema(runServiceMethods.ByName("GetJob")),
149 connect.WithClientOptions(opts...),174 connect.WithClientOptions(opts...),
150 ),175 ),
176+ listAnnotations: connect.NewClient[ermite.ListAnnotationsRequest, ermite.ListAnnotationsResponse](
177+ httpClient,
178+ baseURL+RunServiceListAnnotationsProcedure,
179+ connect.WithSchema(runServiceMethods.ByName("ListAnnotations")),
180+ connect.WithClientOptions(opts...),
181+ ),
182+ getStepSummaries: connect.NewClient[ermite.GetStepSummariesRequest, ermite.GetStepSummariesResponse](
183+ httpClient,
184+ baseURL+RunServiceGetStepSummariesProcedure,
185+ connect.WithSchema(runServiceMethods.ByName("GetStepSummaries")),
186+ connect.WithClientOptions(opts...),
187+ ),
151 getJobLog: connect.NewClient[ermite.GetJobLogRequest, ermite.GetJobLogResponse](188 getJobLog: connect.NewClient[ermite.GetJobLogRequest, ermite.GetJobLogResponse](
152 httpClient,189 httpClient,
153 baseURL+RunServiceGetJobLogProcedure,190 baseURL+RunServiceGetJobLogProcedure,
@@ -189,15 +226,17 @@ func NewRunServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...
189 226
190 // runServiceClient implements RunServiceClient.227 // runServiceClient implements RunServiceClient.
191 type runServiceClient struct {228 type runServiceClient struct {
192- listRuns *connect.Client[ermite.ListRunsRequest, ermite.ListRunsResponse]229+ listRuns *connect.Client[ermite.ListRunsRequest, ermite.ListRunsResponse]
193- getRun *connect.Client[ermite.GetRunRequest, ermite.GetRunResponse]230+ getRun *connect.Client[ermite.GetRunRequest, ermite.GetRunResponse]
194- getJob *connect.Client[ermite.GetJobRequest, ermite.GetJobResponse]231+ getJob *connect.Client[ermite.GetJobRequest, ermite.GetJobResponse]
195- getJobLog *connect.Client[ermite.GetJobLogRequest, ermite.GetJobLogResponse]232+ listAnnotations *connect.Client[ermite.ListAnnotationsRequest, ermite.ListAnnotationsResponse]
196- cancelRun *connect.Client[ermite.CancelRunRequest, ermite.CancelRunResponse]233+ getStepSummaries *connect.Client[ermite.GetStepSummariesRequest, ermite.GetStepSummariesResponse]
197- rerunRun *connect.Client[ermite.RerunRunRequest, ermite.RerunRunResponse]234+ getJobLog *connect.Client[ermite.GetJobLogRequest, ermite.GetJobLogResponse]
198- listChecks *connect.Client[ermite.ListChecksRequest, ermite.ListChecksResponse]235+ cancelRun *connect.Client[ermite.CancelRunRequest, ermite.CancelRunResponse]
199- listRunners *connect.Client[ermite.ListRunnersRequest, ermite.ListRunnersResponse]236+ rerunRun *connect.Client[ermite.RerunRunRequest, ermite.RerunRunResponse]
200- removeRunner *connect.Client[ermite.RemoveRunnerRequest, ermite.RemoveRunnerResponse]237+ listChecks *connect.Client[ermite.ListChecksRequest, ermite.ListChecksResponse]
238+ listRunners *connect.Client[ermite.ListRunnersRequest, ermite.ListRunnersResponse]
239+ removeRunner *connect.Client[ermite.RemoveRunnerRequest, ermite.RemoveRunnerResponse]
201 }240 }
202 241
203 // ListRuns calls ermite.RunService.ListRuns.242 // ListRuns calls ermite.RunService.ListRuns.
@@ -215,6 +254,16 @@ func (c *runServiceClient) GetJob(ctx context.Context, req *connect.Request[ermi
215 return c.getJob.CallUnary(ctx, req)254 return c.getJob.CallUnary(ctx, req)
216 }255 }
217 256
257+// ListAnnotations calls ermite.RunService.ListAnnotations.
258+func (c *runServiceClient) ListAnnotations(ctx context.Context, req *connect.Request[ermite.ListAnnotationsRequest]) (*connect.Response[ermite.ListAnnotationsResponse], error) {
259+ return c.listAnnotations.CallUnary(ctx, req)
260+}
261+
262+// GetStepSummaries calls ermite.RunService.GetStepSummaries.
263+func (c *runServiceClient) GetStepSummaries(ctx context.Context, req *connect.Request[ermite.GetStepSummariesRequest]) (*connect.Response[ermite.GetStepSummariesResponse], error) {
264+ return c.getStepSummaries.CallUnary(ctx, req)
265+}
266+
218 // GetJobLog calls ermite.RunService.GetJobLog.267 // GetJobLog calls ermite.RunService.GetJobLog.
219 func (c *runServiceClient) GetJobLog(ctx context.Context, req *connect.Request[ermite.GetJobLogRequest]) (*connect.ServerStreamForClient[ermite.GetJobLogResponse], error) {268 func (c *runServiceClient) GetJobLog(ctx context.Context, req *connect.Request[ermite.GetJobLogRequest]) (*connect.ServerStreamForClient[ermite.GetJobLogResponse], error) {
220 return c.getJobLog.CallServerStream(ctx, req)269 return c.getJobLog.CallServerStream(ctx, req)
@@ -260,6 +309,25 @@ type RunServiceHandler interface {
260 // GetJob returns one job with its steps. Fails with not_found if the job309 // GetJob returns one job with its steps. Fails with not_found if the job
261 // or the attempt does not exist.310 // or the attempt does not exist.
262 GetJob(context.Context, *connect.Request[ermite.GetJobRequest]) (*connect.Response[ermite.GetJobResponse], error)311 GetJob(context.Context, *connect.Request[ermite.GetJobRequest]) (*connect.Response[ermite.GetJobResponse], error)
312+ // ListAnnotations lists the annotations of a run attempt, optionally of one
313+ // job, ordered by job (as in GetRun), step number, then the order in which
314+ // the step produced them. For each job it gives the annotations of the
315+ // attempt in which that job executed (RunJob.attempt), which is lower than
316+ // the attempt of the run for a job kept by a re-run of failed jobs only. Needs the same access as GetRun. Returns at most
317+ // 1,000 annotations (truncated tells). A run without annotations gives an
318+ // empty list. Only servers with the feature "ci.annotations" implement it
319+ // (unimplemented otherwise). Fails with not_found if the run, the attempt or
320+ // the job does not exist, or if the job is not a job of the run.
321+ ListAnnotations(context.Context, *connect.Request[ermite.ListAnnotationsRequest]) (*connect.Response[ermite.ListAnnotationsResponse], error)
322+ // GetStepSummaries returns the job summaries (the Markdown written to
323+ // GITHUB_STEP_SUMMARY) of the steps of a job that have one, ordered by step
324+ // number. Like GitHub, it returns at most 20 summaries per job: the first 20
325+ // steps that have one, in step order; truncated says there are more. Needs
326+ // the same access as GetJob. Only servers with the feature
327+ // "ci.summaries" implement it (unimplemented otherwise). A job without
328+ // summaries gives an empty list. Fails with not_found if the job or the
329+ // attempt does not exist.
330+ 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. The331 // 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 of332 // server sends them in order in messages of at most 1,000 lines and 64 KiB of
265 // text each.333 // text each.
@@ -334,6 +402,18 @@ func NewRunServiceHandler(svc RunServiceHandler, opts ...connect.HandlerOption)
334 connect.WithSchema(runServiceMethods.ByName("GetJob")),402 connect.WithSchema(runServiceMethods.ByName("GetJob")),
335 connect.WithHandlerOptions(opts...),403 connect.WithHandlerOptions(opts...),
336 )404 )
405+ runServiceListAnnotationsHandler := connect.NewUnaryHandler(
406+ RunServiceListAnnotationsProcedure,
407+ svc.ListAnnotations,
408+ connect.WithSchema(runServiceMethods.ByName("ListAnnotations")),
409+ connect.WithHandlerOptions(opts...),
410+ )
411+ runServiceGetStepSummariesHandler := connect.NewUnaryHandler(
412+ RunServiceGetStepSummariesProcedure,
413+ svc.GetStepSummaries,
414+ connect.WithSchema(runServiceMethods.ByName("GetStepSummaries")),
415+ connect.WithHandlerOptions(opts...),
416+ )
337 runServiceGetJobLogHandler := connect.NewServerStreamHandler(417 runServiceGetJobLogHandler := connect.NewServerStreamHandler(
338 RunServiceGetJobLogProcedure,418 RunServiceGetJobLogProcedure,
339 svc.GetJobLog,419 svc.GetJobLog,
@@ -378,6 +458,10 @@ func NewRunServiceHandler(svc RunServiceHandler, opts ...connect.HandlerOption)
378 runServiceGetRunHandler.ServeHTTP(w, r)458 runServiceGetRunHandler.ServeHTTP(w, r)
379 case RunServiceGetJobProcedure:459 case RunServiceGetJobProcedure:
380 runServiceGetJobHandler.ServeHTTP(w, r)460 runServiceGetJobHandler.ServeHTTP(w, r)
461+ case RunServiceListAnnotationsProcedure:
462+ runServiceListAnnotationsHandler.ServeHTTP(w, r)
463+ case RunServiceGetStepSummariesProcedure:
464+ runServiceGetStepSummariesHandler.ServeHTTP(w, r)
381 case RunServiceGetJobLogProcedure:465 case RunServiceGetJobLogProcedure:
382 runServiceGetJobLogHandler.ServeHTTP(w, r)466 runServiceGetJobLogHandler.ServeHTTP(w, r)
383 case RunServiceCancelRunProcedure:467 case RunServiceCancelRunProcedure:
@@ -411,6 +495,14 @@ func (UnimplementedRunServiceHandler) GetJob(context.Context, *connect.Request[e
411 return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunService.GetJob is not implemented"))495 return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunService.GetJob is not implemented"))
412 }496 }
413 497
498+func (UnimplementedRunServiceHandler) ListAnnotations(context.Context, *connect.Request[ermite.ListAnnotationsRequest]) (*connect.Response[ermite.ListAnnotationsResponse], error) {
499+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunService.ListAnnotations is not implemented"))
500+}
501+
502+func (UnimplementedRunServiceHandler) GetStepSummaries(context.Context, *connect.Request[ermite.GetStepSummariesRequest]) (*connect.Response[ermite.GetStepSummariesResponse], error) {
503+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunService.GetStepSummaries is not implemented"))
504+}
505+
414 func (UnimplementedRunServiceHandler) GetJobLog(context.Context, *connect.Request[ermite.GetJobLogRequest], *connect.ServerStream[ermite.GetJobLogResponse]) error {506 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"))507 return connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunService.GetJobLog is not implemented"))
416 }508 }
M gen/ermite/ermiteconnect/runner.connect.go +184 -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,21 @@ 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. The step of an annotation need not be in the steps of the
119+ // same call, and may already be COMPLETED: its annotations can still be
120+ // replaced (the rule that a COMPLETED step cannot change applies to
121+ // StepUpdate, not to annotations), until the job ends. A runner sends one
122+ // UpdateJob (or CompleteJob) at a time for a job, never concurrent calls,
123+ // so replacements apply in the order of the calls.
124+ //
125+ // Fails with invalid_argument if a rule is broken (including the annotation
126+ // limits), with failed_precondition if a step goes back (a COMPLETED step
127+ // cannot change, a step cannot go from IN_PROGRESS to PENDING) and with
128+ // failed_precondition and a JobEnded detail if the job has ended.
112129 UpdateJob(context.Context, *connect.Request[ermite.UpdateJobRequest]) (*connect.Response[ermite.UpdateJobResponse], error)
113130 // AppendLog appends lines to the log of a job. Needs the job token.
114131 //
@@ -213,9 +230,57 @@ type RunnerServiceClient interface {
213230 // ends are completed by the server as CI_CONCLUSION_CANCELLED (or SKIPPED when
214231 // they never started and the job did not end by cancellation).
215232 //
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.
233+ // Annotations are applied as in UpdateJob (replacement per step number, also
234+ // for COMPLETED steps and steps not in steps), together with the conclusion.
235+ // Summaries are not part of this call: a runner sends them with
236+ // UploadSummary before it.
237+ //
238+ // Fails with invalid_argument if a rule is broken (including the output and
239+ // annotation limits) and failed_precondition if a step goes back, as in
240+ // UpdateJob.
218241 CompleteJob(context.Context, *connect.Request[ermite.CompleteJobRequest]) (*connect.Response[ermite.CompleteJobResponse], error)
242+ // ResolveActions turns the "uses" of workflow and action steps into what the
243+ // runner needs to download the actions, as GitHub's action download info
244+ // does. Needs the job token; the job_id must be the one of the token. It
245+ // may be called as often as needed while the job runs (the runner calls it
246+ // once per level of composite actions, see the runner's depth limit of 9).
247+ // The runner then downloads each archive itself, from the sandbox, with the
248+ // token of the ResolvedAction when there is one.
249+ //
250+ // The response has one ResolvedAction per ActionRef, in the same order. A
251+ // reference that cannot be resolved (unknown repository, tag or branch, a
252+ // repository without access, a "uses" that GitHub itself would reject) does
253+ // not fail the call: its ResolvedAction has only ref and error, where error
254+ // is the message GitHub gives for the same failure, which the runner prints
255+ // as is. Only a failure of the call itself (credential, ended job, bad
256+ // request, server error) is an RPC error; the runner then fails the job.
257+ //
258+ // Resolution is stable for a run attempt: the same ref resolves to the same
259+ // sha (ResolvedAction.resolved_sha) for every job and every call of that
260+ // attempt. A re-run attempt resolves again. Tokens are not shared: each
261+ // job gets its own (ResolvedAction.token).
262+ //
263+ // Fails with unauthenticated or permission_denied as the other job RPCs,
264+ // with failed_precondition and a JobEnded detail if the job has ended, and
265+ // with invalid_argument if there are more than 100 refs or if a "uses" is a
266+ // local action ("./path") or a Docker image ("docker://"), which the runner
267+ // handles without the server.
268+ ResolveActions(context.Context, *connect.Request[ermite.ResolveActionsRequest]) (*connect.Response[ermite.ResolveActionsResponse], error)
269+ // UploadSummary stores the job summary of one step: the Markdown that the
270+ // step wrote to the file named by GITHUB_STEP_SUMMARY. Needs the job token.
271+ // The runner masks it before sending it, and sends it after the step ended
272+ // and before CompleteJob.
273+ //
274+ // It is idempotent: the summary of a step is replaced by each call, so a
275+ // retry changes nothing, and an empty markdown removes the summary of the
276+ // step. The Markdown is at most 1 MiB (1,048,576 bytes) and valid UTF-8. The
277+ // server reads requests of up to 1 MiB plus the envelope (job id, step
278+ // number, framing), so a summary of exactly 1 MiB is accepted; clients set
279+ // their own message size limit above that.
280+ //
281+ // Fails with invalid_argument if a rule is broken (size, UTF-8, step number)
282+ // and with failed_precondition and a JobEnded detail if the job has ended.
283+ UploadSummary(context.Context, *connect.Request[ermite.UploadSummaryRequest]) (*connect.Response[ermite.UploadSummaryResponse], error)
219284 }
220285
221286 // NewRunnerServiceClient constructs a client for the ermite.RunnerService service. By default, it
@@ -271,6 +336,18 @@ func NewRunnerServiceClient(httpClient connect.HTTPClient, baseURL string, opts
271336 connect.WithSchema(runnerServiceMethods.ByName("CompleteJob")),
272337 connect.WithClientOptions(opts...),
273338 ),
339+ resolveActions: connect.NewClient[ermite.ResolveActionsRequest, ermite.ResolveActionsResponse](
340+ httpClient,
341+ baseURL+RunnerServiceResolveActionsProcedure,
342+ connect.WithSchema(runnerServiceMethods.ByName("ResolveActions")),
343+ connect.WithClientOptions(opts...),
344+ ),
345+ uploadSummary: connect.NewClient[ermite.UploadSummaryRequest, ermite.UploadSummaryResponse](
346+ httpClient,
347+ baseURL+RunnerServiceUploadSummaryProcedure,
348+ connect.WithSchema(runnerServiceMethods.ByName("UploadSummary")),
349+ connect.WithClientOptions(opts...),
350+ ),
274351 }
275352 }
276353
@@ -283,6 +360,8 @@ type runnerServiceClient struct {
283360 heartbeat *connect.Client[ermite.HeartbeatRequest, ermite.HeartbeatResponse]
284361 watchJob *connect.Client[ermite.WatchJobRequest, ermite.WatchJobResponse]
285362 completeJob *connect.Client[ermite.CompleteJobRequest, ermite.CompleteJobResponse]
363+ resolveActions *connect.Client[ermite.ResolveActionsRequest, ermite.ResolveActionsResponse]
364+ uploadSummary *connect.Client[ermite.UploadSummaryRequest, ermite.UploadSummaryResponse]
286365 }
287366
288367 // RegisterRunner calls ermite.RunnerService.RegisterRunner.
@@ -320,6 +399,16 @@ func (c *runnerServiceClient) CompleteJob(ctx context.Context, req *connect.Requ
320399 return c.completeJob.CallUnary(ctx, req)
321400 }
322401
402+// ResolveActions calls ermite.RunnerService.ResolveActions.
403+func (c *runnerServiceClient) ResolveActions(ctx context.Context, req *connect.Request[ermite.ResolveActionsRequest]) (*connect.Response[ermite.ResolveActionsResponse], error) {
404+ return c.resolveActions.CallUnary(ctx, req)
405+}
406+
407+// UploadSummary calls ermite.RunnerService.UploadSummary.
408+func (c *runnerServiceClient) UploadSummary(ctx context.Context, req *connect.Request[ermite.UploadSummaryRequest]) (*connect.Response[ermite.UploadSummaryResponse], error) {
409+ return c.uploadSummary.CallUnary(ctx, req)
410+}
411+
323412 // RunnerServiceHandler is an implementation of the ermite.RunnerService service.
324413 type RunnerServiceHandler interface {
325414 // RegisterRunner registers a runner and returns its runner token, which is
@@ -372,10 +461,21 @@ type RunnerServiceHandler interface {
372461 // identical call changes nothing, so a runner may retry after a network
373462 // error. To end the job use CompleteJob.
374463 //
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.
464+ // Annotations are applied with the steps, in the same atomic step: for each
465+ // step_number present in annotations, the annotations of that step stored
466+ // so far are replaced by the ones of the request, so a retry never
467+ // duplicates them. A step number absent from the request keeps its
468+ // annotations. The step of an annotation need not be in the steps of the
469+ // same call, and may already be COMPLETED: its annotations can still be
470+ // replaced (the rule that a COMPLETED step cannot change applies to
471+ // StepUpdate, not to annotations), until the job ends. A runner sends one
472+ // UpdateJob (or CompleteJob) at a time for a job, never concurrent calls,
473+ // so replacements apply in the order of the calls.
474+ //
475+ // Fails with invalid_argument if a rule is broken (including the annotation
476+ // limits), with failed_precondition if a step goes back (a COMPLETED step
477+ // cannot change, a step cannot go from IN_PROGRESS to PENDING) and with
478+ // failed_precondition and a JobEnded detail if the job has ended.
379479 UpdateJob(context.Context, *connect.Request[ermite.UpdateJobRequest]) (*connect.Response[ermite.UpdateJobResponse], error)
380480 // AppendLog appends lines to the log of a job. Needs the job token.
381481 //
@@ -480,9 +580,57 @@ type RunnerServiceHandler interface {
480580 // ends are completed by the server as CI_CONCLUSION_CANCELLED (or SKIPPED when
481581 // they never started and the job did not end by cancellation).
482582 //
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.
583+ // Annotations are applied as in UpdateJob (replacement per step number, also
584+ // for COMPLETED steps and steps not in steps), together with the conclusion.
585+ // Summaries are not part of this call: a runner sends them with
586+ // UploadSummary before it.
587+ //
588+ // Fails with invalid_argument if a rule is broken (including the output and
589+ // annotation limits) and failed_precondition if a step goes back, as in
590+ // UpdateJob.
485591 CompleteJob(context.Context, *connect.Request[ermite.CompleteJobRequest]) (*connect.Response[ermite.CompleteJobResponse], error)
592+ // ResolveActions turns the "uses" of workflow and action steps into what the
593+ // runner needs to download the actions, as GitHub's action download info
594+ // does. Needs the job token; the job_id must be the one of the token. It
595+ // may be called as often as needed while the job runs (the runner calls it
596+ // once per level of composite actions, see the runner's depth limit of 9).
597+ // The runner then downloads each archive itself, from the sandbox, with the
598+ // token of the ResolvedAction when there is one.
599+ //
600+ // The response has one ResolvedAction per ActionRef, in the same order. A
601+ // reference that cannot be resolved (unknown repository, tag or branch, a
602+ // repository without access, a "uses" that GitHub itself would reject) does
603+ // not fail the call: its ResolvedAction has only ref and error, where error
604+ // is the message GitHub gives for the same failure, which the runner prints
605+ // as is. Only a failure of the call itself (credential, ended job, bad
606+ // request, server error) is an RPC error; the runner then fails the job.
607+ //
608+ // Resolution is stable for a run attempt: the same ref resolves to the same
609+ // sha (ResolvedAction.resolved_sha) for every job and every call of that
610+ // attempt. A re-run attempt resolves again. Tokens are not shared: each
611+ // job gets its own (ResolvedAction.token).
612+ //
613+ // Fails with unauthenticated or permission_denied as the other job RPCs,
614+ // with failed_precondition and a JobEnded detail if the job has ended, and
615+ // with invalid_argument if there are more than 100 refs or if a "uses" is a
616+ // local action ("./path") or a Docker image ("docker://"), which the runner
617+ // handles without the server.
618+ ResolveActions(context.Context, *connect.Request[ermite.ResolveActionsRequest]) (*connect.Response[ermite.ResolveActionsResponse], error)
619+ // UploadSummary stores the job summary of one step: the Markdown that the
620+ // step wrote to the file named by GITHUB_STEP_SUMMARY. Needs the job token.
621+ // The runner masks it before sending it, and sends it after the step ended
622+ // and before CompleteJob.
623+ //
624+ // It is idempotent: the summary of a step is replaced by each call, so a
625+ // retry changes nothing, and an empty markdown removes the summary of the
626+ // step. The Markdown is at most 1 MiB (1,048,576 bytes) and valid UTF-8. The
627+ // server reads requests of up to 1 MiB plus the envelope (job id, step
628+ // number, framing), so a summary of exactly 1 MiB is accepted; clients set
629+ // their own message size limit above that.
630+ //
631+ // Fails with invalid_argument if a rule is broken (size, UTF-8, step number)
632+ // and with failed_precondition and a JobEnded detail if the job has ended.
633+ UploadSummary(context.Context, *connect.Request[ermite.UploadSummaryRequest]) (*connect.Response[ermite.UploadSummaryResponse], error)
486634 }
487635
488636 // NewRunnerServiceHandler builds an HTTP handler from the service implementation. It returns the
@@ -534,6 +682,18 @@ func NewRunnerServiceHandler(svc RunnerServiceHandler, opts ...connect.HandlerOp
534682 connect.WithSchema(runnerServiceMethods.ByName("CompleteJob")),
535683 connect.WithHandlerOptions(opts...),
536684 )
685+ runnerServiceResolveActionsHandler := connect.NewUnaryHandler(
686+ RunnerServiceResolveActionsProcedure,
687+ svc.ResolveActions,
688+ connect.WithSchema(runnerServiceMethods.ByName("ResolveActions")),
689+ connect.WithHandlerOptions(opts...),
690+ )
691+ runnerServiceUploadSummaryHandler := connect.NewUnaryHandler(
692+ RunnerServiceUploadSummaryProcedure,
693+ svc.UploadSummary,
694+ connect.WithSchema(runnerServiceMethods.ByName("UploadSummary")),
695+ connect.WithHandlerOptions(opts...),
696+ )
537697 return "/ermite.RunnerService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
538698 switch r.URL.Path {
539699 case RunnerServiceRegisterRunnerProcedure:
@@ -550,6 +710,10 @@ func NewRunnerServiceHandler(svc RunnerServiceHandler, opts ...connect.HandlerOp
550710 runnerServiceWatchJobHandler.ServeHTTP(w, r)
551711 case RunnerServiceCompleteJobProcedure:
552712 runnerServiceCompleteJobHandler.ServeHTTP(w, r)
713+ case RunnerServiceResolveActionsProcedure:
714+ runnerServiceResolveActionsHandler.ServeHTTP(w, r)
715+ case RunnerServiceUploadSummaryProcedure:
716+ runnerServiceUploadSummaryHandler.ServeHTTP(w, r)
553717 default:
554718 http.NotFound(w, r)
555719 }
@@ -586,3 +750,11 @@ func (UnimplementedRunnerServiceHandler) WatchJob(context.Context, *connect.Requ
586750 func (UnimplementedRunnerServiceHandler) CompleteJob(context.Context, *connect.Request[ermite.CompleteJobRequest]) (*connect.Response[ermite.CompleteJobResponse], error) {
587751 return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunnerService.CompleteJob is not implemented"))
588752 }
753+
754+func (UnimplementedRunnerServiceHandler) ResolveActions(context.Context, *connect.Request[ermite.ResolveActionsRequest]) (*connect.Response[ermite.ResolveActionsResponse], error) {
755+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunnerService.ResolveActions is not implemented"))
756+}
757+
758+func (UnimplementedRunnerServiceHandler) UploadSummary(context.Context, *connect.Request[ermite.UploadSummaryRequest]) (*connect.Response[ermite.UploadSummaryResponse], error) {
759+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunnerService.UploadSummary is not implemented"))
760+}
@@ -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,21 @@ 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. The step of an annotation need not be in the steps of the
119+ // same call, and may already be COMPLETED: its annotations can still be
120+ // replaced (the rule that a COMPLETED step cannot change applies to
121+ // StepUpdate, not to annotations), until the job ends. A runner sends one
122+ // UpdateJob (or CompleteJob) at a time for a job, never concurrent calls,
123+ // so replacements apply in the order of the calls.
124+ //
125+ // Fails with invalid_argument if a rule is broken (including the annotation
126+ // limits), with failed_precondition if a step goes back (a COMPLETED step
127+ // cannot change, a step cannot go from IN_PROGRESS to PENDING) and with
128+ // failed_precondition and a JobEnded detail if the job has ended.
112 UpdateJob(context.Context, *connect.Request[ermite.UpdateJobRequest]) (*connect.Response[ermite.UpdateJobResponse], error)129 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.130 // AppendLog appends lines to the log of a job. Needs the job token.
114 //131 //
@@ -213,9 +230,57 @@ type RunnerServiceClient interface {
213 // ends are completed by the server as CI_CONCLUSION_CANCELLED (or SKIPPED when230 // 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).231 // they never started and the job did not end by cancellation).
215 //232 //
216- // Fails with invalid_argument if a rule is broken (including the output233+ // Annotations are applied as in UpdateJob (replacement per step number, also
217- // limits) and failed_precondition if a step goes back, as in UpdateJob.234+ // for COMPLETED steps and steps not in steps), together with the conclusion.
235+ // Summaries are not part of this call: a runner sends them with
236+ // UploadSummary before it.
237+ //
238+ // Fails with invalid_argument if a rule is broken (including the output and
239+ // annotation limits) and failed_precondition if a step goes back, as in
240+ // UpdateJob.
218 CompleteJob(context.Context, *connect.Request[ermite.CompleteJobRequest]) (*connect.Response[ermite.CompleteJobResponse], error)241 CompleteJob(context.Context, *connect.Request[ermite.CompleteJobRequest]) (*connect.Response[ermite.CompleteJobResponse], error)
242+ // ResolveActions turns the "uses" of workflow and action steps into what the
243+ // runner needs to download the actions, as GitHub's action download info
244+ // does. Needs the job token; the job_id must be the one of the token. It
245+ // may be called as often as needed while the job runs (the runner calls it
246+ // once per level of composite actions, see the runner's depth limit of 9).
247+ // The runner then downloads each archive itself, from the sandbox, with the
248+ // token of the ResolvedAction when there is one.
249+ //
250+ // The response has one ResolvedAction per ActionRef, in the same order. A
251+ // reference that cannot be resolved (unknown repository, tag or branch, a
252+ // repository without access, a "uses" that GitHub itself would reject) does
253+ // not fail the call: its ResolvedAction has only ref and error, where error
254+ // is the message GitHub gives for the same failure, which the runner prints
255+ // as is. Only a failure of the call itself (credential, ended job, bad
256+ // request, server error) is an RPC error; the runner then fails the job.
257+ //
258+ // Resolution is stable for a run attempt: the same ref resolves to the same
259+ // sha (ResolvedAction.resolved_sha) for every job and every call of that
260+ // attempt. A re-run attempt resolves again. Tokens are not shared: each
261+ // job gets its own (ResolvedAction.token).
262+ //
263+ // Fails with unauthenticated or permission_denied as the other job RPCs,
264+ // with failed_precondition and a JobEnded detail if the job has ended, and
265+ // with invalid_argument if there are more than 100 refs or if a "uses" is a
266+ // local action ("./path") or a Docker image ("docker://"), which the runner
267+ // handles without the server.
268+ ResolveActions(context.Context, *connect.Request[ermite.ResolveActionsRequest]) (*connect.Response[ermite.ResolveActionsResponse], error)
269+ // UploadSummary stores the job summary of one step: the Markdown that the
270+ // step wrote to the file named by GITHUB_STEP_SUMMARY. Needs the job token.
271+ // The runner masks it before sending it, and sends it after the step ended
272+ // and before CompleteJob.
273+ //
274+ // It is idempotent: the summary of a step is replaced by each call, so a
275+ // retry changes nothing, and an empty markdown removes the summary of the
276+ // step. The Markdown is at most 1 MiB (1,048,576 bytes) and valid UTF-8. The
277+ // server reads requests of up to 1 MiB plus the envelope (job id, step
278+ // number, framing), so a summary of exactly 1 MiB is accepted; clients set
279+ // their own message size limit above that.
280+ //
281+ // Fails with invalid_argument if a rule is broken (size, UTF-8, step number)
282+ // and with failed_precondition and a JobEnded detail if the job has ended.
283+ UploadSummary(context.Context, *connect.Request[ermite.UploadSummaryRequest]) (*connect.Response[ermite.UploadSummaryResponse], error)
219 }284 }
220 285
221 // NewRunnerServiceClient constructs a client for the ermite.RunnerService service. By default, it286 // NewRunnerServiceClient constructs a client for the ermite.RunnerService service. By default, it
@@ -271,6 +336,18 @@ func NewRunnerServiceClient(httpClient connect.HTTPClient, baseURL string, opts
271 connect.WithSchema(runnerServiceMethods.ByName("CompleteJob")),336 connect.WithSchema(runnerServiceMethods.ByName("CompleteJob")),
272 connect.WithClientOptions(opts...),337 connect.WithClientOptions(opts...),
273 ),338 ),
339+ resolveActions: connect.NewClient[ermite.ResolveActionsRequest, ermite.ResolveActionsResponse](
340+ httpClient,
341+ baseURL+RunnerServiceResolveActionsProcedure,
342+ connect.WithSchema(runnerServiceMethods.ByName("ResolveActions")),
343+ connect.WithClientOptions(opts...),
344+ ),
345+ uploadSummary: connect.NewClient[ermite.UploadSummaryRequest, ermite.UploadSummaryResponse](
346+ httpClient,
347+ baseURL+RunnerServiceUploadSummaryProcedure,
348+ connect.WithSchema(runnerServiceMethods.ByName("UploadSummary")),
349+ connect.WithClientOptions(opts...),
350+ ),
274 }351 }
275 }352 }
276 353
@@ -283,6 +360,8 @@ type runnerServiceClient struct {
283 heartbeat *connect.Client[ermite.HeartbeatRequest, ermite.HeartbeatResponse]360 heartbeat *connect.Client[ermite.HeartbeatRequest, ermite.HeartbeatResponse]
284 watchJob *connect.Client[ermite.WatchJobRequest, ermite.WatchJobResponse]361 watchJob *connect.Client[ermite.WatchJobRequest, ermite.WatchJobResponse]
285 completeJob *connect.Client[ermite.CompleteJobRequest, ermite.CompleteJobResponse]362 completeJob *connect.Client[ermite.CompleteJobRequest, ermite.CompleteJobResponse]
363+ resolveActions *connect.Client[ermite.ResolveActionsRequest, ermite.ResolveActionsResponse]
364+ uploadSummary *connect.Client[ermite.UploadSummaryRequest, ermite.UploadSummaryResponse]
286 }365 }
287 366
288 // RegisterRunner calls ermite.RunnerService.RegisterRunner.367 // RegisterRunner calls ermite.RunnerService.RegisterRunner.
@@ -320,6 +399,16 @@ func (c *runnerServiceClient) CompleteJob(ctx context.Context, req *connect.Requ
320 return c.completeJob.CallUnary(ctx, req)399 return c.completeJob.CallUnary(ctx, req)
321 }400 }
322 401
402+// ResolveActions calls ermite.RunnerService.ResolveActions.
403+func (c *runnerServiceClient) ResolveActions(ctx context.Context, req *connect.Request[ermite.ResolveActionsRequest]) (*connect.Response[ermite.ResolveActionsResponse], error) {
404+ return c.resolveActions.CallUnary(ctx, req)
405+}
406+
407+// UploadSummary calls ermite.RunnerService.UploadSummary.
408+func (c *runnerServiceClient) UploadSummary(ctx context.Context, req *connect.Request[ermite.UploadSummaryRequest]) (*connect.Response[ermite.UploadSummaryResponse], error) {
409+ return c.uploadSummary.CallUnary(ctx, req)
410+}
411+
323 // RunnerServiceHandler is an implementation of the ermite.RunnerService service.412 // RunnerServiceHandler is an implementation of the ermite.RunnerService service.
324 type RunnerServiceHandler interface {413 type RunnerServiceHandler interface {
325 // RegisterRunner registers a runner and returns its runner token, which is414 // RegisterRunner registers a runner and returns its runner token, which is
@@ -372,10 +461,21 @@ type RunnerServiceHandler interface {
372 // identical call changes nothing, so a runner may retry after a network461 // identical call changes nothing, so a runner may retry after a network
373 // error. To end the job use CompleteJob.462 // error. To end the job use CompleteJob.
374 //463 //
375- // Fails with invalid_argument if a rule is broken, with failed_precondition464+ // 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 from465+ // step_number present in annotations, the annotations of that step stored
377- // IN_PROGRESS to PENDING) and with failed_precondition and a JobEnded detail466+ // so far are replaced by the ones of the request, so a retry never
378- // if the job has ended.467+ // duplicates them. A step number absent from the request keeps its
468+ // annotations. The step of an annotation need not be in the steps of the
469+ // same call, and may already be COMPLETED: its annotations can still be
470+ // replaced (the rule that a COMPLETED step cannot change applies to
471+ // StepUpdate, not to annotations), until the job ends. A runner sends one
472+ // UpdateJob (or CompleteJob) at a time for a job, never concurrent calls,
473+ // so replacements apply in the order of the calls.
474+ //
475+ // Fails with invalid_argument if a rule is broken (including the annotation
476+ // limits), with failed_precondition if a step goes back (a COMPLETED step
477+ // cannot change, a step cannot go from IN_PROGRESS to PENDING) and with
478+ // failed_precondition and a JobEnded detail if the job has ended.
379 UpdateJob(context.Context, *connect.Request[ermite.UpdateJobRequest]) (*connect.Response[ermite.UpdateJobResponse], error)479 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.480 // AppendLog appends lines to the log of a job. Needs the job token.
381 //481 //
@@ -480,9 +580,57 @@ type RunnerServiceHandler interface {
480 // ends are completed by the server as CI_CONCLUSION_CANCELLED (or SKIPPED when580 // 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).581 // they never started and the job did not end by cancellation).
482 //582 //
483- // Fails with invalid_argument if a rule is broken (including the output583+ // Annotations are applied as in UpdateJob (replacement per step number, also
484- // limits) and failed_precondition if a step goes back, as in UpdateJob.584+ // for COMPLETED steps and steps not in steps), together with the conclusion.
585+ // Summaries are not part of this call: a runner sends them with
586+ // UploadSummary before it.
587+ //
588+ // Fails with invalid_argument if a rule is broken (including the output and
589+ // annotation limits) and failed_precondition if a step goes back, as in
590+ // UpdateJob.
485 CompleteJob(context.Context, *connect.Request[ermite.CompleteJobRequest]) (*connect.Response[ermite.CompleteJobResponse], error)591 CompleteJob(context.Context, *connect.Request[ermite.CompleteJobRequest]) (*connect.Response[ermite.CompleteJobResponse], error)
592+ // ResolveActions turns the "uses" of workflow and action steps into what the
593+ // runner needs to download the actions, as GitHub's action download info
594+ // does. Needs the job token; the job_id must be the one of the token. It
595+ // may be called as often as needed while the job runs (the runner calls it
596+ // once per level of composite actions, see the runner's depth limit of 9).
597+ // The runner then downloads each archive itself, from the sandbox, with the
598+ // token of the ResolvedAction when there is one.
599+ //
600+ // The response has one ResolvedAction per ActionRef, in the same order. A
601+ // reference that cannot be resolved (unknown repository, tag or branch, a
602+ // repository without access, a "uses" that GitHub itself would reject) does
603+ // not fail the call: its ResolvedAction has only ref and error, where error
604+ // is the message GitHub gives for the same failure, which the runner prints
605+ // as is. Only a failure of the call itself (credential, ended job, bad
606+ // request, server error) is an RPC error; the runner then fails the job.
607+ //
608+ // Resolution is stable for a run attempt: the same ref resolves to the same
609+ // sha (ResolvedAction.resolved_sha) for every job and every call of that
610+ // attempt. A re-run attempt resolves again. Tokens are not shared: each
611+ // job gets its own (ResolvedAction.token).
612+ //
613+ // Fails with unauthenticated or permission_denied as the other job RPCs,
614+ // with failed_precondition and a JobEnded detail if the job has ended, and
615+ // with invalid_argument if there are more than 100 refs or if a "uses" is a
616+ // local action ("./path") or a Docker image ("docker://"), which the runner
617+ // handles without the server.
618+ ResolveActions(context.Context, *connect.Request[ermite.ResolveActionsRequest]) (*connect.Response[ermite.ResolveActionsResponse], error)
619+ // UploadSummary stores the job summary of one step: the Markdown that the
620+ // step wrote to the file named by GITHUB_STEP_SUMMARY. Needs the job token.
621+ // The runner masks it before sending it, and sends it after the step ended
622+ // and before CompleteJob.
623+ //
624+ // It is idempotent: the summary of a step is replaced by each call, so a
625+ // retry changes nothing, and an empty markdown removes the summary of the
626+ // step. The Markdown is at most 1 MiB (1,048,576 bytes) and valid UTF-8. The
627+ // server reads requests of up to 1 MiB plus the envelope (job id, step
628+ // number, framing), so a summary of exactly 1 MiB is accepted; clients set
629+ // their own message size limit above that.
630+ //
631+ // Fails with invalid_argument if a rule is broken (size, UTF-8, step number)
632+ // and with failed_precondition and a JobEnded detail if the job has ended.
633+ UploadSummary(context.Context, *connect.Request[ermite.UploadSummaryRequest]) (*connect.Response[ermite.UploadSummaryResponse], error)
486 }634 }
487 635
488 // NewRunnerServiceHandler builds an HTTP handler from the service implementation. It returns the636 // NewRunnerServiceHandler builds an HTTP handler from the service implementation. It returns the
@@ -534,6 +682,18 @@ func NewRunnerServiceHandler(svc RunnerServiceHandler, opts ...connect.HandlerOp
534 connect.WithSchema(runnerServiceMethods.ByName("CompleteJob")),682 connect.WithSchema(runnerServiceMethods.ByName("CompleteJob")),
535 connect.WithHandlerOptions(opts...),683 connect.WithHandlerOptions(opts...),
536 )684 )
685+ runnerServiceResolveActionsHandler := connect.NewUnaryHandler(
686+ RunnerServiceResolveActionsProcedure,
687+ svc.ResolveActions,
688+ connect.WithSchema(runnerServiceMethods.ByName("ResolveActions")),
689+ connect.WithHandlerOptions(opts...),
690+ )
691+ runnerServiceUploadSummaryHandler := connect.NewUnaryHandler(
692+ RunnerServiceUploadSummaryProcedure,
693+ svc.UploadSummary,
694+ connect.WithSchema(runnerServiceMethods.ByName("UploadSummary")),
695+ connect.WithHandlerOptions(opts...),
696+ )
537 return "/ermite.RunnerService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {697 return "/ermite.RunnerService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
538 switch r.URL.Path {698 switch r.URL.Path {
539 case RunnerServiceRegisterRunnerProcedure:699 case RunnerServiceRegisterRunnerProcedure:
@@ -550,6 +710,10 @@ func NewRunnerServiceHandler(svc RunnerServiceHandler, opts ...connect.HandlerOp
550 runnerServiceWatchJobHandler.ServeHTTP(w, r)710 runnerServiceWatchJobHandler.ServeHTTP(w, r)
551 case RunnerServiceCompleteJobProcedure:711 case RunnerServiceCompleteJobProcedure:
552 runnerServiceCompleteJobHandler.ServeHTTP(w, r)712 runnerServiceCompleteJobHandler.ServeHTTP(w, r)
713+ case RunnerServiceResolveActionsProcedure:
714+ runnerServiceResolveActionsHandler.ServeHTTP(w, r)
715+ case RunnerServiceUploadSummaryProcedure:
716+ runnerServiceUploadSummaryHandler.ServeHTTP(w, r)
553 default:717 default:
554 http.NotFound(w, r)718 http.NotFound(w, r)
555 }719 }
@@ -586,3 +750,11 @@ func (UnimplementedRunnerServiceHandler) WatchJob(context.Context, *connect.Requ
586 func (UnimplementedRunnerServiceHandler) CompleteJob(context.Context, *connect.Request[ermite.CompleteJobRequest]) (*connect.Response[ermite.CompleteJobResponse], error) {750 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"))751 return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunnerService.CompleteJob is not implemented"))
588 }752 }
753+
754+func (UnimplementedRunnerServiceHandler) ResolveActions(context.Context, *connect.Request[ermite.ResolveActionsRequest]) (*connect.Response[ermite.ResolveActionsResponse], error) {
755+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunnerService.ResolveActions is not implemented"))
756+}
757+
758+func (UnimplementedRunnerServiceHandler) UploadSummary(context.Context, *connect.Request[ermite.UploadSummaryRequest]) (*connect.Response[ermite.UploadSummaryResponse], error) {
759+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunnerService.UploadSummary is not implemented"))
760+}
M gen/ermite/run.pb.go +550 -132 generated file: collapsed
M gen/ermite/runner.pb.go +838 -78 generated file: collapsed
M gen/ermite/server.pb.go +5 -1 generated file: collapsed
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, "truncated": 2}},
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"}}, Truncated: true},
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, "truncated": 2}},
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"}}, Truncated: true},
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 +93 -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,25 @@ 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. For each job it gives the annotations of the
287+ // attempt in which that job executed (RunJob.attempt), which is lower than
288+ // the attempt of the run for a job kept by a re-run of failed jobs only. Needs the same access as GetRun. Returns at most
289+ // 1,000 annotations (truncated tells). A run without annotations gives an
290+ // empty list. Only servers with the feature "ci.annotations" implement it
291+ // (unimplemented otherwise). Fails with not_found if the run, the attempt or
292+ // the job does not exist, or if the job is not a job of the run.
293+ rpc ListAnnotations(ListAnnotationsRequest) returns (ListAnnotationsResponse);
294+ // GetStepSummaries returns the job summaries (the Markdown written to
295+ // GITHUB_STEP_SUMMARY) of the steps of a job that have one, ordered by step
296+ // number. Like GitHub, it returns at most 20 summaries per job: the first 20
297+ // steps that have one, in step order; truncated says there are more. Needs
298+ // the same access as GetJob. Only servers with the feature
299+ // "ci.summaries" implement it (unimplemented otherwise). A job without
300+ // summaries gives an empty list. Fails with not_found if the job or the
301+ // attempt does not exist.
302+ rpc GetStepSummaries(GetStepSummariesRequest) returns (GetStepSummariesResponse);
276303 // GetJobLog streams the lines of the log of a job, from a line number. The
277304 // server sends them in order in messages of at most 1,000 lines and 64 KiB of
278305 // text each.
@@ -393,6 +420,67 @@ message GetJobResponse {
393420 RunJob job = 1;
394421 }
395422
423+// ListAnnotationsRequest selects the annotations of a run.
424+message ListAnnotationsRequest {
425+ // Repo is the repository name.
426+ string repo = 1;
427+ // RunId is the Run.id.
428+ int64 run_id = 2;
429+ // Attempt is the attempt to read. 0 means the latest, and then each job
430+ // contributes the annotations of its own RunJob.attempt (not necessarily the
431+ // latest attempt of the run, see ListAnnotations).
432+ int32 attempt = 3;
433+ // JobId restricts the result to one job (RunJob.id). Empty means every job
434+ // of the run.
435+ string job_id = 4;
436+}
437+
438+// ListAnnotationsResponse holds the annotations of a run.
439+message ListAnnotationsResponse {
440+ // Annotations are the annotations, at most 1,000.
441+ repeated JobAnnotation annotations = 1;
442+ // Truncated is true when there are more annotations than annotations holds.
443+ bool truncated = 2;
444+}
445+
446+// JobAnnotation is an annotation with the job it belongs to.
447+message JobAnnotation {
448+ // JobId is the RunJob.id of the job.
449+ string job_id = 1;
450+ // Annotation is the annotation as the runner sent it, after the server
451+ // checked it. Its step_number is the step of the job.
452+ Annotation annotation = 2;
453+}
454+
455+// GetStepSummariesRequest selects a job.
456+message GetStepSummariesRequest {
457+ // Repo is the repository name.
458+ string repo = 1;
459+ // JobId is the RunJob.id.
460+ string job_id = 2;
461+ // Attempt is the RunJob.attempt to read. 0 means the latest.
462+ int32 attempt = 3;
463+}
464+
465+// GetStepSummariesResponse holds the summaries of a job.
466+message GetStepSummariesResponse {
467+ // Summaries are the summaries of the steps that have one, ordered by step
468+ // number: at most 20, the first 20 steps having one.
469+ repeated StepSummary summaries = 1;
470+ // Truncated is true when the job has more than 20 steps with a summary and
471+ // summaries holds only the first 20.
472+ bool truncated = 2;
473+}
474+
475+// StepSummary is the job summary of one step.
476+message StepSummary {
477+ // StepNumber is the number of the step (see Step.number).
478+ int32 step_number = 1;
479+ // Markdown is the summary as uploaded, at most 1 MiB. Clients render it
480+ // safely: it is untrusted content (raw HTML and script URLs must not run).
481+ string markdown = 2;
482+}
483+
396484 // GetJobLogRequest selects the lines to stream.
397485 message GetJobLogRequest {
398486 // 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,25 @@ 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. For each job it gives the annotations of the
287+ // attempt in which that job executed (RunJob.attempt), which is lower than
288+ // the attempt of the run for a job kept by a re-run of failed jobs only. Needs the same access as GetRun. Returns at most
289+ // 1,000 annotations (truncated tells). A run without annotations gives an
290+ // empty list. Only servers with the feature "ci.annotations" implement it
291+ // (unimplemented otherwise). Fails with not_found if the run, the attempt or
292+ // the job does not exist, or if the job is not a job of the run.
293+ rpc ListAnnotations(ListAnnotationsRequest) returns (ListAnnotationsResponse);
294+ // GetStepSummaries returns the job summaries (the Markdown written to
295+ // GITHUB_STEP_SUMMARY) of the steps of a job that have one, ordered by step
296+ // number. Like GitHub, it returns at most 20 summaries per job: the first 20
297+ // steps that have one, in step order; truncated says there are more. Needs
298+ // the same access as GetJob. Only servers with the feature
299+ // "ci.summaries" implement it (unimplemented otherwise). A job without
300+ // summaries gives an empty list. Fails with not_found if the job or the
301+ // attempt does not exist.
302+ rpc GetStepSummaries(GetStepSummariesRequest) returns (GetStepSummariesResponse);
276 // GetJobLog streams the lines of the log of a job, from a line number. The303 // 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 of304 // server sends them in order in messages of at most 1,000 lines and 64 KiB of
278 // text each.305 // text each.
@@ -393,6 +420,67 @@ message GetJobResponse {
393 RunJob job = 1;420 RunJob job = 1;
394 }421 }
395 422
423+// ListAnnotationsRequest selects the annotations of a run.
424+message ListAnnotationsRequest {
425+ // Repo is the repository name.
426+ string repo = 1;
427+ // RunId is the Run.id.
428+ int64 run_id = 2;
429+ // Attempt is the attempt to read. 0 means the latest, and then each job
430+ // contributes the annotations of its own RunJob.attempt (not necessarily the
431+ // latest attempt of the run, see ListAnnotations).
432+ int32 attempt = 3;
433+ // JobId restricts the result to one job (RunJob.id). Empty means every job
434+ // of the run.
435+ string job_id = 4;
436+}
437+
438+// ListAnnotationsResponse holds the annotations of a run.
439+message ListAnnotationsResponse {
440+ // Annotations are the annotations, at most 1,000.
441+ repeated JobAnnotation annotations = 1;
442+ // Truncated is true when there are more annotations than annotations holds.
443+ bool truncated = 2;
444+}
445+
446+// JobAnnotation is an annotation with the job it belongs to.
447+message JobAnnotation {
448+ // JobId is the RunJob.id of the job.
449+ string job_id = 1;
450+ // Annotation is the annotation as the runner sent it, after the server
451+ // checked it. Its step_number is the step of the job.
452+ Annotation annotation = 2;
453+}
454+
455+// GetStepSummariesRequest selects a job.
456+message GetStepSummariesRequest {
457+ // Repo is the repository name.
458+ string repo = 1;
459+ // JobId is the RunJob.id.
460+ string job_id = 2;
461+ // Attempt is the RunJob.attempt to read. 0 means the latest.
462+ int32 attempt = 3;
463+}
464+
465+// GetStepSummariesResponse holds the summaries of a job.
466+message GetStepSummariesResponse {
467+ // Summaries are the summaries of the steps that have one, ordered by step
468+ // number: at most 20, the first 20 steps having one.
469+ repeated StepSummary summaries = 1;
470+ // Truncated is true when the job has more than 20 steps with a summary and
471+ // summaries holds only the first 20.
472+ bool truncated = 2;
473+}
474+
475+// StepSummary is the job summary of one step.
476+message StepSummary {
477+ // StepNumber is the number of the step (see Step.number).
478+ int32 step_number = 1;
479+ // Markdown is the summary as uploaded, at most 1 MiB. Clients render it
480+ // safely: it is untrusted content (raw HTML and script URLs must not run).
481+ string markdown = 2;
482+}
483+
396 // GetJobLogRequest selects the lines to stream.484 // GetJobLogRequest selects the lines to stream.
397 message GetJobLogRequest {485 message GetJobLogRequest {
398 // Repo is the repository name.486 // Repo is the repository name.
M proto/ermite/runner.proto +251 -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,21 @@ 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. The step of an annotation need not be in the steps of the
233+ // same call, and may already be COMPLETED: its annotations can still be
234+ // replaced (the rule that a COMPLETED step cannot change applies to
235+ // StepUpdate, not to annotations), until the job ends. A runner sends one
236+ // UpdateJob (or CompleteJob) at a time for a job, never concurrent calls,
237+ // so replacements apply in the order of the calls.
238+ //
239+ // Fails with invalid_argument if a rule is broken (including the annotation
240+ // limits), with failed_precondition if a step goes back (a COMPLETED step
241+ // cannot change, a step cannot go from IN_PROGRESS to PENDING) and with
242+ // failed_precondition and a JobEnded detail if the job has ended.
198243 rpc UpdateJob(UpdateJobRequest) returns (UpdateJobResponse);
199244 // AppendLog appends lines to the log of a job. Needs the job token.
200245 //
@@ -299,9 +344,57 @@ service RunnerService {
299344 // ends are completed by the server as CI_CONCLUSION_CANCELLED (or SKIPPED when
300345 // they never started and the job did not end by cancellation).
301346 //
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.
347+ // Annotations are applied as in UpdateJob (replacement per step number, also
348+ // for COMPLETED steps and steps not in steps), together with the conclusion.
349+ // Summaries are not part of this call: a runner sends them with
350+ // UploadSummary before it.
351+ //
352+ // Fails with invalid_argument if a rule is broken (including the output and
353+ // annotation limits) and failed_precondition if a step goes back, as in
354+ // UpdateJob.
304355 rpc CompleteJob(CompleteJobRequest) returns (CompleteJobResponse);
356+ // ResolveActions turns the "uses" of workflow and action steps into what the
357+ // runner needs to download the actions, as GitHub's action download info
358+ // does. Needs the job token; the job_id must be the one of the token. It
359+ // may be called as often as needed while the job runs (the runner calls it
360+ // once per level of composite actions, see the runner's depth limit of 9).
361+ // The runner then downloads each archive itself, from the sandbox, with the
362+ // token of the ResolvedAction when there is one.
363+ //
364+ // The response has one ResolvedAction per ActionRef, in the same order. A
365+ // reference that cannot be resolved (unknown repository, tag or branch, a
366+ // repository without access, a "uses" that GitHub itself would reject) does
367+ // not fail the call: its ResolvedAction has only ref and error, where error
368+ // is the message GitHub gives for the same failure, which the runner prints
369+ // as is. Only a failure of the call itself (credential, ended job, bad
370+ // request, server error) is an RPC error; the runner then fails the job.
371+ //
372+ // Resolution is stable for a run attempt: the same ref resolves to the same
373+ // sha (ResolvedAction.resolved_sha) for every job and every call of that
374+ // attempt. A re-run attempt resolves again. Tokens are not shared: each
375+ // job gets its own (ResolvedAction.token).
376+ //
377+ // Fails with unauthenticated or permission_denied as the other job RPCs,
378+ // with failed_precondition and a JobEnded detail if the job has ended, and
379+ // with invalid_argument if there are more than 100 refs or if a "uses" is a
380+ // local action ("./path") or a Docker image ("docker://"), which the runner
381+ // handles without the server.
382+ rpc ResolveActions(ResolveActionsRequest) returns (ResolveActionsResponse);
383+ // UploadSummary stores the job summary of one step: the Markdown that the
384+ // step wrote to the file named by GITHUB_STEP_SUMMARY. Needs the job token.
385+ // The runner masks it before sending it, and sends it after the step ended
386+ // and before CompleteJob.
387+ //
388+ // It is idempotent: the summary of a step is replaced by each call, so a
389+ // retry changes nothing, and an empty markdown removes the summary of the
390+ // step. The Markdown is at most 1 MiB (1,048,576 bytes) and valid UTF-8. The
391+ // server reads requests of up to 1 MiB plus the envelope (job id, step
392+ // number, framing), so a summary of exactly 1 MiB is accepted; clients set
393+ // their own message size limit above that.
394+ //
395+ // Fails with invalid_argument if a rule is broken (size, UTF-8, step number)
396+ // and with failed_precondition and a JobEnded detail if the job has ended.
397+ rpc UploadSummary(UploadSummaryRequest) returns (UploadSummaryResponse);
305398 }
306399
307400 // RegisterRunnerRequest describes the runner to register.
@@ -437,6 +530,18 @@ message Job {
437530 // Ref is the full ref name that triggered the run, for example
438531 // "refs/heads/main" or "refs/tags/v1.0.0".
439532 string ref = 14;
533+ // GithubToken is the job repository token, the "github.token" and
534+ // "secrets.GITHUB_TOKEN" of the job: "ermitegh_" and 43 base64url characters
535+ // (32 random bytes, no padding). Unlike job_token it is meant for the code of
536+ // the job: the runner gives it to the sandbox (actions/checkout clones with
537+ // it). It can only read the repository of the run (read-only, whatever
538+ // "permissions:" says), and is valid from the assignment until the job ends
539+ // (it is revoked then, on cancellation and when the job is lost). It is a
540+ // secret: the runner registers it in its masker before anything runs, and
541+ // neither the runner nor the server logs it. It is shown only in this
542+ // response, or in a repeat of the same poll_id; a new assignment of the same
543+ // job has a new one. Empty from servers without the feature "ci.actions".
544+ string github_token = 15;
440545 }
441546
442547 // UpdateJobRequest reports the steps of a running job.
@@ -449,6 +554,13 @@ message UpdateJobRequest {
449554 // implicit "Set up job" is 1, the steps of the workflow follow in order, and
450555 // the implicit "Complete job" is last.
451556 repeated StepUpdate steps = 2;
557+ // Annotations are the annotations of the steps that the call names: for
558+ // each step_number present, all the annotations of that step known so far,
559+ // not only the new ones (see the rules on UpdateJob). At most 10 per level
560+ // and per step, and each one within the limits of Annotation; otherwise the
561+ // whole call is invalid_argument. The server ignores them without the
562+ // feature "ci.annotations".
563+ repeated Annotation annotations = 3;
452564 }
453565
454566 // StepUpdate is the state of one step as the runner knows it.
@@ -472,6 +584,14 @@ message StepUpdate {
472584 // CompletedAt is when the step ended, by the clock of the runner. Required
473585 // when state is COMPLETED.
474586 google.protobuf.Timestamp completed_at = 6;
587+ // Outcome is how the step ended before continue-on-error was applied, GitHub's
588+ // "steps.<id>.outcome", whereas conclusion is the result after it
589+ // ("steps.<id>.conclusion"). Same values as conclusion. They differ only for
590+ // a step with continue-on-error that failed: outcome FAILURE, conclusion
591+ // SUCCESS. Set by the runner when state is COMPLETED and UNSPECIFIED
592+ // otherwise; UNSPECIFIED in a COMPLETED step means the same as conclusion
593+ // (a runner that does not know the field).
594+ CiConclusion outcome = 7;
475595 }
476596
477597 // UpdateJobResponse is empty.
@@ -504,6 +624,11 @@ message CompleteJobRequest {
504624 // Steps are the final steps, with the same meaning as UpdateJobRequest.steps,
505625 // applied in the same atomic step as the conclusion.
506626 repeated StepUpdate steps = 5;
627+ // Annotations are those of the steps that changed since the last call, with
628+ // the rules of UpdateJobRequest.annotations (all the annotations of each
629+ // step named). The server ignores them without the feature
630+ // "ci.annotations".
631+ repeated Annotation annotations = 6;
507632 }
508633
509634 // CompleteJobResponse is the stored result of a job.
@@ -620,3 +745,117 @@ message CancelRequest {
620745 // server.
621746 google.protobuf.Timestamp requested_at = 3;
622747 }
748+
749+// Annotation is a message attached to a step, and optionally to a place in a
750+// file of the repository, like an annotation of a GitHub check run. The runner
751+// produces them from the workflow commands ::notice, ::warning and ::error and
752+// from problem matchers, after masking.
753+//
754+// Limits, checked by the server: at most 10 annotations of each level per step
755+// (the runner sends no more: the ones beyond only reach the log, as with
756+// GitHub). A call that breaks a limit below is rejected whole with
757+// invalid_argument. Lengths count characters (Unicode code points), not bytes.
758+message Annotation {
759+ // Level is the severity. Required.
760+ AnnotationLevel level = 1;
761+ // Title is the optional title, at most 255 characters. Empty when not given.
762+ string title = 2;
763+ // Message is the text. Required, not empty, at most 4,096 characters; a
764+ // longer one is invalid_argument. The runner never sends a longer one: like
765+ // GitHub, it only logs such an annotation.
766+ string message = 3;
767+ // File is the path the annotation is about, relative to the repository when
768+ // the runner can tell, else as given. At most 1,024 characters. Empty when
769+ // the annotation is not about a file.
770+ string file = 4;
771+ // StartLine is the first line, counted from 1. 0 when not given.
772+ int32 start_line = 5;
773+ // EndLine is the last line, counted from 1, not before start_line. 0 when
774+ // not given.
775+ int32 end_line = 6;
776+ // StartColumn is the first column, counted from 1. 0 when not given.
777+ int32 start_column = 7;
778+ // EndColumn is the last column, counted from 1. 0 when not given.
779+ int32 end_column = 8;
780+ // StepNumber is the number of the step that produced it (see
781+ // StepUpdate.number), from 1 to 1,000. An annotation of a step inside a
782+ // composite action belongs to the step of the workflow that uses the action.
783+ int32 step_number = 9;
784+}
785+
786+// ActionRef is an action to resolve.
787+message ActionRef {
788+ // Uses is the value of "uses" as written, after expression evaluation, for
789+ // example "actions/checkout@v4" or "owner/repo/path/to/action@ref". Required.
790+ string uses = 1;
791+}
792+
793+// ResolveActionsRequest lists the actions to resolve.
794+message ResolveActionsRequest {
795+ // JobId is the Job.job_id. It must be the job of the token.
796+ string job_id = 1;
797+ // Refs are the actions to resolve, at most 100. Duplicates are allowed and
798+ // answered one by one.
799+ repeated ActionRef refs = 2;
800+}
801+
802+// ResolveActionsResponse holds the resolution of each action.
803+message ResolveActionsResponse {
804+ // Actions has one entry per ActionRef of the request, in the same order.
805+ repeated ResolvedAction actions = 1;
806+}
807+
808+// ResolvedAction is where to download one action, or why it cannot be.
809+message ResolvedAction {
810+ // Ref is the ActionRef.uses of the request, unchanged.
811+ string ref = 1;
812+ // NameWithOwner is the repository of the action, "owner/repo", with the
813+ // case of the repository as the source gives it. Empty when error is set.
814+ string name_with_owner = 2;
815+ // Path is the sub-path of the action inside the repository, a clean
816+ // relative path: "path/to/action" for "owner/repo/path/to/action@ref", with
817+ // "/" separators, no leading or trailing slash, no empty, "." or ".."
818+ // element, never absolute. Empty for an action at the root of the
819+ // repository.
820+ string path = 3;
821+ // ResolvedSha is the full id of the commit the ref points to, 40 lowercase
822+ // hexadecimal characters for SHA-1 (the commit that an annotated tag points
823+ // to, not the tag object). Stable for a run attempt: every job and every
824+ // call of the attempt gets the same value for the same ref. Empty when error
825+ // is set.
826+ string resolved_sha = 4;
827+ // TarballUrl is where to download the archive of the repository at
828+ // resolved_sha, a gzip-compressed tar with one top-level directory. The
829+ // runner downloads it from the sandbox. Empty when error is set.
830+ string tarball_url = 5;
831+ // Token authenticates the download: the runner sends it as "Authorization:
832+ // Basic" with the user "x-access-token". Empty for ACTION_SOURCE_GITHUB.
833+ // For ACTION_SOURCE_ERMITE it is a read token for that one repository,
834+ // valid until the job ends, so a job can use an action of another private
835+ // repository of the server that its own repository token cannot read. It is
836+ // issued per job and never shared across jobs (two jobs resolving the same
837+ // action get different tokens). It is a secret: the runner masks it, and neither the runner nor the server logs
838+ // it.
839+ string token = 6;
840+ // Source is where the action was found. UNSPECIFIED when error is set.
841+ ActionSource source = 7;
842+ // Error is why the reference could not be resolved, in the words of GitHub
843+ // for the same failure. Empty when the action was resolved, in which case
844+ // the other fields describe it.
845+ string error = 8;
846+}
847+
848+// UploadSummaryRequest carries the job summary of one step.
849+message UploadSummaryRequest {
850+ // JobId is the Job.job_id. It must be the job of the token.
851+ string job_id = 1;
852+ // StepNumber is the number of the step that wrote the summary (see
853+ // StepUpdate.number), from 1 to 1,000.
854+ int32 step_number = 2;
855+ // Markdown is the summary, masked, valid UTF-8, at most 1 MiB (1,048,576
856+ // bytes). Empty removes the summary of the step.
857+ string markdown = 3;
858+}
859+
860+// UploadSummaryResponse is empty.
861+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,21 @@ 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. The step of an annotation need not be in the steps of the
233+ // same call, and may already be COMPLETED: its annotations can still be
234+ // replaced (the rule that a COMPLETED step cannot change applies to
235+ // StepUpdate, not to annotations), until the job ends. A runner sends one
236+ // UpdateJob (or CompleteJob) at a time for a job, never concurrent calls,
237+ // so replacements apply in the order of the calls.
238+ //
239+ // Fails with invalid_argument if a rule is broken (including the annotation
240+ // limits), with failed_precondition if a step goes back (a COMPLETED step
241+ // cannot change, a step cannot go from IN_PROGRESS to PENDING) and with
242+ // failed_precondition and a JobEnded detail if the job has ended.
198 rpc UpdateJob(UpdateJobRequest) returns (UpdateJobResponse);243 rpc UpdateJob(UpdateJobRequest) returns (UpdateJobResponse);
199 // AppendLog appends lines to the log of a job. Needs the job token.244 // AppendLog appends lines to the log of a job. Needs the job token.
200 //245 //
@@ -299,9 +344,57 @@ service RunnerService {
299 // ends are completed by the server as CI_CONCLUSION_CANCELLED (or SKIPPED when344 // 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).345 // they never started and the job did not end by cancellation).
301 //346 //
302- // Fails with invalid_argument if a rule is broken (including the output347+ // Annotations are applied as in UpdateJob (replacement per step number, also
303- // limits) and failed_precondition if a step goes back, as in UpdateJob.348+ // for COMPLETED steps and steps not in steps), together with the conclusion.
349+ // Summaries are not part of this call: a runner sends them with
350+ // UploadSummary before it.
351+ //
352+ // Fails with invalid_argument if a rule is broken (including the output and
353+ // annotation limits) and failed_precondition if a step goes back, as in
354+ // UpdateJob.
304 rpc CompleteJob(CompleteJobRequest) returns (CompleteJobResponse);355 rpc CompleteJob(CompleteJobRequest) returns (CompleteJobResponse);
356+ // ResolveActions turns the "uses" of workflow and action steps into what the
357+ // runner needs to download the actions, as GitHub's action download info
358+ // does. Needs the job token; the job_id must be the one of the token. It
359+ // may be called as often as needed while the job runs (the runner calls it
360+ // once per level of composite actions, see the runner's depth limit of 9).
361+ // The runner then downloads each archive itself, from the sandbox, with the
362+ // token of the ResolvedAction when there is one.
363+ //
364+ // The response has one ResolvedAction per ActionRef, in the same order. A
365+ // reference that cannot be resolved (unknown repository, tag or branch, a
366+ // repository without access, a "uses" that GitHub itself would reject) does
367+ // not fail the call: its ResolvedAction has only ref and error, where error
368+ // is the message GitHub gives for the same failure, which the runner prints
369+ // as is. Only a failure of the call itself (credential, ended job, bad
370+ // request, server error) is an RPC error; the runner then fails the job.
371+ //
372+ // Resolution is stable for a run attempt: the same ref resolves to the same
373+ // sha (ResolvedAction.resolved_sha) for every job and every call of that
374+ // attempt. A re-run attempt resolves again. Tokens are not shared: each
375+ // job gets its own (ResolvedAction.token).
376+ //
377+ // Fails with unauthenticated or permission_denied as the other job RPCs,
378+ // with failed_precondition and a JobEnded detail if the job has ended, and
379+ // with invalid_argument if there are more than 100 refs or if a "uses" is a
380+ // local action ("./path") or a Docker image ("docker://"), which the runner
381+ // handles without the server.
382+ rpc ResolveActions(ResolveActionsRequest) returns (ResolveActionsResponse);
383+ // UploadSummary stores the job summary of one step: the Markdown that the
384+ // step wrote to the file named by GITHUB_STEP_SUMMARY. Needs the job token.
385+ // The runner masks it before sending it, and sends it after the step ended
386+ // and before CompleteJob.
387+ //
388+ // It is idempotent: the summary of a step is replaced by each call, so a
389+ // retry changes nothing, and an empty markdown removes the summary of the
390+ // step. The Markdown is at most 1 MiB (1,048,576 bytes) and valid UTF-8. The
391+ // server reads requests of up to 1 MiB plus the envelope (job id, step
392+ // number, framing), so a summary of exactly 1 MiB is accepted; clients set
393+ // their own message size limit above that.
394+ //
395+ // Fails with invalid_argument if a rule is broken (size, UTF-8, step number)
396+ // and with failed_precondition and a JobEnded detail if the job has ended.
397+ rpc UploadSummary(UploadSummaryRequest) returns (UploadSummaryResponse);
305 }398 }
306 399
307 // RegisterRunnerRequest describes the runner to register.400 // RegisterRunnerRequest describes the runner to register.
@@ -437,6 +530,18 @@ message Job {
437 // Ref is the full ref name that triggered the run, for example530 // Ref is the full ref name that triggered the run, for example
438 // "refs/heads/main" or "refs/tags/v1.0.0".531 // "refs/heads/main" or "refs/tags/v1.0.0".
439 string ref = 14;532 string ref = 14;
533+ // GithubToken is the job repository token, the "github.token" and
534+ // "secrets.GITHUB_TOKEN" of the job: "ermitegh_" and 43 base64url characters
535+ // (32 random bytes, no padding). Unlike job_token it is meant for the code of
536+ // the job: the runner gives it to the sandbox (actions/checkout clones with
537+ // it). It can only read the repository of the run (read-only, whatever
538+ // "permissions:" says), and is valid from the assignment until the job ends
539+ // (it is revoked then, on cancellation and when the job is lost). It is a
540+ // secret: the runner registers it in its masker before anything runs, and
541+ // neither the runner nor the server logs it. It is shown only in this
542+ // response, or in a repeat of the same poll_id; a new assignment of the same
543+ // job has a new one. Empty from servers without the feature "ci.actions".
544+ string github_token = 15;
440 }545 }
441 546
442 // UpdateJobRequest reports the steps of a running job.547 // UpdateJobRequest reports the steps of a running job.
@@ -449,6 +554,13 @@ message UpdateJobRequest {
449 // implicit "Set up job" is 1, the steps of the workflow follow in order, and554 // implicit "Set up job" is 1, the steps of the workflow follow in order, and
450 // the implicit "Complete job" is last.555 // the implicit "Complete job" is last.
451 repeated StepUpdate steps = 2;556 repeated StepUpdate steps = 2;
557+ // Annotations are the annotations of the steps that the call names: for
558+ // each step_number present, all the annotations of that step known so far,
559+ // not only the new ones (see the rules on UpdateJob). At most 10 per level
560+ // and per step, and each one within the limits of Annotation; otherwise the
561+ // whole call is invalid_argument. The server ignores them without the
562+ // feature "ci.annotations".
563+ repeated Annotation annotations = 3;
452 }564 }
453 565
454 // StepUpdate is the state of one step as the runner knows it.566 // StepUpdate is the state of one step as the runner knows it.
@@ -472,6 +584,14 @@ message StepUpdate {
472 // CompletedAt is when the step ended, by the clock of the runner. Required584 // CompletedAt is when the step ended, by the clock of the runner. Required
473 // when state is COMPLETED.585 // when state is COMPLETED.
474 google.protobuf.Timestamp completed_at = 6;586 google.protobuf.Timestamp completed_at = 6;
587+ // Outcome is how the step ended before continue-on-error was applied, GitHub's
588+ // "steps.<id>.outcome", whereas conclusion is the result after it
589+ // ("steps.<id>.conclusion"). Same values as conclusion. They differ only for
590+ // a step with continue-on-error that failed: outcome FAILURE, conclusion
591+ // SUCCESS. Set by the runner when state is COMPLETED and UNSPECIFIED
592+ // otherwise; UNSPECIFIED in a COMPLETED step means the same as conclusion
593+ // (a runner that does not know the field).
594+ CiConclusion outcome = 7;
475 }595 }
476 596
477 // UpdateJobResponse is empty.597 // UpdateJobResponse is empty.
@@ -504,6 +624,11 @@ message CompleteJobRequest {
504 // Steps are the final steps, with the same meaning as UpdateJobRequest.steps,624 // Steps are the final steps, with the same meaning as UpdateJobRequest.steps,
505 // applied in the same atomic step as the conclusion.625 // applied in the same atomic step as the conclusion.
506 repeated StepUpdate steps = 5;626 repeated StepUpdate steps = 5;
627+ // Annotations are those of the steps that changed since the last call, with
628+ // the rules of UpdateJobRequest.annotations (all the annotations of each
629+ // step named). The server ignores them without the feature
630+ // "ci.annotations".
631+ repeated Annotation annotations = 6;
507 }632 }
508 633
509 // CompleteJobResponse is the stored result of a job.634 // CompleteJobResponse is the stored result of a job.
@@ -620,3 +745,117 @@ message CancelRequest {
620 // server.745 // server.
621 google.protobuf.Timestamp requested_at = 3;746 google.protobuf.Timestamp requested_at = 3;
622 }747 }
748+
749+// Annotation is a message attached to a step, and optionally to a place in a
750+// file of the repository, like an annotation of a GitHub check run. The runner
751+// produces them from the workflow commands ::notice, ::warning and ::error and
752+// from problem matchers, after masking.
753+//
754+// Limits, checked by the server: at most 10 annotations of each level per step
755+// (the runner sends no more: the ones beyond only reach the log, as with
756+// GitHub). A call that breaks a limit below is rejected whole with
757+// invalid_argument. Lengths count characters (Unicode code points), not bytes.
758+message Annotation {
759+ // Level is the severity. Required.
760+ AnnotationLevel level = 1;
761+ // Title is the optional title, at most 255 characters. Empty when not given.
762+ string title = 2;
763+ // Message is the text. Required, not empty, at most 4,096 characters; a
764+ // longer one is invalid_argument. The runner never sends a longer one: like
765+ // GitHub, it only logs such an annotation.
766+ string message = 3;
767+ // File is the path the annotation is about, relative to the repository when
768+ // the runner can tell, else as given. At most 1,024 characters. Empty when
769+ // the annotation is not about a file.
770+ string file = 4;
771+ // StartLine is the first line, counted from 1. 0 when not given.
772+ int32 start_line = 5;
773+ // EndLine is the last line, counted from 1, not before start_line. 0 when
774+ // not given.
775+ int32 end_line = 6;
776+ // StartColumn is the first column, counted from 1. 0 when not given.
777+ int32 start_column = 7;
778+ // EndColumn is the last column, counted from 1. 0 when not given.
779+ int32 end_column = 8;
780+ // StepNumber is the number of the step that produced it (see
781+ // StepUpdate.number), from 1 to 1,000. An annotation of a step inside a
782+ // composite action belongs to the step of the workflow that uses the action.
783+ int32 step_number = 9;
784+}
785+
786+// ActionRef is an action to resolve.
787+message ActionRef {
788+ // Uses is the value of "uses" as written, after expression evaluation, for
789+ // example "actions/checkout@v4" or "owner/repo/path/to/action@ref". Required.
790+ string uses = 1;
791+}
792+
793+// ResolveActionsRequest lists the actions to resolve.
794+message ResolveActionsRequest {
795+ // JobId is the Job.job_id. It must be the job of the token.
796+ string job_id = 1;
797+ // Refs are the actions to resolve, at most 100. Duplicates are allowed and
798+ // answered one by one.
799+ repeated ActionRef refs = 2;
800+}
801+
802+// ResolveActionsResponse holds the resolution of each action.
803+message ResolveActionsResponse {
804+ // Actions has one entry per ActionRef of the request, in the same order.
805+ repeated ResolvedAction actions = 1;
806+}
807+
808+// ResolvedAction is where to download one action, or why it cannot be.
809+message ResolvedAction {
810+ // Ref is the ActionRef.uses of the request, unchanged.
811+ string ref = 1;
812+ // NameWithOwner is the repository of the action, "owner/repo", with the
813+ // case of the repository as the source gives it. Empty when error is set.
814+ string name_with_owner = 2;
815+ // Path is the sub-path of the action inside the repository, a clean
816+ // relative path: "path/to/action" for "owner/repo/path/to/action@ref", with
817+ // "/" separators, no leading or trailing slash, no empty, "." or ".."
818+ // element, never absolute. Empty for an action at the root of the
819+ // repository.
820+ string path = 3;
821+ // ResolvedSha is the full id of the commit the ref points to, 40 lowercase
822+ // hexadecimal characters for SHA-1 (the commit that an annotated tag points
823+ // to, not the tag object). Stable for a run attempt: every job and every
824+ // call of the attempt gets the same value for the same ref. Empty when error
825+ // is set.
826+ string resolved_sha = 4;
827+ // TarballUrl is where to download the archive of the repository at
828+ // resolved_sha, a gzip-compressed tar with one top-level directory. The
829+ // runner downloads it from the sandbox. Empty when error is set.
830+ string tarball_url = 5;
831+ // Token authenticates the download: the runner sends it as "Authorization:
832+ // Basic" with the user "x-access-token". Empty for ACTION_SOURCE_GITHUB.
833+ // For ACTION_SOURCE_ERMITE it is a read token for that one repository,
834+ // valid until the job ends, so a job can use an action of another private
835+ // repository of the server that its own repository token cannot read. It is
836+ // issued per job and never shared across jobs (two jobs resolving the same
837+ // action get different tokens). It is a secret: the runner masks it, and neither the runner nor the server logs
838+ // it.
839+ string token = 6;
840+ // Source is where the action was found. UNSPECIFIED when error is set.
841+ ActionSource source = 7;
842+ // Error is why the reference could not be resolved, in the words of GitHub
843+ // for the same failure. Empty when the action was resolved, in which case
844+ // the other fields describe it.
845+ string error = 8;
846+}
847+
848+// UploadSummaryRequest carries the job summary of one step.
849+message UploadSummaryRequest {
850+ // JobId is the Job.job_id. It must be the job of the token.
851+ string job_id = 1;
852+ // StepNumber is the number of the step that wrote the summary (see
853+ // StepUpdate.number), from 1 to 1,000.
854+ int32 step_number = 2;
855+ // Markdown is the summary, masked, valid UTF-8, at most 1 MiB (1,048,576
856+ // bytes). Empty removes the summary of the step.
857+ string markdown = 3;
858+}
859+
860+// UploadSummaryResponse is empty.
861+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