Merge pull request 'feat(ci): add actions, annotations and summaries to the CI protocol' (#9) from feat/ci2-protocol into main
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. | |||
| 22 | 22 | | `MergeRequestService` | `CreateMergeRequest`, `ListMergeRequests`, `GetMergeRequest` (commits, diff, comments), `AddComment`, `ResolveComment`, `CloseMergeRequest`, `MergeMergeRequest`, `MarkRead` | | |
| 23 | 23 | | `SessionService` | `CreateLoginLink`, `SignOutEverywhere` (owner only) | | |
| 24 | 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 | | |
| 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` | | |
| 27 | 27 | | `ServerService` | `GetVersion` (server version and feature list), `GetSigningKey` (commit signing key) | | |
| 28 | 28 | ||
| 29 | 29 | 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 ( | |||
| 41 | 41 | RunServiceGetRunProcedure = "/ermite.RunService/GetRun" | |
| 42 | 42 | // RunServiceGetJobProcedure is the fully-qualified name of the RunService's GetJob RPC. | |
| 43 | 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 | 50 | // RunServiceGetJobLogProcedure is the fully-qualified name of the RunService's GetJobLog RPC. | |
| 45 | 51 | RunServiceGetJobLogProcedure = "/ermite.RunService/GetJobLog" | |
| 46 | 52 | // RunServiceCancelRunProcedure is the fully-qualified name of the RunService's CancelRun RPC. | |
| @@ -70,6 +76,25 @@ type RunServiceClient interface { | |||
| 70 | 76 | // GetJob returns one job with its steps. Fails with not_found if the job | |
| 71 | 77 | // or the attempt does not exist. | |
| 72 | 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 | 98 | // GetJobLog streams the lines of the log of a job, from a line number. The | |
| 74 | 99 | // server sends them in order in messages of at most 1,000 lines and 64 KiB of | |
| 75 | 100 | // text each. | |
| @@ -148,6 +173,18 @@ func NewRunServiceClient(httpClient connect.HTTPClient, baseURL string, opts ... | |||
| 148 | 173 | connect.WithSchema(runServiceMethods.ByName("GetJob")), | |
| 149 | 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 | 188 | getJobLog: connect.NewClient[ermite.GetJobLogRequest, ermite.GetJobLogResponse]( | |
| 152 | 189 | httpClient, | |
| 153 | 190 | baseURL+RunServiceGetJobLogProcedure, | |
| @@ -189,15 +226,17 @@ func NewRunServiceClient(httpClient connect.HTTPClient, baseURL string, opts ... | |||
| 189 | 226 | ||
| 190 | 227 | // runServiceClient implements RunServiceClient. | |
| 191 | 228 | 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] | |
| 201 | 240 | } | |
| 202 | 241 | ||
| 203 | 242 | // ListRuns calls ermite.RunService.ListRuns. | |
| @@ -215,6 +254,16 @@ func (c *runServiceClient) GetJob(ctx context.Context, req *connect.Request[ermi | |||
| 215 | 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 | 267 | // GetJobLog calls ermite.RunService.GetJobLog. | |
| 219 | 268 | func (c *runServiceClient) GetJobLog(ctx context.Context, req *connect.Request[ermite.GetJobLogRequest]) (*connect.ServerStreamForClient[ermite.GetJobLogResponse], error) { | |
| 220 | 269 | return c.getJobLog.CallServerStream(ctx, req) | |
| @@ -260,6 +309,25 @@ type RunServiceHandler interface { | |||
| 260 | 309 | // GetJob returns one job with its steps. Fails with not_found if the job | |
| 261 | 310 | // or the attempt does not exist. | |
| 262 | 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 | 331 | // GetJobLog streams the lines of the log of a job, from a line number. The | |
| 264 | 332 | // server sends them in order in messages of at most 1,000 lines and 64 KiB of | |
| 265 | 333 | // text each. | |
| @@ -334,6 +402,18 @@ func NewRunServiceHandler(svc RunServiceHandler, opts ...connect.HandlerOption) | |||
| 334 | 402 | connect.WithSchema(runServiceMethods.ByName("GetJob")), | |
| 335 | 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 | 417 | runServiceGetJobLogHandler := connect.NewServerStreamHandler( | |
| 338 | 418 | RunServiceGetJobLogProcedure, | |
| 339 | 419 | svc.GetJobLog, | |
| @@ -378,6 +458,10 @@ func NewRunServiceHandler(svc RunServiceHandler, opts ...connect.HandlerOption) | |||
| 378 | 458 | runServiceGetRunHandler.ServeHTTP(w, r) | |
| 379 | 459 | case RunServiceGetJobProcedure: | |
| 380 | 460 | runServiceGetJobHandler.ServeHTTP(w, r) | |
| 461 | + | case RunServiceListAnnotationsProcedure: | |
| 462 | + | runServiceListAnnotationsHandler.ServeHTTP(w, r) | |
| 463 | + | case RunServiceGetStepSummariesProcedure: | |
| 464 | + | runServiceGetStepSummariesHandler.ServeHTTP(w, r) | |
| 381 | 465 | case RunServiceGetJobLogProcedure: | |
| 382 | 466 | runServiceGetJobLogHandler.ServeHTTP(w, r) | |
| 383 | 467 | case RunServiceCancelRunProcedure: | |
| @@ -411,6 +495,14 @@ func (UnimplementedRunServiceHandler) GetJob(context.Context, *connect.Request[e | |||
| 411 | 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 | 506 | func (UnimplementedRunServiceHandler) GetJobLog(context.Context, *connect.Request[ermite.GetJobLogRequest], *connect.ServerStream[ermite.GetJobLogResponse]) error { | |
| 415 | 507 | return connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunService.GetJobLog is not implemented")) | |
| 416 | 508 | } | |
| @@ -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 job | 76 | // 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. The | 98 | // 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 of | 99 | // 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 job | 309 | // 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. The | 331 | // 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 of | 332 | // 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 ( | |||
| 51 | 51 | // RunnerServiceCompleteJobProcedure is the fully-qualified name of the RunnerService's CompleteJob | |
| 52 | 52 | // RPC. | |
| 53 | 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 | 62 | // RunnerServiceClient is a client for the ermite.RunnerService service. | |
| @@ -105,10 +111,21 @@ type RunnerServiceClient interface { | |||
| 105 | 111 | // identical call changes nothing, so a runner may retry after a network | |
| 106 | 112 | // error. To end the job use CompleteJob. | |
| 107 | 113 | // | |
| 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. | |
| 112 | 129 | UpdateJob(context.Context, *connect.Request[ermite.UpdateJobRequest]) (*connect.Response[ermite.UpdateJobResponse], error) | |
| 113 | 130 | // AppendLog appends lines to the log of a job. Needs the job token. | |
| 114 | 131 | // | |
| @@ -213,9 +230,57 @@ type RunnerServiceClient interface { | |||
| 213 | 230 | // ends are completed by the server as CI_CONCLUSION_CANCELLED (or SKIPPED when | |
| 214 | 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 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. | |
| 218 | 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 | 286 | // 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 | 336 | connect.WithSchema(runnerServiceMethods.ByName("CompleteJob")), | |
| 272 | 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 | 360 | heartbeat *connect.Client[ermite.HeartbeatRequest, ermite.HeartbeatResponse] | |
| 284 | 361 | watchJob *connect.Client[ermite.WatchJobRequest, ermite.WatchJobResponse] | |
| 285 | 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 | 367 | // RegisterRunner calls ermite.RunnerService.RegisterRunner. | |
| @@ -320,6 +399,16 @@ func (c *runnerServiceClient) CompleteJob(ctx context.Context, req *connect.Requ | |||
| 320 | 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 | 412 | // RunnerServiceHandler is an implementation of the ermite.RunnerService service. | |
| 324 | 413 | type RunnerServiceHandler interface { | |
| 325 | 414 | // RegisterRunner registers a runner and returns its runner token, which is | |
| @@ -372,10 +461,21 @@ type RunnerServiceHandler interface { | |||
| 372 | 461 | // identical call changes nothing, so a runner may retry after a network | |
| 373 | 462 | // error. To end the job use CompleteJob. | |
| 374 | 463 | // | |
| 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. | |
| 379 | 479 | UpdateJob(context.Context, *connect.Request[ermite.UpdateJobRequest]) (*connect.Response[ermite.UpdateJobResponse], error) | |
| 380 | 480 | // AppendLog appends lines to the log of a job. Needs the job token. | |
| 381 | 481 | // | |
| @@ -480,9 +580,57 @@ type RunnerServiceHandler interface { | |||
| 480 | 580 | // ends are completed by the server as CI_CONCLUSION_CANCELLED (or SKIPPED when | |
| 481 | 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 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. | |
| 485 | 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 | 636 | // NewRunnerServiceHandler builds an HTTP handler from the service implementation. It returns the | |
| @@ -534,6 +682,18 @@ func NewRunnerServiceHandler(svc RunnerServiceHandler, opts ...connect.HandlerOp | |||
| 534 | 682 | connect.WithSchema(runnerServiceMethods.ByName("CompleteJob")), | |
| 535 | 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 | 697 | return "/ermite.RunnerService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| 538 | 698 | switch r.URL.Path { | |
| 539 | 699 | case RunnerServiceRegisterRunnerProcedure: | |
| @@ -550,6 +710,10 @@ func NewRunnerServiceHandler(svc RunnerServiceHandler, opts ...connect.HandlerOp | |||
| 550 | 710 | runnerServiceWatchJobHandler.ServeHTTP(w, r) | |
| 551 | 711 | case RunnerServiceCompleteJobProcedure: | |
| 552 | 712 | runnerServiceCompleteJobHandler.ServeHTTP(w, r) | |
| 713 | + | case RunnerServiceResolveActionsProcedure: | |
| 714 | + | runnerServiceResolveActionsHandler.ServeHTTP(w, r) | |
| 715 | + | case RunnerServiceUploadSummaryProcedure: | |
| 716 | + | runnerServiceUploadSummaryHandler.ServeHTTP(w, r) | |
| 553 | 717 | default: | |
| 554 | 718 | http.NotFound(w, r) | |
| 555 | 719 | } | |
| @@ -586,3 +750,11 @@ func (UnimplementedRunnerServiceHandler) WatchJob(context.Context, *connect.Requ | |||
| 586 | 750 | func (UnimplementedRunnerServiceHandler) CompleteJob(context.Context, *connect.Request[ermite.CompleteJobRequest]) (*connect.Response[ermite.CompleteJobResponse], error) { | |
| 587 | 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 | + | } | |
| @@ -51,6 +51,12 @@ const ( | |||||
| 51 | // RunnerServiceCompleteJobProcedure is the fully-qualified name of the RunnerService's CompleteJob | 51 | // 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 network | 111 | // 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_precondition | 114 | + | // 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 from | 115 | + | // step_number present in annotations, the annotations of that step stored |
| 110 | - | // IN_PROGRESS to PENDING) and with failed_precondition and a JobEnded detail | 116 | + | // 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 when | 230 | // 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 output | 233 | + | // 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, it | 286 | // 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 is | 414 | // 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 network | 461 | // 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_precondition | 464 | + | // 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 from | 465 | + | // step_number present in annotations, the annotations of that step stored |
| 377 | - | // IN_PROGRESS to PENDING) and with failed_precondition and a JobEnded detail | 466 | + | // 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 when | 580 | // 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 output | 583 | + | // 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 the | 636 | // 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) { | |||
| 54 | 54 | {ermiteconnect.RunnerServiceHeartbeatProcedure, "/ermite.RunnerService/Heartbeat"}, | |
| 55 | 55 | {ermiteconnect.RunnerServiceWatchJobProcedure, "/ermite.RunnerService/WatchJob"}, | |
| 56 | 56 | {ermiteconnect.RunnerServiceCompleteJobProcedure, "/ermite.RunnerService/CompleteJob"}, | |
| 57 | + | {ermiteconnect.RunnerServiceResolveActionsProcedure, "/ermite.RunnerService/ResolveActions"}, | |
| 58 | + | {ermiteconnect.RunnerServiceUploadSummaryProcedure, "/ermite.RunnerService/UploadSummary"}, | |
| 57 | 59 | {ermiteconnect.RunServiceListRunsProcedure, "/ermite.RunService/ListRuns"}, | |
| 58 | 60 | {ermiteconnect.RunServiceGetRunProcedure, "/ermite.RunService/GetRun"}, | |
| 59 | 61 | {ermiteconnect.RunServiceGetJobProcedure, "/ermite.RunService/GetJob"}, | |
| 60 | 62 | {ermiteconnect.RunServiceGetJobLogProcedure, "/ermite.RunService/GetJobLog"}, | |
| 63 | + | {ermiteconnect.RunServiceListAnnotationsProcedure, "/ermite.RunService/ListAnnotations"}, | |
| 64 | + | {ermiteconnect.RunServiceGetStepSummariesProcedure, "/ermite.RunService/GetStepSummaries"}, | |
| 61 | 65 | {ermiteconnect.RunServiceCancelRunProcedure, "/ermite.RunService/CancelRun"}, | |
| 62 | 66 | {ermiteconnect.RunServiceRerunRunProcedure, "/ermite.RunService/RerunRun"}, | |
| 63 | 67 | {ermiteconnect.RunServiceListChecksProcedure, "/ermite.RunService/ListChecks"}, | |
| @@ -120,6 +124,13 @@ func TestEnumNumbers(t *testing.T) { | |||
| 120 | 124 | {"CANCEL_REASON_USER", int32(ermite.CancelReason_CANCEL_REASON_USER), 1}, | |
| 121 | 125 | {"CANCEL_REASON_FAIL_FAST", int32(ermite.CancelReason_CANCEL_REASON_FAIL_FAST), 2}, | |
| 122 | 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 | 135 | for _, e := range enums { | |
| 125 | 136 | if e.got != e.want { | |
| @@ -350,3 +361,92 @@ func TestRunNameField(t *testing.T) { | |||
| 350 | 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 | + | } | |
| @@ -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 { | |||
| 172 | 172 | // IN_PROGRESS step it is the number of lines held at the time of the | |
| 173 | 173 | // call and grows; for a step without a line it equals log_start. | |
| 174 | 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 | 182 | // Check is the result of one job on one commit, the unit shown next to a | |
| @@ -238,9 +243,9 @@ message Runner { | |||
| 238 | 243 | // RunService reads and controls CI runs, like `gh run` does on GitHub. It is for | |
| 239 | 244 | // owners and their tools; runners use RunnerService. | |
| 240 | 245 | // | |
| 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 | |
| 244 | 249 | // the caller cannot read gives not_found; so does a run or a job that belongs | |
| 245 | 250 | // to another repository than the one named (ids are global but are always | |
| 246 | 251 | // checked against the repository). Cancelling and re-running need the ci:write | |
| @@ -255,10 +260,13 @@ message Runner { | |||
| 255 | 260 | // page_token is the next_page_token of a previous response for the same | |
| 256 | 261 | // request (other fields must be unchanged). | |
| 257 | 262 | // | |
| 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 | |
| 260 | 266 | // implement the runners side list "ci.runners", as RunnerService does. A server | |
| 261 | 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 | 270 | service RunService { | |
| 263 | 271 | // ListRuns lists the runs of a repository, highest id (newest) first. Each | |
| 264 | 272 | // run is described by its latest attempt. The filters narrow the result; a | |
| @@ -273,6 +281,25 @@ service RunService { | |||
| 273 | 281 | // GetJob returns one job with its steps. Fails with not_found if the job | |
| 274 | 282 | // or the attempt does not exist. | |
| 275 | 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 | 303 | // GetJobLog streams the lines of the log of a job, from a line number. The | |
| 277 | 304 | // server sends them in order in messages of at most 1,000 lines and 64 KiB of | |
| 278 | 305 | // text each. | |
| @@ -393,6 +420,67 @@ message GetJobResponse { | |||
| 393 | 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 | 484 | // GetJobLogRequest selects the lines to stream. | |
| 397 | 485 | message GetJobLogRequest { | |
| 398 | 486 | // 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 the | 172 | // 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 a | 182 | // 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 for | 243 | // 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 needs | 246 | + | // Runs, jobs, checks, logs, annotations and summaries belong to a repository, |
| 242 | - | // the same access as reading it: nothing for a public repository, the | 247 | + | // 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 that | 248 | + | // 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 belongs | 249 | // 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 always | 250 | // 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:write | 251 | // 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 same | 260 | // 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 and | 263 | + | // Servers that implement the runs side (every RPC but ListRunners, |
| 259 | - | // RemoveRunner) list "ci.runs" in GetVersionResponse.features; those that | 264 | + | // 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 server | 266 | // 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. Each | 271 | // 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; a | 272 | // 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 job | 281 | // 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. The | 303 | // 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 of | 304 | // 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 { | |||
| 93 | 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 | 122 | // RunnerService is what a CI runner (ermite-runner) calls to get jobs and | |
| 97 | 123 | // report on them. It is not for people: owners use RunService. | |
| 98 | 124 | // | |
| @@ -104,8 +130,8 @@ enum CancelReason { | |||
| 104 | 130 | // - FetchJob: the runner token returned by RegisterRunner. It identifies the | |
| 105 | 131 | // runner and only works for FetchJob. It is "ermiterun_" followed by 43 | |
| 106 | 132 | // 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 | |
| 109 | 135 | // one assignment. It is "ermitejob_" followed by 43 base64url characters | |
| 110 | 136 | // (32 random bytes, no padding). The job_id of the request must be the | |
| 111 | 137 | // one of the token. | |
| @@ -120,8 +146,8 @@ enum CancelReason { | |||
| 120 | 146 | // | |
| 121 | 147 | // The end of a job. A job ends when its conclusion is set, by CompleteJob or | |
| 122 | 148 | // 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 | |
| 125 | 151 | // Connect error details) that gives the conclusion; the runner stops the job | |
| 126 | 152 | // and sends nothing more. The job token is still recognised for 24 hours after | |
| 127 | 153 | // the end, so that these calls can say so and so that CompleteJob, the only | |
| @@ -132,14 +158,22 @@ enum CancelReason { | |||
| 132 | 158 | // Secrets and trust. Runner tokens and job tokens are secrets: the server | |
| 133 | 159 | // never logs them and clients must not log them. The job token stays in the | |
| 134 | 160 | // 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 | |
| 136 | 164 | // files, contexts and (from CI-2) their code, and from CI-3 their secrets: the | |
| 137 | 165 | // runner:register scope therefore effectively gives read access to the code | |
| 138 | 166 | // and the secrets of every repository, private ones included. Give it only to | |
| 139 | 167 | // runners you operate. | |
| 140 | 168 | // | |
| 141 | 169 | // 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 | 177 | service RunnerService { | |
| 144 | 178 | // RegisterRunner registers a runner and returns its runner token, which is | |
| 145 | 179 | // shown only in this response; the runner keeps it. The caller needs the | |
| @@ -191,10 +225,21 @@ service RunnerService { | |||
| 191 | 225 | // identical call changes nothing, so a runner may retry after a network | |
| 192 | 226 | // error. To end the job use CompleteJob. | |
| 193 | 227 | // | |
| 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. | |
| 198 | 243 | rpc UpdateJob(UpdateJobRequest) returns (UpdateJobResponse); | |
| 199 | 244 | // AppendLog appends lines to the log of a job. Needs the job token. | |
| 200 | 245 | // | |
| @@ -299,9 +344,57 @@ service RunnerService { | |||
| 299 | 344 | // ends are completed by the server as CI_CONCLUSION_CANCELLED (or SKIPPED when | |
| 300 | 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 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. | |
| 304 | 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 | 400 | // RegisterRunnerRequest describes the runner to register. | |
| @@ -437,6 +530,18 @@ message Job { | |||
| 437 | 530 | // Ref is the full ref name that triggered the run, for example | |
| 438 | 531 | // "refs/heads/main" or "refs/tags/v1.0.0". | |
| 439 | 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 | 547 | // UpdateJobRequest reports the steps of a running job. | |
| @@ -449,6 +554,13 @@ message UpdateJobRequest { | |||
| 449 | 554 | // implicit "Set up job" is 1, the steps of the workflow follow in order, and | |
| 450 | 555 | // the implicit "Complete job" is last. | |
| 451 | 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 | 566 | // StepUpdate is the state of one step as the runner knows it. | |
| @@ -472,6 +584,14 @@ message StepUpdate { | |||
| 472 | 584 | // CompletedAt is when the step ended, by the clock of the runner. Required | |
| 473 | 585 | // when state is COMPLETED. | |
| 474 | 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 | 597 | // UpdateJobResponse is empty. | |
| @@ -504,6 +624,11 @@ message CompleteJobRequest { | |||
| 504 | 624 | // Steps are the final steps, with the same meaning as UpdateJobRequest.steps, | |
| 505 | 625 | // applied in the same atomic step as the conclusion. | |
| 506 | 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 | 634 | // CompleteJobResponse is the stored result of a job. | |
| @@ -620,3 +745,117 @@ message CancelRequest { | |||
| 620 | 745 | // server. | |
| 621 | 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 {} | |
| @@ -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 and | 122 | // 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 the | 130 | // - FetchJob: the runner token returned by RegisterRunner. It identifies the | ||
| 105 | // runner and only works for FetchJob. It is "ermiterun_" followed by 43 | 131 | // 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 of | 133 | + | // - UpdateJob, AppendLog, Heartbeat, WatchJob, CompleteJob, ResolveActions, |
| 108 | - | // the Job that FetchJob returned. It is valid for that one job and that | 134 | + | // 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 characters | 135 | // one assignment. It is "ermitejob_" followed by 43 base64url characters | ||
| 110 | // (32 random bytes, no padding). The job_id of the request must be the | 136 | // (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 or | 147 | // 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, queue | 148 | // by the server (cancellation not honoured, lost runner, log limit, queue | ||
| 123 | - | // timeout). From then on UpdateJob, AppendLog, Heartbeat and WatchJob answer | 149 | + | // timeout). From then on UpdateJob, AppendLog, Heartbeat, WatchJob, |
| 124 | - | // failed_precondition with a JobEnded error detail (a JobEnded message in the | 150 | + | // 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 job | 151 | // 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 after | 152 | // 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 only | 153 | // 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 server | 158 | // 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 the | 159 | // 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's | 160 | // 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 | |||
| 136 | // files, contexts and (from CI-2) their code, and from CI-3 their secrets: the | 164 | // 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 code | 165 | // runner:register scope therefore effectively gives read access to the code | ||
| 138 | // and the secrets of every repository, private ones included. Give it only to | 166 | // 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" in | 169 | // 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 is | 178 | // RegisterRunner registers a runner and returns its runner token, which is | ||
| 145 | // shown only in this response; the runner keeps it. The caller needs the | 179 | // 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 network | 225 | // 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_precondition | 228 | + | // 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 from | 229 | + | // step_number present in annotations, the annotations of that step stored |
| 196 | - | // IN_PROGRESS to PENDING) and with failed_precondition and a JobEnded detail | 230 | + | // 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 when | 344 | // 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 output | 347 | + | // 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 example | 530 | // 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, and | 554 | // 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. Required | 584 | // 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 { | |||
| 35 | 35 | // identifiers are "ci.runs" (RunService runs, jobs, logs, checks, and the | |
| 36 | 36 | // run_updated and job_updated events), "ci.runs.filters" (ListRuns honours | |
| 37 | 37 | // 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 | 43 | repeated string features = 2; | |
| 40 | 44 | } | |
| 41 | 45 | ||
| @@ -35,7 +35,11 @@ message GetVersionResponse { | |||||
| 35 | // identifiers are "ci.runs" (RunService runs, jobs, logs, checks, and the | 35 | // identifiers are "ci.runs" (RunService runs, jobs, logs, checks, and the | ||
| 36 | // run_updated and job_updated events), "ci.runs.filters" (ListRuns honours | 36 | // run_updated and job_updated events), "ci.runs.filters" (ListRuns honours | ||
| 37 | // its filters) and "ci.runners" (RunnerService, and RunService.ListRunners | 37 | // 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 | ||||