Merge pull request 'feat(api): add the CI runner and run services' (#5) from feat/ci-protocol into main
13 files · +6934 -34
M README.md +4 -2
| @@ -2,7 +2,7 @@ | |||
| 2 | 2 | ||
| 3 | 3 | The public API contract of [ermite](https://git.bnema.dev/bnema/ermite), a single-user Git forge: protobuf definitions for its ConnectRPC services and the Go code generated from them. | |
| 4 | 4 | ||
| 5 | - | Status: early development. The contract defines seven services (below); the server that implements them is being built. | |
| 5 | + | Status: early development. The contract defines nine services (below); the server that implements them is being built. | |
| 6 | 6 | ||
| 7 | 7 | ```go | |
| 8 | 8 | import "bnema.dev/ermite-api/gen/ermite" | |
| @@ -22,13 +22,15 @@ 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 | 27 | | `ServerService` | `GetVersion` (server version and feature list), `GetSigningKey` (commit signing key) | | |
| 26 | 28 | ||
| 27 | 29 | Tokens carry scopes only; presets such as `cli` or `agent` are a client concern. | |
| 28 | 30 | ||
| 29 | 31 | ## Layout | |
| 30 | 32 | ||
| 31 | - | - `proto/ermite/`: `.proto` files of the `ermite` package, one per service (`repo`, `git`, `merge_request`, `token`, `session`, `event`, `server`) plus shared `types`. Checked by `buf lint` and `buf breaking`. | |
| 33 | + | - `proto/ermite/`: `.proto` files of the `ermite` package, one per service (`repo`, `git`, `merge_request`, `token`, `session`, `event`, `server`, `runner`, `run`) plus shared `types`. Checked by `buf lint` and `buf breaking`. | |
| 32 | 34 | - `gen/ermite/`: generated Go messages. `gen/ermite/ermiteconnect/`: generated Connect clients and handlers. Committed, never edited by hand. | |
| 33 | 35 | ||
| 34 | 36 | ## Generate | |
| @@ -2,7 +2,7 @@ | |||||
| 2 | 2 | ||||
| 3 | The public API contract of [ermite](https://git.bnema.dev/bnema/ermite), a single-user Git forge: protobuf definitions for its ConnectRPC services and the Go code generated from them. | 3 | The public API contract of [ermite](https://git.bnema.dev/bnema/ermite), a single-user Git forge: protobuf definitions for its ConnectRPC services and the Go code generated from them. | ||
| 4 | 4 | ||||
| 5 | - | Status: early development. The contract defines seven services (below); the server that implements them is being built. | 5 | + | Status: early development. The contract defines nine services (below); the server that implements them is being built. |
| 6 | 6 | ||||
| 7 | ```go | 7 | ```go | ||
| 8 | import "bnema.dev/ermite-api/gen/ermite" | 8 | import "bnema.dev/ermite-api/gen/ermite" | ||
| @@ -22,13 +22,15 @@ 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 | | |||
| 26 | + | | `RunService` | `ListRuns`, `GetRun`, `GetJob`, `GetJobLog` (streamed, follow), `CancelRun`, `RerunRun`, `ListChecks`, `ListRunners`, `RemoveRunner` | | |||
| 25 | | `ServerService` | `GetVersion` (server version and feature list), `GetSigningKey` (commit signing key) | | 27 | | `ServerService` | `GetVersion` (server version and feature list), `GetSigningKey` (commit signing key) | | ||
| 26 | 28 | ||||
| 27 | 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. | ||
| 28 | 30 | ||||
| 29 | ## Layout | 31 | ## Layout | ||
| 30 | 32 | ||||
| 31 | - | - `proto/ermite/`: `.proto` files of the `ermite` package, one per service (`repo`, `git`, `merge_request`, `token`, `session`, `event`, `server`) plus shared `types`. Checked by `buf lint` and `buf breaking`. | 33 | + | - `proto/ermite/`: `.proto` files of the `ermite` package, one per service (`repo`, `git`, `merge_request`, `token`, `session`, `event`, `server`, `runner`, `run`) plus shared `types`. Checked by `buf lint` and `buf breaking`. |
| 32 | - `gen/ermite/`: generated Go messages. `gen/ermite/ermiteconnect/`: generated Connect clients and handlers. Committed, never edited by hand. | 34 | - `gen/ermite/`: generated Go messages. `gen/ermite/ermiteconnect/`: generated Connect clients and handlers. Committed, never edited by hand. | ||
| 33 | 35 | ||||
| 34 | ## Generate | 36 | ## Generate | ||
M gen/ermite/ermiteconnect/event.connect.go +22 -2
| @@ -49,7 +49,17 @@ type EventServiceClient interface { | |||
| 49 | 49 | // for merge requests, merge_request_opened, merge_request_merged, | |
| 50 | 50 | // merge_request_closed and comment_added. Each one is delivered on the | |
| 51 | 51 | // "repo:<name>" topic of its repository and on "home", subject to the | |
| 52 | - | // visibility of the repository. There is no event for a change of title or | |
| 52 | + | // visibility of the repository. | |
| 53 | + | // | |
| 54 | + | // Servers with the feature "ci.runs" also publish run_updated and | |
| 55 | + | // job_updated, only on the "repo:<name>" topic of their repository (never on | |
| 56 | + | // "home"), subject to the same visibility. They carry the state after the | |
| 57 | + | // change and are published when a run or a job is created, changes status | |
| 58 | + | // or conclusion, or when one of its steps starts or ends (never per log | |
| 59 | + | // line: follow a log with RunService.GetJobLog). The order of these events | |
| 60 | + | // is not guaranteed, between jobs and runs or between two updates of the same | |
| 61 | + | // one: a client keeps the copy with the higher Run.version or | |
| 62 | + | // RunJob.version and drops the other. There is no event for a change of title or | |
| 53 | 63 | // description, nor for a resolved comment: after a gap, or to see such | |
| 54 | 64 | // changes, clients read the merge request again. A merge publishes | |
| 55 | 65 | // merge_request_merged, then pushed for the target branch. | |
| @@ -111,7 +121,17 @@ type EventServiceHandler interface { | |||
| 111 | 121 | // for merge requests, merge_request_opened, merge_request_merged, | |
| 112 | 122 | // merge_request_closed and comment_added. Each one is delivered on the | |
| 113 | 123 | // "repo:<name>" topic of its repository and on "home", subject to the | |
| 114 | - | // visibility of the repository. There is no event for a change of title or | |
| 124 | + | // visibility of the repository. | |
| 125 | + | // | |
| 126 | + | // Servers with the feature "ci.runs" also publish run_updated and | |
| 127 | + | // job_updated, only on the "repo:<name>" topic of their repository (never on | |
| 128 | + | // "home"), subject to the same visibility. They carry the state after the | |
| 129 | + | // change and are published when a run or a job is created, changes status | |
| 130 | + | // or conclusion, or when one of its steps starts or ends (never per log | |
| 131 | + | // line: follow a log with RunService.GetJobLog). The order of these events | |
| 132 | + | // is not guaranteed, between jobs and runs or between two updates of the same | |
| 133 | + | // one: a client keeps the copy with the higher Run.version or | |
| 134 | + | // RunJob.version and drops the other. There is no event for a change of title or | |
| 115 | 135 | // description, nor for a resolved comment: after a gap, or to see such | |
| 116 | 136 | // changes, clients read the merge request again. A merge publishes | |
| 117 | 137 | // merge_request_merged, then pushed for the target branch. | |
| @@ -49,7 +49,17 @@ type EventServiceClient interface { | |||||
| 49 | // for merge requests, merge_request_opened, merge_request_merged, | 49 | // for merge requests, merge_request_opened, merge_request_merged, | ||
| 50 | // merge_request_closed and comment_added. Each one is delivered on the | 50 | // merge_request_closed and comment_added. Each one is delivered on the | ||
| 51 | // "repo:<name>" topic of its repository and on "home", subject to the | 51 | // "repo:<name>" topic of its repository and on "home", subject to the | ||
| 52 | - | // visibility of the repository. There is no event for a change of title or | 52 | + | // visibility of the repository. |
| 53 | + | // | |||
| 54 | + | // Servers with the feature "ci.runs" also publish run_updated and | |||
| 55 | + | // job_updated, only on the "repo:<name>" topic of their repository (never on | |||
| 56 | + | // "home"), subject to the same visibility. They carry the state after the | |||
| 57 | + | // change and are published when a run or a job is created, changes status | |||
| 58 | + | // or conclusion, or when one of its steps starts or ends (never per log | |||
| 59 | + | // line: follow a log with RunService.GetJobLog). The order of these events | |||
| 60 | + | // is not guaranteed, between jobs and runs or between two updates of the same | |||
| 61 | + | // one: a client keeps the copy with the higher Run.version or | |||
| 62 | + | // RunJob.version and drops the other. There is no event for a change of title or | |||
| 53 | // description, nor for a resolved comment: after a gap, or to see such | 63 | // description, nor for a resolved comment: after a gap, or to see such | ||
| 54 | // changes, clients read the merge request again. A merge publishes | 64 | // changes, clients read the merge request again. A merge publishes | ||
| 55 | // merge_request_merged, then pushed for the target branch. | 65 | // merge_request_merged, then pushed for the target branch. | ||
| @@ -111,7 +121,17 @@ type EventServiceHandler interface { | |||||
| 111 | // for merge requests, merge_request_opened, merge_request_merged, | 121 | // for merge requests, merge_request_opened, merge_request_merged, | ||
| 112 | // merge_request_closed and comment_added. Each one is delivered on the | 122 | // merge_request_closed and comment_added. Each one is delivered on the | ||
| 113 | // "repo:<name>" topic of its repository and on "home", subject to the | 123 | // "repo:<name>" topic of its repository and on "home", subject to the | ||
| 114 | - | // visibility of the repository. There is no event for a change of title or | 124 | + | // visibility of the repository. |
| 125 | + | // | |||
| 126 | + | // Servers with the feature "ci.runs" also publish run_updated and | |||
| 127 | + | // job_updated, only on the "repo:<name>" topic of their repository (never on | |||
| 128 | + | // "home"), subject to the same visibility. They carry the state after the | |||
| 129 | + | // change and are published when a run or a job is created, changes status | |||
| 130 | + | // or conclusion, or when one of its steps starts or ends (never per log | |||
| 131 | + | // line: follow a log with RunService.GetJobLog). The order of these events | |||
| 132 | + | // is not guaranteed, between jobs and runs or between two updates of the same | |||
| 133 | + | // one: a client keeps the copy with the higher Run.version or | |||
| 134 | + | // RunJob.version and drops the other. There is no event for a change of title or | |||
| 115 | // description, nor for a resolved comment: after a gap, or to see such | 135 | // description, nor for a resolved comment: after a gap, or to see such | ||
| 116 | // changes, clients read the merge request again. A merge publishes | 136 | // changes, clients read the merge request again. A merge publishes | ||
| 117 | // merge_request_merged, then pushed for the target branch. | 137 | // merge_request_merged, then pushed for the target branch. | ||
A gen/ermite/ermiteconnect/run.connect.go +436 -0
| @@ -0,0 +1,436 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | // Code generated by protoc-gen-connect-go. DO NOT EDIT. | |
| 4 | + | // | |
| 5 | + | // Source: ermite/run.proto | |
| 6 | + | ||
| 7 | + | package ermiteconnect | |
| 8 | + | ||
| 9 | + | import ( | |
| 10 | + | ermite "bnema.dev/ermite-api/gen/ermite" | |
| 11 | + | connect "connectrpc.com/connect" | |
| 12 | + | context "context" | |
| 13 | + | errors "errors" | |
| 14 | + | http "net/http" | |
| 15 | + | strings "strings" | |
| 16 | + | ) | |
| 17 | + | ||
| 18 | + | // This is a compile-time assertion to ensure that this generated file and the connect package are | |
| 19 | + | // compatible. If you get a compiler error that this constant is not defined, this code was | |
| 20 | + | // generated with a version of connect newer than the one compiled into your binary. You can fix the | |
| 21 | + | // problem by either regenerating this code with an older version of connect or updating the connect | |
| 22 | + | // version compiled into your binary. | |
| 23 | + | const _ = connect.IsAtLeastVersion1_13_0 | |
| 24 | + | ||
| 25 | + | const ( | |
| 26 | + | // RunServiceName is the fully-qualified name of the RunService service. | |
| 27 | + | RunServiceName = "ermite.RunService" | |
| 28 | + | ) | |
| 29 | + | ||
| 30 | + | // These constants are the fully-qualified names of the RPCs defined in this package. They're | |
| 31 | + | // exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. | |
| 32 | + | // | |
| 33 | + | // Note that these are different from the fully-qualified method names used by | |
| 34 | + | // google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to | |
| 35 | + | // reflection-formatted method names, remove the leading slash and convert the remaining slash to a | |
| 36 | + | // period. | |
| 37 | + | const ( | |
| 38 | + | // RunServiceListRunsProcedure is the fully-qualified name of the RunService's ListRuns RPC. | |
| 39 | + | RunServiceListRunsProcedure = "/ermite.RunService/ListRuns" | |
| 40 | + | // RunServiceGetRunProcedure is the fully-qualified name of the RunService's GetRun RPC. | |
| 41 | + | RunServiceGetRunProcedure = "/ermite.RunService/GetRun" | |
| 42 | + | // RunServiceGetJobProcedure is the fully-qualified name of the RunService's GetJob RPC. | |
| 43 | + | RunServiceGetJobProcedure = "/ermite.RunService/GetJob" | |
| 44 | + | // RunServiceGetJobLogProcedure is the fully-qualified name of the RunService's GetJobLog RPC. | |
| 45 | + | RunServiceGetJobLogProcedure = "/ermite.RunService/GetJobLog" | |
| 46 | + | // RunServiceCancelRunProcedure is the fully-qualified name of the RunService's CancelRun RPC. | |
| 47 | + | RunServiceCancelRunProcedure = "/ermite.RunService/CancelRun" | |
| 48 | + | // RunServiceRerunRunProcedure is the fully-qualified name of the RunService's RerunRun RPC. | |
| 49 | + | RunServiceRerunRunProcedure = "/ermite.RunService/RerunRun" | |
| 50 | + | // RunServiceListChecksProcedure is the fully-qualified name of the RunService's ListChecks RPC. | |
| 51 | + | RunServiceListChecksProcedure = "/ermite.RunService/ListChecks" | |
| 52 | + | // RunServiceListRunnersProcedure is the fully-qualified name of the RunService's ListRunners RPC. | |
| 53 | + | RunServiceListRunnersProcedure = "/ermite.RunService/ListRunners" | |
| 54 | + | // RunServiceRemoveRunnerProcedure is the fully-qualified name of the RunService's RemoveRunner RPC. | |
| 55 | + | RunServiceRemoveRunnerProcedure = "/ermite.RunService/RemoveRunner" | |
| 56 | + | ) | |
| 57 | + | ||
| 58 | + | // RunServiceClient is a client for the ermite.RunService service. | |
| 59 | + | type RunServiceClient interface { | |
| 60 | + | // ListRuns lists the runs of a repository, highest id (newest) first. Each | |
| 61 | + | // run is described by its latest attempt. The filters narrow the result; a | |
| 62 | + | // server without the feature "ci.runs.filters" ignores them and returns | |
| 63 | + | // every run, so a client that needs them checks the feature. Fails with | |
| 64 | + | // invalid_argument if a filter is malformed. | |
| 65 | + | ListRuns(context.Context, *connect.Request[ermite.ListRunsRequest]) (*connect.Response[ermite.ListRunsResponse], error) | |
| 66 | + | // GetRun returns a run with its jobs and their steps, for the latest attempt | |
| 67 | + | // or for the one asked. Fails with not_found if the run or the attempt does | |
| 68 | + | // not exist. | |
| 69 | + | GetRun(context.Context, *connect.Request[ermite.GetRunRequest]) (*connect.Response[ermite.GetRunResponse], error) | |
| 70 | + | // GetJob returns one job with its steps. Fails with not_found if the job | |
| 71 | + | // or the attempt does not exist. | |
| 72 | + | GetJob(context.Context, *connect.Request[ermite.GetJobRequest]) (*connect.Response[ermite.GetJobResponse], error) | |
| 73 | + | // 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 | |
| 75 | + | // text each. | |
| 76 | + | // | |
| 77 | + | // Without follow, the stream ends after the last line stored when the call | |
| 78 | + | // was made. With follow, it goes on as lines arrive, with at most 1 second of | |
| 79 | + | // delay after the runner's AppendLog, and ends without error once the job has | |
| 80 | + | // completed and all its lines were sent; on a completed job follow changes | |
| 81 | + | // nothing. A client that loses the stream calls again with the offset after | |
| 82 | + | // the last line it received. The client may cancel at any time. | |
| 83 | + | // | |
| 84 | + | // Fails with not_found if the job, the attempt or its log does not exist (the | |
| 85 | + | // log is removed by the retention sweep, and a queued job has none: with | |
| 86 | + | // follow, the call waits for the first line instead), with out_of_range if | |
| 87 | + | // offset is beyond the end of the log of a job without follow, and with | |
| 88 | + | // resource_exhausted when there are too many concurrent log streams, in | |
| 89 | + | // total or from one client. Like Watch, a stream ends with unauthenticated | |
| 90 | + | // when the token was revoked or expired. | |
| 91 | + | GetJobLog(context.Context, *connect.Request[ermite.GetJobLogRequest]) (*connect.ServerStreamForClient[ermite.GetJobLogResponse], error) | |
| 92 | + | // CancelRun asks to cancel the latest attempt of a run. Needs the ci:write | |
| 93 | + | // scope. Jobs that have not started are cancelled at once; jobs in progress | |
| 94 | + | // get a CANCEL_REASON_USER request through WatchJob, and the run completes | |
| 95 | + | // when they have ended, so the returned run may still be IN_PROGRESS. | |
| 96 | + | // Cancelling a run that already has a cancellation requested succeeds and | |
| 97 | + | // changes nothing. Fails with failed_precondition if the run has completed. | |
| 98 | + | CancelRun(context.Context, *connect.Request[ermite.CancelRunRequest]) (*connect.Response[ermite.CancelRunResponse], error) | |
| 99 | + | // RerunRun starts a new attempt of a completed run: same run id and number, | |
| 100 | + | // attempt + 1, same commit and same workflow file as at the original run (the | |
| 101 | + | // file of the repository is not read again). Needs the ci:write scope. Fails | |
| 102 | + | // with failed_precondition if the latest attempt has not completed, or if | |
| 103 | + | // failed_only is set and no job failed. The new attempt is returned QUEUED. | |
| 104 | + | RerunRun(context.Context, *connect.Request[ermite.RerunRunRequest]) (*connect.Response[ermite.RerunRunResponse], error) | |
| 105 | + | // ListChecks lists the checks of a commit: for each job of each run on that | |
| 106 | + | // sha, the one of the latest attempt, ordered by creation time then name. | |
| 107 | + | // A commit without a run has no check: the list is empty, which is not an | |
| 108 | + | // error, and the server does not verify that the commit exists. Returns at | |
| 109 | + | // most 1,000 checks (truncated tells). | |
| 110 | + | ListChecks(context.Context, *connect.Request[ermite.ListChecksRequest]) (*connect.Response[ermite.ListChecksResponse], error) | |
| 111 | + | // ListRunners lists the registered runners, ordered by name. Needs the | |
| 112 | + | // runner:register scope. | |
| 113 | + | ListRunners(context.Context, *connect.Request[ermite.ListRunnersRequest]) (*connect.Response[ermite.ListRunnersResponse], error) | |
| 114 | + | // RemoveRunner removes a runner: its runner token stops working at once (a | |
| 115 | + | // FetchJob in progress ends with unauthenticated) and it no longer gets | |
| 116 | + | // jobs. A job it is running is not affected: it ends normally, or fails as | |
| 117 | + | // a lost runner without heartbeats. Needs the runner:register scope. Fails | |
| 118 | + | // with not_found if the id is unknown. | |
| 119 | + | RemoveRunner(context.Context, *connect.Request[ermite.RemoveRunnerRequest]) (*connect.Response[ermite.RemoveRunnerResponse], error) | |
| 120 | + | } | |
| 121 | + | ||
| 122 | + | // NewRunServiceClient constructs a client for the ermite.RunService service. By default, it uses | |
| 123 | + | // the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and sends | |
| 124 | + | // uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or | |
| 125 | + | // connect.WithGRPCWeb() options. | |
| 126 | + | // | |
| 127 | + | // The URL supplied here should be the base URL for the Connect or gRPC server (for example, | |
| 128 | + | // http://api.acme.com or https://acme.com/grpc). | |
| 129 | + | func NewRunServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) RunServiceClient { | |
| 130 | + | baseURL = strings.TrimRight(baseURL, "/") | |
| 131 | + | runServiceMethods := ermite.File_ermite_run_proto.Services().ByName("RunService").Methods() | |
| 132 | + | return &runServiceClient{ | |
| 133 | + | listRuns: connect.NewClient[ermite.ListRunsRequest, ermite.ListRunsResponse]( | |
| 134 | + | httpClient, | |
| 135 | + | baseURL+RunServiceListRunsProcedure, | |
| 136 | + | connect.WithSchema(runServiceMethods.ByName("ListRuns")), | |
| 137 | + | connect.WithClientOptions(opts...), | |
| 138 | + | ), | |
| 139 | + | getRun: connect.NewClient[ermite.GetRunRequest, ermite.GetRunResponse]( | |
| 140 | + | httpClient, | |
| 141 | + | baseURL+RunServiceGetRunProcedure, | |
| 142 | + | connect.WithSchema(runServiceMethods.ByName("GetRun")), | |
| 143 | + | connect.WithClientOptions(opts...), | |
| 144 | + | ), | |
| 145 | + | getJob: connect.NewClient[ermite.GetJobRequest, ermite.GetJobResponse]( | |
| 146 | + | httpClient, | |
| 147 | + | baseURL+RunServiceGetJobProcedure, | |
| 148 | + | connect.WithSchema(runServiceMethods.ByName("GetJob")), | |
| 149 | + | connect.WithClientOptions(opts...), | |
| 150 | + | ), | |
| 151 | + | getJobLog: connect.NewClient[ermite.GetJobLogRequest, ermite.GetJobLogResponse]( | |
| 152 | + | httpClient, | |
| 153 | + | baseURL+RunServiceGetJobLogProcedure, | |
| 154 | + | connect.WithSchema(runServiceMethods.ByName("GetJobLog")), | |
| 155 | + | connect.WithClientOptions(opts...), | |
| 156 | + | ), | |
| 157 | + | cancelRun: connect.NewClient[ermite.CancelRunRequest, ermite.CancelRunResponse]( | |
| 158 | + | httpClient, | |
| 159 | + | baseURL+RunServiceCancelRunProcedure, | |
| 160 | + | connect.WithSchema(runServiceMethods.ByName("CancelRun")), | |
| 161 | + | connect.WithClientOptions(opts...), | |
| 162 | + | ), | |
| 163 | + | rerunRun: connect.NewClient[ermite.RerunRunRequest, ermite.RerunRunResponse]( | |
| 164 | + | httpClient, | |
| 165 | + | baseURL+RunServiceRerunRunProcedure, | |
| 166 | + | connect.WithSchema(runServiceMethods.ByName("RerunRun")), | |
| 167 | + | connect.WithClientOptions(opts...), | |
| 168 | + | ), | |
| 169 | + | listChecks: connect.NewClient[ermite.ListChecksRequest, ermite.ListChecksResponse]( | |
| 170 | + | httpClient, | |
| 171 | + | baseURL+RunServiceListChecksProcedure, | |
| 172 | + | connect.WithSchema(runServiceMethods.ByName("ListChecks")), | |
| 173 | + | connect.WithClientOptions(opts...), | |
| 174 | + | ), | |
| 175 | + | listRunners: connect.NewClient[ermite.ListRunnersRequest, ermite.ListRunnersResponse]( | |
| 176 | + | httpClient, | |
| 177 | + | baseURL+RunServiceListRunnersProcedure, | |
| 178 | + | connect.WithSchema(runServiceMethods.ByName("ListRunners")), | |
| 179 | + | connect.WithClientOptions(opts...), | |
| 180 | + | ), | |
| 181 | + | removeRunner: connect.NewClient[ermite.RemoveRunnerRequest, ermite.RemoveRunnerResponse]( | |
| 182 | + | httpClient, | |
| 183 | + | baseURL+RunServiceRemoveRunnerProcedure, | |
| 184 | + | connect.WithSchema(runServiceMethods.ByName("RemoveRunner")), | |
| 185 | + | connect.WithClientOptions(opts...), | |
| 186 | + | ), | |
| 187 | + | } | |
| 188 | + | } | |
| 189 | + | ||
| 190 | + | // runServiceClient implements RunServiceClient. | |
| 191 | + | 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] | |
| 201 | + | } | |
| 202 | + | ||
| 203 | + | // ListRuns calls ermite.RunService.ListRuns. | |
| 204 | + | func (c *runServiceClient) ListRuns(ctx context.Context, req *connect.Request[ermite.ListRunsRequest]) (*connect.Response[ermite.ListRunsResponse], error) { | |
| 205 | + | return c.listRuns.CallUnary(ctx, req) | |
| 206 | + | } | |
| 207 | + | ||
| 208 | + | // GetRun calls ermite.RunService.GetRun. | |
| 209 | + | func (c *runServiceClient) GetRun(ctx context.Context, req *connect.Request[ermite.GetRunRequest]) (*connect.Response[ermite.GetRunResponse], error) { | |
| 210 | + | return c.getRun.CallUnary(ctx, req) | |
| 211 | + | } | |
| 212 | + | ||
| 213 | + | // GetJob calls ermite.RunService.GetJob. | |
| 214 | + | func (c *runServiceClient) GetJob(ctx context.Context, req *connect.Request[ermite.GetJobRequest]) (*connect.Response[ermite.GetJobResponse], error) { | |
| 215 | + | return c.getJob.CallUnary(ctx, req) | |
| 216 | + | } | |
| 217 | + | ||
| 218 | + | // GetJobLog calls ermite.RunService.GetJobLog. | |
| 219 | + | func (c *runServiceClient) GetJobLog(ctx context.Context, req *connect.Request[ermite.GetJobLogRequest]) (*connect.ServerStreamForClient[ermite.GetJobLogResponse], error) { | |
| 220 | + | return c.getJobLog.CallServerStream(ctx, req) | |
| 221 | + | } | |
| 222 | + | ||
| 223 | + | // CancelRun calls ermite.RunService.CancelRun. | |
| 224 | + | func (c *runServiceClient) CancelRun(ctx context.Context, req *connect.Request[ermite.CancelRunRequest]) (*connect.Response[ermite.CancelRunResponse], error) { | |
| 225 | + | return c.cancelRun.CallUnary(ctx, req) | |
| 226 | + | } | |
| 227 | + | ||
| 228 | + | // RerunRun calls ermite.RunService.RerunRun. | |
| 229 | + | func (c *runServiceClient) RerunRun(ctx context.Context, req *connect.Request[ermite.RerunRunRequest]) (*connect.Response[ermite.RerunRunResponse], error) { | |
| 230 | + | return c.rerunRun.CallUnary(ctx, req) | |
| 231 | + | } | |
| 232 | + | ||
| 233 | + | // ListChecks calls ermite.RunService.ListChecks. | |
| 234 | + | func (c *runServiceClient) ListChecks(ctx context.Context, req *connect.Request[ermite.ListChecksRequest]) (*connect.Response[ermite.ListChecksResponse], error) { | |
| 235 | + | return c.listChecks.CallUnary(ctx, req) | |
| 236 | + | } | |
| 237 | + | ||
| 238 | + | // ListRunners calls ermite.RunService.ListRunners. | |
| 239 | + | func (c *runServiceClient) ListRunners(ctx context.Context, req *connect.Request[ermite.ListRunnersRequest]) (*connect.Response[ermite.ListRunnersResponse], error) { | |
| 240 | + | return c.listRunners.CallUnary(ctx, req) | |
| 241 | + | } | |
| 242 | + | ||
| 243 | + | // RemoveRunner calls ermite.RunService.RemoveRunner. | |
| 244 | + | func (c *runServiceClient) RemoveRunner(ctx context.Context, req *connect.Request[ermite.RemoveRunnerRequest]) (*connect.Response[ermite.RemoveRunnerResponse], error) { | |
| 245 | + | return c.removeRunner.CallUnary(ctx, req) | |
| 246 | + | } | |
| 247 | + | ||
| 248 | + | // RunServiceHandler is an implementation of the ermite.RunService service. | |
| 249 | + | type RunServiceHandler interface { | |
| 250 | + | // ListRuns lists the runs of a repository, highest id (newest) first. Each | |
| 251 | + | // run is described by its latest attempt. The filters narrow the result; a | |
| 252 | + | // server without the feature "ci.runs.filters" ignores them and returns | |
| 253 | + | // every run, so a client that needs them checks the feature. Fails with | |
| 254 | + | // invalid_argument if a filter is malformed. | |
| 255 | + | ListRuns(context.Context, *connect.Request[ermite.ListRunsRequest]) (*connect.Response[ermite.ListRunsResponse], error) | |
| 256 | + | // GetRun returns a run with its jobs and their steps, for the latest attempt | |
| 257 | + | // or for the one asked. Fails with not_found if the run or the attempt does | |
| 258 | + | // not exist. | |
| 259 | + | GetRun(context.Context, *connect.Request[ermite.GetRunRequest]) (*connect.Response[ermite.GetRunResponse], error) | |
| 260 | + | // GetJob returns one job with its steps. Fails with not_found if the job | |
| 261 | + | // or the attempt does not exist. | |
| 262 | + | GetJob(context.Context, *connect.Request[ermite.GetJobRequest]) (*connect.Response[ermite.GetJobResponse], error) | |
| 263 | + | // 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 | |
| 265 | + | // text each. | |
| 266 | + | // | |
| 267 | + | // Without follow, the stream ends after the last line stored when the call | |
| 268 | + | // was made. With follow, it goes on as lines arrive, with at most 1 second of | |
| 269 | + | // delay after the runner's AppendLog, and ends without error once the job has | |
| 270 | + | // completed and all its lines were sent; on a completed job follow changes | |
| 271 | + | // nothing. A client that loses the stream calls again with the offset after | |
| 272 | + | // the last line it received. The client may cancel at any time. | |
| 273 | + | // | |
| 274 | + | // Fails with not_found if the job, the attempt or its log does not exist (the | |
| 275 | + | // log is removed by the retention sweep, and a queued job has none: with | |
| 276 | + | // follow, the call waits for the first line instead), with out_of_range if | |
| 277 | + | // offset is beyond the end of the log of a job without follow, and with | |
| 278 | + | // resource_exhausted when there are too many concurrent log streams, in | |
| 279 | + | // total or from one client. Like Watch, a stream ends with unauthenticated | |
| 280 | + | // when the token was revoked or expired. | |
| 281 | + | GetJobLog(context.Context, *connect.Request[ermite.GetJobLogRequest], *connect.ServerStream[ermite.GetJobLogResponse]) error | |
| 282 | + | // CancelRun asks to cancel the latest attempt of a run. Needs the ci:write | |
| 283 | + | // scope. Jobs that have not started are cancelled at once; jobs in progress | |
| 284 | + | // get a CANCEL_REASON_USER request through WatchJob, and the run completes | |
| 285 | + | // when they have ended, so the returned run may still be IN_PROGRESS. | |
| 286 | + | // Cancelling a run that already has a cancellation requested succeeds and | |
| 287 | + | // changes nothing. Fails with failed_precondition if the run has completed. | |
| 288 | + | CancelRun(context.Context, *connect.Request[ermite.CancelRunRequest]) (*connect.Response[ermite.CancelRunResponse], error) | |
| 289 | + | // RerunRun starts a new attempt of a completed run: same run id and number, | |
| 290 | + | // attempt + 1, same commit and same workflow file as at the original run (the | |
| 291 | + | // file of the repository is not read again). Needs the ci:write scope. Fails | |
| 292 | + | // with failed_precondition if the latest attempt has not completed, or if | |
| 293 | + | // failed_only is set and no job failed. The new attempt is returned QUEUED. | |
| 294 | + | RerunRun(context.Context, *connect.Request[ermite.RerunRunRequest]) (*connect.Response[ermite.RerunRunResponse], error) | |
| 295 | + | // ListChecks lists the checks of a commit: for each job of each run on that | |
| 296 | + | // sha, the one of the latest attempt, ordered by creation time then name. | |
| 297 | + | // A commit without a run has no check: the list is empty, which is not an | |
| 298 | + | // error, and the server does not verify that the commit exists. Returns at | |
| 299 | + | // most 1,000 checks (truncated tells). | |
| 300 | + | ListChecks(context.Context, *connect.Request[ermite.ListChecksRequest]) (*connect.Response[ermite.ListChecksResponse], error) | |
| 301 | + | // ListRunners lists the registered runners, ordered by name. Needs the | |
| 302 | + | // runner:register scope. | |
| 303 | + | ListRunners(context.Context, *connect.Request[ermite.ListRunnersRequest]) (*connect.Response[ermite.ListRunnersResponse], error) | |
| 304 | + | // RemoveRunner removes a runner: its runner token stops working at once (a | |
| 305 | + | // FetchJob in progress ends with unauthenticated) and it no longer gets | |
| 306 | + | // jobs. A job it is running is not affected: it ends normally, or fails as | |
| 307 | + | // a lost runner without heartbeats. Needs the runner:register scope. Fails | |
| 308 | + | // with not_found if the id is unknown. | |
| 309 | + | RemoveRunner(context.Context, *connect.Request[ermite.RemoveRunnerRequest]) (*connect.Response[ermite.RemoveRunnerResponse], error) | |
| 310 | + | } | |
| 311 | + | ||
| 312 | + | // NewRunServiceHandler builds an HTTP handler from the service implementation. It returns the path | |
| 313 | + | // on which to mount the handler and the handler itself. | |
| 314 | + | // | |
| 315 | + | // By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf | |
| 316 | + | // and JSON codecs. They also support gzip compression. | |
| 317 | + | func NewRunServiceHandler(svc RunServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { | |
| 318 | + | runServiceMethods := ermite.File_ermite_run_proto.Services().ByName("RunService").Methods() | |
| 319 | + | runServiceListRunsHandler := connect.NewUnaryHandler( | |
| 320 | + | RunServiceListRunsProcedure, | |
| 321 | + | svc.ListRuns, | |
| 322 | + | connect.WithSchema(runServiceMethods.ByName("ListRuns")), | |
| 323 | + | connect.WithHandlerOptions(opts...), | |
| 324 | + | ) | |
| 325 | + | runServiceGetRunHandler := connect.NewUnaryHandler( | |
| 326 | + | RunServiceGetRunProcedure, | |
| 327 | + | svc.GetRun, | |
| 328 | + | connect.WithSchema(runServiceMethods.ByName("GetRun")), | |
| 329 | + | connect.WithHandlerOptions(opts...), | |
| 330 | + | ) | |
| 331 | + | runServiceGetJobHandler := connect.NewUnaryHandler( | |
| 332 | + | RunServiceGetJobProcedure, | |
| 333 | + | svc.GetJob, | |
| 334 | + | connect.WithSchema(runServiceMethods.ByName("GetJob")), | |
| 335 | + | connect.WithHandlerOptions(opts...), | |
| 336 | + | ) | |
| 337 | + | runServiceGetJobLogHandler := connect.NewServerStreamHandler( | |
| 338 | + | RunServiceGetJobLogProcedure, | |
| 339 | + | svc.GetJobLog, | |
| 340 | + | connect.WithSchema(runServiceMethods.ByName("GetJobLog")), | |
| 341 | + | connect.WithHandlerOptions(opts...), | |
| 342 | + | ) | |
| 343 | + | runServiceCancelRunHandler := connect.NewUnaryHandler( | |
| 344 | + | RunServiceCancelRunProcedure, | |
| 345 | + | svc.CancelRun, | |
| 346 | + | connect.WithSchema(runServiceMethods.ByName("CancelRun")), | |
| 347 | + | connect.WithHandlerOptions(opts...), | |
| 348 | + | ) | |
| 349 | + | runServiceRerunRunHandler := connect.NewUnaryHandler( | |
| 350 | + | RunServiceRerunRunProcedure, | |
| 351 | + | svc.RerunRun, | |
| 352 | + | connect.WithSchema(runServiceMethods.ByName("RerunRun")), | |
| 353 | + | connect.WithHandlerOptions(opts...), | |
| 354 | + | ) | |
| 355 | + | runServiceListChecksHandler := connect.NewUnaryHandler( | |
| 356 | + | RunServiceListChecksProcedure, | |
| 357 | + | svc.ListChecks, | |
| 358 | + | connect.WithSchema(runServiceMethods.ByName("ListChecks")), | |
| 359 | + | connect.WithHandlerOptions(opts...), | |
| 360 | + | ) | |
| 361 | + | runServiceListRunnersHandler := connect.NewUnaryHandler( | |
| 362 | + | RunServiceListRunnersProcedure, | |
| 363 | + | svc.ListRunners, | |
| 364 | + | connect.WithSchema(runServiceMethods.ByName("ListRunners")), | |
| 365 | + | connect.WithHandlerOptions(opts...), | |
| 366 | + | ) | |
| 367 | + | runServiceRemoveRunnerHandler := connect.NewUnaryHandler( | |
| 368 | + | RunServiceRemoveRunnerProcedure, | |
| 369 | + | svc.RemoveRunner, | |
| 370 | + | connect.WithSchema(runServiceMethods.ByName("RemoveRunner")), | |
| 371 | + | connect.WithHandlerOptions(opts...), | |
| 372 | + | ) | |
| 373 | + | return "/ermite.RunService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| 374 | + | switch r.URL.Path { | |
| 375 | + | case RunServiceListRunsProcedure: | |
| 376 | + | runServiceListRunsHandler.ServeHTTP(w, r) | |
| 377 | + | case RunServiceGetRunProcedure: | |
| 378 | + | runServiceGetRunHandler.ServeHTTP(w, r) | |
| 379 | + | case RunServiceGetJobProcedure: | |
| 380 | + | runServiceGetJobHandler.ServeHTTP(w, r) | |
| 381 | + | case RunServiceGetJobLogProcedure: | |
| 382 | + | runServiceGetJobLogHandler.ServeHTTP(w, r) | |
| 383 | + | case RunServiceCancelRunProcedure: | |
| 384 | + | runServiceCancelRunHandler.ServeHTTP(w, r) | |
| 385 | + | case RunServiceRerunRunProcedure: | |
| 386 | + | runServiceRerunRunHandler.ServeHTTP(w, r) | |
| 387 | + | case RunServiceListChecksProcedure: | |
| 388 | + | runServiceListChecksHandler.ServeHTTP(w, r) | |
| 389 | + | case RunServiceListRunnersProcedure: | |
| 390 | + | runServiceListRunnersHandler.ServeHTTP(w, r) | |
| 391 | + | case RunServiceRemoveRunnerProcedure: | |
| 392 | + | runServiceRemoveRunnerHandler.ServeHTTP(w, r) | |
| 393 | + | default: | |
| 394 | + | http.NotFound(w, r) | |
| 395 | + | } | |
| 396 | + | }) | |
| 397 | + | } | |
| 398 | + | ||
| 399 | + | // UnimplementedRunServiceHandler returns CodeUnimplemented from all methods. | |
| 400 | + | type UnimplementedRunServiceHandler struct{} | |
| 401 | + | ||
| 402 | + | func (UnimplementedRunServiceHandler) ListRuns(context.Context, *connect.Request[ermite.ListRunsRequest]) (*connect.Response[ermite.ListRunsResponse], error) { | |
| 403 | + | return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunService.ListRuns is not implemented")) | |
| 404 | + | } | |
| 405 | + | ||
| 406 | + | func (UnimplementedRunServiceHandler) GetRun(context.Context, *connect.Request[ermite.GetRunRequest]) (*connect.Response[ermite.GetRunResponse], error) { | |
| 407 | + | return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunService.GetRun is not implemented")) | |
| 408 | + | } | |
| 409 | + | ||
| 410 | + | func (UnimplementedRunServiceHandler) GetJob(context.Context, *connect.Request[ermite.GetJobRequest]) (*connect.Response[ermite.GetJobResponse], error) { | |
| 411 | + | return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunService.GetJob is not implemented")) | |
| 412 | + | } | |
| 413 | + | ||
| 414 | + | 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")) | |
| 416 | + | } | |
| 417 | + | ||
| 418 | + | func (UnimplementedRunServiceHandler) CancelRun(context.Context, *connect.Request[ermite.CancelRunRequest]) (*connect.Response[ermite.CancelRunResponse], error) { | |
| 419 | + | return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunService.CancelRun is not implemented")) | |
| 420 | + | } | |
| 421 | + | ||
| 422 | + | func (UnimplementedRunServiceHandler) RerunRun(context.Context, *connect.Request[ermite.RerunRunRequest]) (*connect.Response[ermite.RerunRunResponse], error) { | |
| 423 | + | return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunService.RerunRun is not implemented")) | |
| 424 | + | } | |
| 425 | + | ||
| 426 | + | func (UnimplementedRunServiceHandler) ListChecks(context.Context, *connect.Request[ermite.ListChecksRequest]) (*connect.Response[ermite.ListChecksResponse], error) { | |
| 427 | + | return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunService.ListChecks is not implemented")) | |
| 428 | + | } | |
| 429 | + | ||
| 430 | + | func (UnimplementedRunServiceHandler) ListRunners(context.Context, *connect.Request[ermite.ListRunnersRequest]) (*connect.Response[ermite.ListRunnersResponse], error) { | |
| 431 | + | return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunService.ListRunners is not implemented")) | |
| 432 | + | } | |
| 433 | + | ||
| 434 | + | func (UnimplementedRunServiceHandler) RemoveRunner(context.Context, *connect.Request[ermite.RemoveRunnerRequest]) (*connect.Response[ermite.RemoveRunnerResponse], error) { | |
| 435 | + | return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunService.RemoveRunner is not implemented")) | |
| 436 | + | } | |
| @@ -0,0 +1,436 @@ | |||||
| 1 | + | // SPDX-License-Identifier: MIT | |||
| 2 | + | ||||
| 3 | + | // Code generated by protoc-gen-connect-go. DO NOT EDIT. | |||
| 4 | + | // | |||
| 5 | + | // Source: ermite/run.proto | |||
| 6 | + | ||||
| 7 | + | package ermiteconnect | |||
| 8 | + | ||||
| 9 | + | import ( | |||
| 10 | + | ermite "bnema.dev/ermite-api/gen/ermite" | |||
| 11 | + | connect "connectrpc.com/connect" | |||
| 12 | + | context "context" | |||
| 13 | + | errors "errors" | |||
| 14 | + | http "net/http" | |||
| 15 | + | strings "strings" | |||
| 16 | + | ) | |||
| 17 | + | ||||
| 18 | + | // This is a compile-time assertion to ensure that this generated file and the connect package are | |||
| 19 | + | // compatible. If you get a compiler error that this constant is not defined, this code was | |||
| 20 | + | // generated with a version of connect newer than the one compiled into your binary. You can fix the | |||
| 21 | + | // problem by either regenerating this code with an older version of connect or updating the connect | |||
| 22 | + | // version compiled into your binary. | |||
| 23 | + | const _ = connect.IsAtLeastVersion1_13_0 | |||
| 24 | + | ||||
| 25 | + | const ( | |||
| 26 | + | // RunServiceName is the fully-qualified name of the RunService service. | |||
| 27 | + | RunServiceName = "ermite.RunService" | |||
| 28 | + | ) | |||
| 29 | + | ||||
| 30 | + | // These constants are the fully-qualified names of the RPCs defined in this package. They're | |||
| 31 | + | // exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. | |||
| 32 | + | // | |||
| 33 | + | // Note that these are different from the fully-qualified method names used by | |||
| 34 | + | // google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to | |||
| 35 | + | // reflection-formatted method names, remove the leading slash and convert the remaining slash to a | |||
| 36 | + | // period. | |||
| 37 | + | const ( | |||
| 38 | + | // RunServiceListRunsProcedure is the fully-qualified name of the RunService's ListRuns RPC. | |||
| 39 | + | RunServiceListRunsProcedure = "/ermite.RunService/ListRuns" | |||
| 40 | + | // RunServiceGetRunProcedure is the fully-qualified name of the RunService's GetRun RPC. | |||
| 41 | + | RunServiceGetRunProcedure = "/ermite.RunService/GetRun" | |||
| 42 | + | // RunServiceGetJobProcedure is the fully-qualified name of the RunService's GetJob RPC. | |||
| 43 | + | RunServiceGetJobProcedure = "/ermite.RunService/GetJob" | |||
| 44 | + | // RunServiceGetJobLogProcedure is the fully-qualified name of the RunService's GetJobLog RPC. | |||
| 45 | + | RunServiceGetJobLogProcedure = "/ermite.RunService/GetJobLog" | |||
| 46 | + | // RunServiceCancelRunProcedure is the fully-qualified name of the RunService's CancelRun RPC. | |||
| 47 | + | RunServiceCancelRunProcedure = "/ermite.RunService/CancelRun" | |||
| 48 | + | // RunServiceRerunRunProcedure is the fully-qualified name of the RunService's RerunRun RPC. | |||
| 49 | + | RunServiceRerunRunProcedure = "/ermite.RunService/RerunRun" | |||
| 50 | + | // RunServiceListChecksProcedure is the fully-qualified name of the RunService's ListChecks RPC. | |||
| 51 | + | RunServiceListChecksProcedure = "/ermite.RunService/ListChecks" | |||
| 52 | + | // RunServiceListRunnersProcedure is the fully-qualified name of the RunService's ListRunners RPC. | |||
| 53 | + | RunServiceListRunnersProcedure = "/ermite.RunService/ListRunners" | |||
| 54 | + | // RunServiceRemoveRunnerProcedure is the fully-qualified name of the RunService's RemoveRunner RPC. | |||
| 55 | + | RunServiceRemoveRunnerProcedure = "/ermite.RunService/RemoveRunner" | |||
| 56 | + | ) | |||
| 57 | + | ||||
| 58 | + | // RunServiceClient is a client for the ermite.RunService service. | |||
| 59 | + | type RunServiceClient interface { | |||
| 60 | + | // ListRuns lists the runs of a repository, highest id (newest) first. Each | |||
| 61 | + | // run is described by its latest attempt. The filters narrow the result; a | |||
| 62 | + | // server without the feature "ci.runs.filters" ignores them and returns | |||
| 63 | + | // every run, so a client that needs them checks the feature. Fails with | |||
| 64 | + | // invalid_argument if a filter is malformed. | |||
| 65 | + | ListRuns(context.Context, *connect.Request[ermite.ListRunsRequest]) (*connect.Response[ermite.ListRunsResponse], error) | |||
| 66 | + | // GetRun returns a run with its jobs and their steps, for the latest attempt | |||
| 67 | + | // or for the one asked. Fails with not_found if the run or the attempt does | |||
| 68 | + | // not exist. | |||
| 69 | + | GetRun(context.Context, *connect.Request[ermite.GetRunRequest]) (*connect.Response[ermite.GetRunResponse], error) | |||
| 70 | + | // GetJob returns one job with its steps. Fails with not_found if the job | |||
| 71 | + | // or the attempt does not exist. | |||
| 72 | + | GetJob(context.Context, *connect.Request[ermite.GetJobRequest]) (*connect.Response[ermite.GetJobResponse], error) | |||
| 73 | + | // 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 | |||
| 75 | + | // text each. | |||
| 76 | + | // | |||
| 77 | + | // Without follow, the stream ends after the last line stored when the call | |||
| 78 | + | // was made. With follow, it goes on as lines arrive, with at most 1 second of | |||
| 79 | + | // delay after the runner's AppendLog, and ends without error once the job has | |||
| 80 | + | // completed and all its lines were sent; on a completed job follow changes | |||
| 81 | + | // nothing. A client that loses the stream calls again with the offset after | |||
| 82 | + | // the last line it received. The client may cancel at any time. | |||
| 83 | + | // | |||
| 84 | + | // Fails with not_found if the job, the attempt or its log does not exist (the | |||
| 85 | + | // log is removed by the retention sweep, and a queued job has none: with | |||
| 86 | + | // follow, the call waits for the first line instead), with out_of_range if | |||
| 87 | + | // offset is beyond the end of the log of a job without follow, and with | |||
| 88 | + | // resource_exhausted when there are too many concurrent log streams, in | |||
| 89 | + | // total or from one client. Like Watch, a stream ends with unauthenticated | |||
| 90 | + | // when the token was revoked or expired. | |||
| 91 | + | GetJobLog(context.Context, *connect.Request[ermite.GetJobLogRequest]) (*connect.ServerStreamForClient[ermite.GetJobLogResponse], error) | |||
| 92 | + | // CancelRun asks to cancel the latest attempt of a run. Needs the ci:write | |||
| 93 | + | // scope. Jobs that have not started are cancelled at once; jobs in progress | |||
| 94 | + | // get a CANCEL_REASON_USER request through WatchJob, and the run completes | |||
| 95 | + | // when they have ended, so the returned run may still be IN_PROGRESS. | |||
| 96 | + | // Cancelling a run that already has a cancellation requested succeeds and | |||
| 97 | + | // changes nothing. Fails with failed_precondition if the run has completed. | |||
| 98 | + | CancelRun(context.Context, *connect.Request[ermite.CancelRunRequest]) (*connect.Response[ermite.CancelRunResponse], error) | |||
| 99 | + | // RerunRun starts a new attempt of a completed run: same run id and number, | |||
| 100 | + | // attempt + 1, same commit and same workflow file as at the original run (the | |||
| 101 | + | // file of the repository is not read again). Needs the ci:write scope. Fails | |||
| 102 | + | // with failed_precondition if the latest attempt has not completed, or if | |||
| 103 | + | // failed_only is set and no job failed. The new attempt is returned QUEUED. | |||
| 104 | + | RerunRun(context.Context, *connect.Request[ermite.RerunRunRequest]) (*connect.Response[ermite.RerunRunResponse], error) | |||
| 105 | + | // ListChecks lists the checks of a commit: for each job of each run on that | |||
| 106 | + | // sha, the one of the latest attempt, ordered by creation time then name. | |||
| 107 | + | // A commit without a run has no check: the list is empty, which is not an | |||
| 108 | + | // error, and the server does not verify that the commit exists. Returns at | |||
| 109 | + | // most 1,000 checks (truncated tells). | |||
| 110 | + | ListChecks(context.Context, *connect.Request[ermite.ListChecksRequest]) (*connect.Response[ermite.ListChecksResponse], error) | |||
| 111 | + | // ListRunners lists the registered runners, ordered by name. Needs the | |||
| 112 | + | // runner:register scope. | |||
| 113 | + | ListRunners(context.Context, *connect.Request[ermite.ListRunnersRequest]) (*connect.Response[ermite.ListRunnersResponse], error) | |||
| 114 | + | // RemoveRunner removes a runner: its runner token stops working at once (a | |||
| 115 | + | // FetchJob in progress ends with unauthenticated) and it no longer gets | |||
| 116 | + | // jobs. A job it is running is not affected: it ends normally, or fails as | |||
| 117 | + | // a lost runner without heartbeats. Needs the runner:register scope. Fails | |||
| 118 | + | // with not_found if the id is unknown. | |||
| 119 | + | RemoveRunner(context.Context, *connect.Request[ermite.RemoveRunnerRequest]) (*connect.Response[ermite.RemoveRunnerResponse], error) | |||
| 120 | + | } | |||
| 121 | + | ||||
| 122 | + | // NewRunServiceClient constructs a client for the ermite.RunService service. By default, it uses | |||
| 123 | + | // the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and sends | |||
| 124 | + | // uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or | |||
| 125 | + | // connect.WithGRPCWeb() options. | |||
| 126 | + | // | |||
| 127 | + | // The URL supplied here should be the base URL for the Connect or gRPC server (for example, | |||
| 128 | + | // http://api.acme.com or https://acme.com/grpc). | |||
| 129 | + | func NewRunServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) RunServiceClient { | |||
| 130 | + | baseURL = strings.TrimRight(baseURL, "/") | |||
| 131 | + | runServiceMethods := ermite.File_ermite_run_proto.Services().ByName("RunService").Methods() | |||
| 132 | + | return &runServiceClient{ | |||
| 133 | + | listRuns: connect.NewClient[ermite.ListRunsRequest, ermite.ListRunsResponse]( | |||
| 134 | + | httpClient, | |||
| 135 | + | baseURL+RunServiceListRunsProcedure, | |||
| 136 | + | connect.WithSchema(runServiceMethods.ByName("ListRuns")), | |||
| 137 | + | connect.WithClientOptions(opts...), | |||
| 138 | + | ), | |||
| 139 | + | getRun: connect.NewClient[ermite.GetRunRequest, ermite.GetRunResponse]( | |||
| 140 | + | httpClient, | |||
| 141 | + | baseURL+RunServiceGetRunProcedure, | |||
| 142 | + | connect.WithSchema(runServiceMethods.ByName("GetRun")), | |||
| 143 | + | connect.WithClientOptions(opts...), | |||
| 144 | + | ), | |||
| 145 | + | getJob: connect.NewClient[ermite.GetJobRequest, ermite.GetJobResponse]( | |||
| 146 | + | httpClient, | |||
| 147 | + | baseURL+RunServiceGetJobProcedure, | |||
| 148 | + | connect.WithSchema(runServiceMethods.ByName("GetJob")), | |||
| 149 | + | connect.WithClientOptions(opts...), | |||
| 150 | + | ), | |||
| 151 | + | getJobLog: connect.NewClient[ermite.GetJobLogRequest, ermite.GetJobLogResponse]( | |||
| 152 | + | httpClient, | |||
| 153 | + | baseURL+RunServiceGetJobLogProcedure, | |||
| 154 | + | connect.WithSchema(runServiceMethods.ByName("GetJobLog")), | |||
| 155 | + | connect.WithClientOptions(opts...), | |||
| 156 | + | ), | |||
| 157 | + | cancelRun: connect.NewClient[ermite.CancelRunRequest, ermite.CancelRunResponse]( | |||
| 158 | + | httpClient, | |||
| 159 | + | baseURL+RunServiceCancelRunProcedure, | |||
| 160 | + | connect.WithSchema(runServiceMethods.ByName("CancelRun")), | |||
| 161 | + | connect.WithClientOptions(opts...), | |||
| 162 | + | ), | |||
| 163 | + | rerunRun: connect.NewClient[ermite.RerunRunRequest, ermite.RerunRunResponse]( | |||
| 164 | + | httpClient, | |||
| 165 | + | baseURL+RunServiceRerunRunProcedure, | |||
| 166 | + | connect.WithSchema(runServiceMethods.ByName("RerunRun")), | |||
| 167 | + | connect.WithClientOptions(opts...), | |||
| 168 | + | ), | |||
| 169 | + | listChecks: connect.NewClient[ermite.ListChecksRequest, ermite.ListChecksResponse]( | |||
| 170 | + | httpClient, | |||
| 171 | + | baseURL+RunServiceListChecksProcedure, | |||
| 172 | + | connect.WithSchema(runServiceMethods.ByName("ListChecks")), | |||
| 173 | + | connect.WithClientOptions(opts...), | |||
| 174 | + | ), | |||
| 175 | + | listRunners: connect.NewClient[ermite.ListRunnersRequest, ermite.ListRunnersResponse]( | |||
| 176 | + | httpClient, | |||
| 177 | + | baseURL+RunServiceListRunnersProcedure, | |||
| 178 | + | connect.WithSchema(runServiceMethods.ByName("ListRunners")), | |||
| 179 | + | connect.WithClientOptions(opts...), | |||
| 180 | + | ), | |||
| 181 | + | removeRunner: connect.NewClient[ermite.RemoveRunnerRequest, ermite.RemoveRunnerResponse]( | |||
| 182 | + | httpClient, | |||
| 183 | + | baseURL+RunServiceRemoveRunnerProcedure, | |||
| 184 | + | connect.WithSchema(runServiceMethods.ByName("RemoveRunner")), | |||
| 185 | + | connect.WithClientOptions(opts...), | |||
| 186 | + | ), | |||
| 187 | + | } | |||
| 188 | + | } | |||
| 189 | + | ||||
| 190 | + | // runServiceClient implements RunServiceClient. | |||
| 191 | + | 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] | |||
| 201 | + | } | |||
| 202 | + | ||||
| 203 | + | // ListRuns calls ermite.RunService.ListRuns. | |||
| 204 | + | func (c *runServiceClient) ListRuns(ctx context.Context, req *connect.Request[ermite.ListRunsRequest]) (*connect.Response[ermite.ListRunsResponse], error) { | |||
| 205 | + | return c.listRuns.CallUnary(ctx, req) | |||
| 206 | + | } | |||
| 207 | + | ||||
| 208 | + | // GetRun calls ermite.RunService.GetRun. | |||
| 209 | + | func (c *runServiceClient) GetRun(ctx context.Context, req *connect.Request[ermite.GetRunRequest]) (*connect.Response[ermite.GetRunResponse], error) { | |||
| 210 | + | return c.getRun.CallUnary(ctx, req) | |||
| 211 | + | } | |||
| 212 | + | ||||
| 213 | + | // GetJob calls ermite.RunService.GetJob. | |||
| 214 | + | func (c *runServiceClient) GetJob(ctx context.Context, req *connect.Request[ermite.GetJobRequest]) (*connect.Response[ermite.GetJobResponse], error) { | |||
| 215 | + | return c.getJob.CallUnary(ctx, req) | |||
| 216 | + | } | |||
| 217 | + | ||||
| 218 | + | // GetJobLog calls ermite.RunService.GetJobLog. | |||
| 219 | + | func (c *runServiceClient) GetJobLog(ctx context.Context, req *connect.Request[ermite.GetJobLogRequest]) (*connect.ServerStreamForClient[ermite.GetJobLogResponse], error) { | |||
| 220 | + | return c.getJobLog.CallServerStream(ctx, req) | |||
| 221 | + | } | |||
| 222 | + | ||||
| 223 | + | // CancelRun calls ermite.RunService.CancelRun. | |||
| 224 | + | func (c *runServiceClient) CancelRun(ctx context.Context, req *connect.Request[ermite.CancelRunRequest]) (*connect.Response[ermite.CancelRunResponse], error) { | |||
| 225 | + | return c.cancelRun.CallUnary(ctx, req) | |||
| 226 | + | } | |||
| 227 | + | ||||
| 228 | + | // RerunRun calls ermite.RunService.RerunRun. | |||
| 229 | + | func (c *runServiceClient) RerunRun(ctx context.Context, req *connect.Request[ermite.RerunRunRequest]) (*connect.Response[ermite.RerunRunResponse], error) { | |||
| 230 | + | return c.rerunRun.CallUnary(ctx, req) | |||
| 231 | + | } | |||
| 232 | + | ||||
| 233 | + | // ListChecks calls ermite.RunService.ListChecks. | |||
| 234 | + | func (c *runServiceClient) ListChecks(ctx context.Context, req *connect.Request[ermite.ListChecksRequest]) (*connect.Response[ermite.ListChecksResponse], error) { | |||
| 235 | + | return c.listChecks.CallUnary(ctx, req) | |||
| 236 | + | } | |||
| 237 | + | ||||
| 238 | + | // ListRunners calls ermite.RunService.ListRunners. | |||
| 239 | + | func (c *runServiceClient) ListRunners(ctx context.Context, req *connect.Request[ermite.ListRunnersRequest]) (*connect.Response[ermite.ListRunnersResponse], error) { | |||
| 240 | + | return c.listRunners.CallUnary(ctx, req) | |||
| 241 | + | } | |||
| 242 | + | ||||
| 243 | + | // RemoveRunner calls ermite.RunService.RemoveRunner. | |||
| 244 | + | func (c *runServiceClient) RemoveRunner(ctx context.Context, req *connect.Request[ermite.RemoveRunnerRequest]) (*connect.Response[ermite.RemoveRunnerResponse], error) { | |||
| 245 | + | return c.removeRunner.CallUnary(ctx, req) | |||
| 246 | + | } | |||
| 247 | + | ||||
| 248 | + | // RunServiceHandler is an implementation of the ermite.RunService service. | |||
| 249 | + | type RunServiceHandler interface { | |||
| 250 | + | // ListRuns lists the runs of a repository, highest id (newest) first. Each | |||
| 251 | + | // run is described by its latest attempt. The filters narrow the result; a | |||
| 252 | + | // server without the feature "ci.runs.filters" ignores them and returns | |||
| 253 | + | // every run, so a client that needs them checks the feature. Fails with | |||
| 254 | + | // invalid_argument if a filter is malformed. | |||
| 255 | + | ListRuns(context.Context, *connect.Request[ermite.ListRunsRequest]) (*connect.Response[ermite.ListRunsResponse], error) | |||
| 256 | + | // GetRun returns a run with its jobs and their steps, for the latest attempt | |||
| 257 | + | // or for the one asked. Fails with not_found if the run or the attempt does | |||
| 258 | + | // not exist. | |||
| 259 | + | GetRun(context.Context, *connect.Request[ermite.GetRunRequest]) (*connect.Response[ermite.GetRunResponse], error) | |||
| 260 | + | // GetJob returns one job with its steps. Fails with not_found if the job | |||
| 261 | + | // or the attempt does not exist. | |||
| 262 | + | GetJob(context.Context, *connect.Request[ermite.GetJobRequest]) (*connect.Response[ermite.GetJobResponse], error) | |||
| 263 | + | // 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 | |||
| 265 | + | // text each. | |||
| 266 | + | // | |||
| 267 | + | // Without follow, the stream ends after the last line stored when the call | |||
| 268 | + | // was made. With follow, it goes on as lines arrive, with at most 1 second of | |||
| 269 | + | // delay after the runner's AppendLog, and ends without error once the job has | |||
| 270 | + | // completed and all its lines were sent; on a completed job follow changes | |||
| 271 | + | // nothing. A client that loses the stream calls again with the offset after | |||
| 272 | + | // the last line it received. The client may cancel at any time. | |||
| 273 | + | // | |||
| 274 | + | // Fails with not_found if the job, the attempt or its log does not exist (the | |||
| 275 | + | // log is removed by the retention sweep, and a queued job has none: with | |||
| 276 | + | // follow, the call waits for the first line instead), with out_of_range if | |||
| 277 | + | // offset is beyond the end of the log of a job without follow, and with | |||
| 278 | + | // resource_exhausted when there are too many concurrent log streams, in | |||
| 279 | + | // total or from one client. Like Watch, a stream ends with unauthenticated | |||
| 280 | + | // when the token was revoked or expired. | |||
| 281 | + | GetJobLog(context.Context, *connect.Request[ermite.GetJobLogRequest], *connect.ServerStream[ermite.GetJobLogResponse]) error | |||
| 282 | + | // CancelRun asks to cancel the latest attempt of a run. Needs the ci:write | |||
| 283 | + | // scope. Jobs that have not started are cancelled at once; jobs in progress | |||
| 284 | + | // get a CANCEL_REASON_USER request through WatchJob, and the run completes | |||
| 285 | + | // when they have ended, so the returned run may still be IN_PROGRESS. | |||
| 286 | + | // Cancelling a run that already has a cancellation requested succeeds and | |||
| 287 | + | // changes nothing. Fails with failed_precondition if the run has completed. | |||
| 288 | + | CancelRun(context.Context, *connect.Request[ermite.CancelRunRequest]) (*connect.Response[ermite.CancelRunResponse], error) | |||
| 289 | + | // RerunRun starts a new attempt of a completed run: same run id and number, | |||
| 290 | + | // attempt + 1, same commit and same workflow file as at the original run (the | |||
| 291 | + | // file of the repository is not read again). Needs the ci:write scope. Fails | |||
| 292 | + | // with failed_precondition if the latest attempt has not completed, or if | |||
| 293 | + | // failed_only is set and no job failed. The new attempt is returned QUEUED. | |||
| 294 | + | RerunRun(context.Context, *connect.Request[ermite.RerunRunRequest]) (*connect.Response[ermite.RerunRunResponse], error) | |||
| 295 | + | // ListChecks lists the checks of a commit: for each job of each run on that | |||
| 296 | + | // sha, the one of the latest attempt, ordered by creation time then name. | |||
| 297 | + | // A commit without a run has no check: the list is empty, which is not an | |||
| 298 | + | // error, and the server does not verify that the commit exists. Returns at | |||
| 299 | + | // most 1,000 checks (truncated tells). | |||
| 300 | + | ListChecks(context.Context, *connect.Request[ermite.ListChecksRequest]) (*connect.Response[ermite.ListChecksResponse], error) | |||
| 301 | + | // ListRunners lists the registered runners, ordered by name. Needs the | |||
| 302 | + | // runner:register scope. | |||
| 303 | + | ListRunners(context.Context, *connect.Request[ermite.ListRunnersRequest]) (*connect.Response[ermite.ListRunnersResponse], error) | |||
| 304 | + | // RemoveRunner removes a runner: its runner token stops working at once (a | |||
| 305 | + | // FetchJob in progress ends with unauthenticated) and it no longer gets | |||
| 306 | + | // jobs. A job it is running is not affected: it ends normally, or fails as | |||
| 307 | + | // a lost runner without heartbeats. Needs the runner:register scope. Fails | |||
| 308 | + | // with not_found if the id is unknown. | |||
| 309 | + | RemoveRunner(context.Context, *connect.Request[ermite.RemoveRunnerRequest]) (*connect.Response[ermite.RemoveRunnerResponse], error) | |||
| 310 | + | } | |||
| 311 | + | ||||
| 312 | + | // NewRunServiceHandler builds an HTTP handler from the service implementation. It returns the path | |||
| 313 | + | // on which to mount the handler and the handler itself. | |||
| 314 | + | // | |||
| 315 | + | // By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf | |||
| 316 | + | // and JSON codecs. They also support gzip compression. | |||
| 317 | + | func NewRunServiceHandler(svc RunServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { | |||
| 318 | + | runServiceMethods := ermite.File_ermite_run_proto.Services().ByName("RunService").Methods() | |||
| 319 | + | runServiceListRunsHandler := connect.NewUnaryHandler( | |||
| 320 | + | RunServiceListRunsProcedure, | |||
| 321 | + | svc.ListRuns, | |||
| 322 | + | connect.WithSchema(runServiceMethods.ByName("ListRuns")), | |||
| 323 | + | connect.WithHandlerOptions(opts...), | |||
| 324 | + | ) | |||
| 325 | + | runServiceGetRunHandler := connect.NewUnaryHandler( | |||
| 326 | + | RunServiceGetRunProcedure, | |||
| 327 | + | svc.GetRun, | |||
| 328 | + | connect.WithSchema(runServiceMethods.ByName("GetRun")), | |||
| 329 | + | connect.WithHandlerOptions(opts...), | |||
| 330 | + | ) | |||
| 331 | + | runServiceGetJobHandler := connect.NewUnaryHandler( | |||
| 332 | + | RunServiceGetJobProcedure, | |||
| 333 | + | svc.GetJob, | |||
| 334 | + | connect.WithSchema(runServiceMethods.ByName("GetJob")), | |||
| 335 | + | connect.WithHandlerOptions(opts...), | |||
| 336 | + | ) | |||
| 337 | + | runServiceGetJobLogHandler := connect.NewServerStreamHandler( | |||
| 338 | + | RunServiceGetJobLogProcedure, | |||
| 339 | + | svc.GetJobLog, | |||
| 340 | + | connect.WithSchema(runServiceMethods.ByName("GetJobLog")), | |||
| 341 | + | connect.WithHandlerOptions(opts...), | |||
| 342 | + | ) | |||
| 343 | + | runServiceCancelRunHandler := connect.NewUnaryHandler( | |||
| 344 | + | RunServiceCancelRunProcedure, | |||
| 345 | + | svc.CancelRun, | |||
| 346 | + | connect.WithSchema(runServiceMethods.ByName("CancelRun")), | |||
| 347 | + | connect.WithHandlerOptions(opts...), | |||
| 348 | + | ) | |||
| 349 | + | runServiceRerunRunHandler := connect.NewUnaryHandler( | |||
| 350 | + | RunServiceRerunRunProcedure, | |||
| 351 | + | svc.RerunRun, | |||
| 352 | + | connect.WithSchema(runServiceMethods.ByName("RerunRun")), | |||
| 353 | + | connect.WithHandlerOptions(opts...), | |||
| 354 | + | ) | |||
| 355 | + | runServiceListChecksHandler := connect.NewUnaryHandler( | |||
| 356 | + | RunServiceListChecksProcedure, | |||
| 357 | + | svc.ListChecks, | |||
| 358 | + | connect.WithSchema(runServiceMethods.ByName("ListChecks")), | |||
| 359 | + | connect.WithHandlerOptions(opts...), | |||
| 360 | + | ) | |||
| 361 | + | runServiceListRunnersHandler := connect.NewUnaryHandler( | |||
| 362 | + | RunServiceListRunnersProcedure, | |||
| 363 | + | svc.ListRunners, | |||
| 364 | + | connect.WithSchema(runServiceMethods.ByName("ListRunners")), | |||
| 365 | + | connect.WithHandlerOptions(opts...), | |||
| 366 | + | ) | |||
| 367 | + | runServiceRemoveRunnerHandler := connect.NewUnaryHandler( | |||
| 368 | + | RunServiceRemoveRunnerProcedure, | |||
| 369 | + | svc.RemoveRunner, | |||
| 370 | + | connect.WithSchema(runServiceMethods.ByName("RemoveRunner")), | |||
| 371 | + | connect.WithHandlerOptions(opts...), | |||
| 372 | + | ) | |||
| 373 | + | return "/ermite.RunService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |||
| 374 | + | switch r.URL.Path { | |||
| 375 | + | case RunServiceListRunsProcedure: | |||
| 376 | + | runServiceListRunsHandler.ServeHTTP(w, r) | |||
| 377 | + | case RunServiceGetRunProcedure: | |||
| 378 | + | runServiceGetRunHandler.ServeHTTP(w, r) | |||
| 379 | + | case RunServiceGetJobProcedure: | |||
| 380 | + | runServiceGetJobHandler.ServeHTTP(w, r) | |||
| 381 | + | case RunServiceGetJobLogProcedure: | |||
| 382 | + | runServiceGetJobLogHandler.ServeHTTP(w, r) | |||
| 383 | + | case RunServiceCancelRunProcedure: | |||
| 384 | + | runServiceCancelRunHandler.ServeHTTP(w, r) | |||
| 385 | + | case RunServiceRerunRunProcedure: | |||
| 386 | + | runServiceRerunRunHandler.ServeHTTP(w, r) | |||
| 387 | + | case RunServiceListChecksProcedure: | |||
| 388 | + | runServiceListChecksHandler.ServeHTTP(w, r) | |||
| 389 | + | case RunServiceListRunnersProcedure: | |||
| 390 | + | runServiceListRunnersHandler.ServeHTTP(w, r) | |||
| 391 | + | case RunServiceRemoveRunnerProcedure: | |||
| 392 | + | runServiceRemoveRunnerHandler.ServeHTTP(w, r) | |||
| 393 | + | default: | |||
| 394 | + | http.NotFound(w, r) | |||
| 395 | + | } | |||
| 396 | + | }) | |||
| 397 | + | } | |||
| 398 | + | ||||
| 399 | + | // UnimplementedRunServiceHandler returns CodeUnimplemented from all methods. | |||
| 400 | + | type UnimplementedRunServiceHandler struct{} | |||
| 401 | + | ||||
| 402 | + | func (UnimplementedRunServiceHandler) ListRuns(context.Context, *connect.Request[ermite.ListRunsRequest]) (*connect.Response[ermite.ListRunsResponse], error) { | |||
| 403 | + | return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunService.ListRuns is not implemented")) | |||
| 404 | + | } | |||
| 405 | + | ||||
| 406 | + | func (UnimplementedRunServiceHandler) GetRun(context.Context, *connect.Request[ermite.GetRunRequest]) (*connect.Response[ermite.GetRunResponse], error) { | |||
| 407 | + | return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunService.GetRun is not implemented")) | |||
| 408 | + | } | |||
| 409 | + | ||||
| 410 | + | func (UnimplementedRunServiceHandler) GetJob(context.Context, *connect.Request[ermite.GetJobRequest]) (*connect.Response[ermite.GetJobResponse], error) { | |||
| 411 | + | return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunService.GetJob is not implemented")) | |||
| 412 | + | } | |||
| 413 | + | ||||
| 414 | + | 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")) | |||
| 416 | + | } | |||
| 417 | + | ||||
| 418 | + | func (UnimplementedRunServiceHandler) CancelRun(context.Context, *connect.Request[ermite.CancelRunRequest]) (*connect.Response[ermite.CancelRunResponse], error) { | |||
| 419 | + | return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunService.CancelRun is not implemented")) | |||
| 420 | + | } | |||
| 421 | + | ||||
| 422 | + | func (UnimplementedRunServiceHandler) RerunRun(context.Context, *connect.Request[ermite.RerunRunRequest]) (*connect.Response[ermite.RerunRunResponse], error) { | |||
| 423 | + | return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunService.RerunRun is not implemented")) | |||
| 424 | + | } | |||
| 425 | + | ||||
| 426 | + | func (UnimplementedRunServiceHandler) ListChecks(context.Context, *connect.Request[ermite.ListChecksRequest]) (*connect.Response[ermite.ListChecksResponse], error) { | |||
| 427 | + | return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunService.ListChecks is not implemented")) | |||
| 428 | + | } | |||
| 429 | + | ||||
| 430 | + | func (UnimplementedRunServiceHandler) ListRunners(context.Context, *connect.Request[ermite.ListRunnersRequest]) (*connect.Response[ermite.ListRunnersResponse], error) { | |||
| 431 | + | return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunService.ListRunners is not implemented")) | |||
| 432 | + | } | |||
| 433 | + | ||||
| 434 | + | func (UnimplementedRunServiceHandler) RemoveRunner(context.Context, *connect.Request[ermite.RemoveRunnerRequest]) (*connect.Response[ermite.RemoveRunnerResponse], error) { | |||
| 435 | + | return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunService.RemoveRunner is not implemented")) | |||
| 436 | + | } | |||
A gen/ermite/ermiteconnect/runner.connect.go +588 -0
| @@ -0,0 +1,588 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | // Code generated by protoc-gen-connect-go. DO NOT EDIT. | |
| 4 | + | // | |
| 5 | + | // Source: ermite/runner.proto | |
| 6 | + | ||
| 7 | + | package ermiteconnect | |
| 8 | + | ||
| 9 | + | import ( | |
| 10 | + | ermite "bnema.dev/ermite-api/gen/ermite" | |
| 11 | + | connect "connectrpc.com/connect" | |
| 12 | + | context "context" | |
| 13 | + | errors "errors" | |
| 14 | + | http "net/http" | |
| 15 | + | strings "strings" | |
| 16 | + | ) | |
| 17 | + | ||
| 18 | + | // This is a compile-time assertion to ensure that this generated file and the connect package are | |
| 19 | + | // compatible. If you get a compiler error that this constant is not defined, this code was | |
| 20 | + | // generated with a version of connect newer than the one compiled into your binary. You can fix the | |
| 21 | + | // problem by either regenerating this code with an older version of connect or updating the connect | |
| 22 | + | // version compiled into your binary. | |
| 23 | + | const _ = connect.IsAtLeastVersion1_13_0 | |
| 24 | + | ||
| 25 | + | const ( | |
| 26 | + | // RunnerServiceName is the fully-qualified name of the RunnerService service. | |
| 27 | + | RunnerServiceName = "ermite.RunnerService" | |
| 28 | + | ) | |
| 29 | + | ||
| 30 | + | // These constants are the fully-qualified names of the RPCs defined in this package. They're | |
| 31 | + | // exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. | |
| 32 | + | // | |
| 33 | + | // Note that these are different from the fully-qualified method names used by | |
| 34 | + | // google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to | |
| 35 | + | // reflection-formatted method names, remove the leading slash and convert the remaining slash to a | |
| 36 | + | // period. | |
| 37 | + | const ( | |
| 38 | + | // RunnerServiceRegisterRunnerProcedure is the fully-qualified name of the RunnerService's | |
| 39 | + | // RegisterRunner RPC. | |
| 40 | + | RunnerServiceRegisterRunnerProcedure = "/ermite.RunnerService/RegisterRunner" | |
| 41 | + | // RunnerServiceFetchJobProcedure is the fully-qualified name of the RunnerService's FetchJob RPC. | |
| 42 | + | RunnerServiceFetchJobProcedure = "/ermite.RunnerService/FetchJob" | |
| 43 | + | // RunnerServiceUpdateJobProcedure is the fully-qualified name of the RunnerService's UpdateJob RPC. | |
| 44 | + | RunnerServiceUpdateJobProcedure = "/ermite.RunnerService/UpdateJob" | |
| 45 | + | // RunnerServiceAppendLogProcedure is the fully-qualified name of the RunnerService's AppendLog RPC. | |
| 46 | + | RunnerServiceAppendLogProcedure = "/ermite.RunnerService/AppendLog" | |
| 47 | + | // RunnerServiceHeartbeatProcedure is the fully-qualified name of the RunnerService's Heartbeat RPC. | |
| 48 | + | RunnerServiceHeartbeatProcedure = "/ermite.RunnerService/Heartbeat" | |
| 49 | + | // RunnerServiceWatchJobProcedure is the fully-qualified name of the RunnerService's WatchJob RPC. | |
| 50 | + | RunnerServiceWatchJobProcedure = "/ermite.RunnerService/WatchJob" | |
| 51 | + | // RunnerServiceCompleteJobProcedure is the fully-qualified name of the RunnerService's CompleteJob | |
| 52 | + | // RPC. | |
| 53 | + | RunnerServiceCompleteJobProcedure = "/ermite.RunnerService/CompleteJob" | |
| 54 | + | ) | |
| 55 | + | ||
| 56 | + | // RunnerServiceClient is a client for the ermite.RunnerService service. | |
| 57 | + | type RunnerServiceClient interface { | |
| 58 | + | // RegisterRunner registers a runner and returns its runner token, which is | |
| 59 | + | // shown only in this response; the runner keeps it. The caller needs the | |
| 60 | + | // runner:register scope on a token that is not restricted to repositories | |
| 61 | + | // (a runner serves every repository), or the root token. Fails with | |
| 62 | + | // unauthenticated without a valid token, permission_denied without the | |
| 63 | + | // scope or with a repository-restricted token, invalid_argument if a field | |
| 64 | + | // breaks a rule, and already_exists if name is taken by another runner and | |
| 65 | + | // replace is not set. | |
| 66 | + | RegisterRunner(context.Context, *connect.Request[ermite.RegisterRunnerRequest]) (*connect.Response[ermite.RegisterRunnerResponse], error) | |
| 67 | + | // FetchJob asks for a job to run (long poll). The server holds the call | |
| 68 | + | // until a queued job matches the runner, or about 50 seconds (never more | |
| 69 | + | // than 50) pass, then answers; without a job the response has no job and the | |
| 70 | + | // runner calls again after retry_after. Clients set a call timeout above 50 | |
| 71 | + | // seconds (60 is suggested) and must not poll when they have no free | |
| 72 | + | // capacity. | |
| 73 | + | // | |
| 74 | + | // A job matches when every label of its runs-on is among the labels of the | |
| 75 | + | // runner (case-insensitive). A job is given to exactly one runner, even when | |
| 76 | + | // many runners and many polls compete; the assignment is recorded before | |
| 77 | + | // the response is sent. The job stays QUEUED for the owner until it is | |
| 78 | + | // acknowledged. | |
| 79 | + | // | |
| 80 | + | // Acknowledgement. The runner's first Heartbeat for the job is the | |
| 81 | + | // acknowledgement, and the runner sends it as soon as it has the job, before | |
| 82 | + | // anything else. If none arrives within 30 seconds of the assignment (the | |
| 83 | + | // response was lost, or the runner died), the server cancels the assignment: | |
| 84 | + | // the job token is revoked, the job goes back to QUEUED with a fresh token | |
| 85 | + | // and can be given to any runner, the same one included; nothing the old | |
| 86 | + | // assignment stored is kept. This is not a failure of the job. A job whose | |
| 87 | + | // assignment is cancelled 3 times in a row is failed with CI_CONCLUSION_FAILURE | |
| 88 | + | // and an error saying so. | |
| 89 | + | // | |
| 90 | + | // Retry after a lost response. poll_id lets a client retry a poll safely: | |
| 91 | + | // a call with the same poll_id as an earlier call of the same runner that | |
| 92 | + | // assigned a job returns that same assignment (same Job, same token) as long | |
| 93 | + | // as it has not been acknowledged or cancelled, instead of taking another. | |
| 94 | + | // | |
| 95 | + | // A runner that crashed or whose host restarted cannot resume a job: it does | |
| 96 | + | // not keep the job token, so the job fails with the lost-runner error after | |
| 97 | + | // the heartbeat timeout (see Heartbeat). | |
| 98 | + | // | |
| 99 | + | // Needs the runner token. Fails with unauthenticated when the runner was | |
| 100 | + | // removed (also ending a call in progress) and invalid_argument if labels | |
| 101 | + | // or poll_id break a rule. | |
| 102 | + | FetchJob(context.Context, *connect.Request[ermite.FetchJobRequest]) (*connect.Response[ermite.FetchJobResponse], error) | |
| 103 | + | // UpdateJob reports the state of the steps of a job while it runs. Needs the | |
| 104 | + | // job token. Each call is applied whole or not at all, and repeating an | |
| 105 | + | // identical call changes nothing, so a runner may retry after a network | |
| 106 | + | // error. To end the job use CompleteJob. | |
| 107 | + | // | |
| 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. | |
| 112 | + | 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. | |
| 114 | + | // | |
| 115 | + | // The log of a job is one sequence of lines, numbered from 0 across all | |
| 116 | + | // steps; offset is the number of lines the server holds when the request is | |
| 117 | + | // applied, that is the number of the first line of the request. Calls must | |
| 118 | + | // go in order, one at a time, and are idempotent: | |
| 119 | + | // | |
| 120 | + | // - offset equal to the server's line count: the lines are appended; | |
| 121 | + | // - offset below it: the first (count - offset) lines were already stored | |
| 122 | + | // (a retry after a lost response) and are skipped, and the rest is | |
| 123 | + | // appended; a request entirely stored changes nothing. The skipped lines | |
| 124 | + | // are not compared with the stored ones: a retry whose lines differ from | |
| 125 | + | // the first attempt is silently dropped, so a client must resend the | |
| 126 | + | // same lines at the same offset; | |
| 127 | + | // - offset above it: out_of_range (a gap is a bug of the client; the | |
| 128 | + | // message gives the line count the server holds). | |
| 129 | + | // | |
| 130 | + | // The response gives the line count after the call, so a client can check | |
| 131 | + | // itself. Offsets resume only after a network error within the same runner | |
| 132 | + | // process; they are not a way to resume a job after a crash. | |
| 133 | + | // | |
| 134 | + | // A request has at most 1,000 lines whose text adds up to at most 64 KiB | |
| 135 | + | // (65,536 bytes), all in the same step; more is invalid_argument. A line | |
| 136 | + | // longer than that is cut by the client into pieces, see LogLine.continued. | |
| 137 | + | // | |
| 138 | + | // Steps. The step of a request must be at least the step of the last line | |
| 139 | + | // the server holds, because the log is ordered by step (applied lines only: | |
| 140 | + | // lines skipped as already stored are not checked); a lower step is | |
| 141 | + | // failed_precondition. Lines for a step that UpdateJob has not announced | |
| 142 | + | // create it: it starts IN_PROGRESS at the time of its first line, with an | |
| 143 | + | // empty name until an UpdateJob gives one. | |
| 144 | + | // | |
| 145 | + | // The log of a job is limited to 512 MiB (536,870,912 bytes) as stored by | |
| 146 | + | // the server (text, timestamps and separators). The call that would exceed | |
| 147 | + | // it appends nothing; the server adds a last line saying that the limit was | |
| 148 | + | // reached, ends the job with CI_CONCLUSION_FAILURE and RunJob.error set, and | |
| 149 | + | // answers failed_precondition with a JobEnded detail like any call after the | |
| 150 | + | // end of a job. The runner then stops the job. | |
| 151 | + | // | |
| 152 | + | // Lines sent after the end of the job are rejected, so a runner flushes all | |
| 153 | + | // its AppendLog lines before it calls CompleteJob. | |
| 154 | + | // | |
| 155 | + | // Fails with invalid_argument if a rule is broken (step number, text with a | |
| 156 | + | // line feed or invalid UTF-8, missing time, a continued first line). | |
| 157 | + | AppendLog(context.Context, *connect.Request[ermite.AppendLogRequest]) (*connect.Response[ermite.AppendLogResponse], error) | |
| 158 | + | // Heartbeat tells the server that the runner is alive and still working on | |
| 159 | + | // the job. Needs the job token. The first one acknowledges the assignment | |
| 160 | + | // (see FetchJob). The runner then calls it every 30 seconds for as long as | |
| 161 | + | // the job runs. The deadline is 2 minutes after the later of the last | |
| 162 | + | // heartbeat and the start of the server (so a server restart does not fail | |
| 163 | + | // the jobs of runners that could not reach it): past it the server ends the | |
| 164 | + | // job with CI_CONCLUSION_FAILURE and the message "The self-hosted runner: | |
| 165 | + | // <name> lost communication with the server...". Only Heartbeat counts: | |
| 166 | + | // other calls do not keep a job alive. | |
| 167 | + | // | |
| 168 | + | // A runner that gets unavailable (or any network error) retries Heartbeat | |
| 169 | + | // every 5 seconds until it succeeds, instead of waiting for its next 30 | |
| 170 | + | // second tick, and keeps the job running meanwhile. | |
| 171 | + | Heartbeat(context.Context, *connect.Request[ermite.HeartbeatRequest]) (*connect.Response[ermite.HeartbeatResponse], error) | |
| 172 | + | // WatchJob streams what the runner must react to. Needs the job token. The | |
| 173 | + | // runner opens it when it starts the job and keeps it open; it reopens it | |
| 174 | + | // after any error other than unauthenticated and the failed_precondition | |
| 175 | + | // that says the job ended, and after a clean close of | |
| 176 | + | // the stream that did not end with a JobEnded message. | |
| 177 | + | // | |
| 178 | + | // When the stream opens and a cancellation has already been requested, the | |
| 179 | + | // first message is that request, so a reconnect never misses one. Only the | |
| 180 | + | // first reason is sent: a job already cancelled by the owner does not get a | |
| 181 | + | // second message when its time budget expires. The server also sends a | |
| 182 | + | // Keepalive at least every 30 seconds, which the client ignores. When the | |
| 183 | + | // job ends the server sends JobEnded, then closes the stream without error. | |
| 184 | + | // On a job that has already ended the call fails like the other RPCs | |
| 185 | + | // (failed_precondition with a JobEnded detail). | |
| 186 | + | // | |
| 187 | + | // On a CancelRequest the runner stops the job as the official runner does | |
| 188 | + | // (interrupt, then terminate, then kill) and reports the conclusion given | |
| 189 | + | // for the reason with CompleteJob. | |
| 190 | + | // | |
| 191 | + | // The server does not trust the runner to obey. If no conclusion arrives | |
| 192 | + | // within 60 seconds after a cancellation request, whatever the reason (user, | |
| 193 | + | // fail-fast, timeout), the server ends the job itself: CI_CONCLUSION_CANCELLED | |
| 194 | + | // for user and fail-fast, CI_CONCLUSION_TIMED_OUT for timeout, with an error | |
| 195 | + | // saying that the runner did not answer, and the steps not completed are | |
| 196 | + | // completed as cancelled. The runner's later CompleteJob returns that result. | |
| 197 | + | WatchJob(context.Context, *connect.Request[ermite.WatchJobRequest]) (*connect.ServerStreamForClient[ermite.WatchJobResponse], error) | |
| 198 | + | // CompleteJob ends a job: it reports the final steps, the outputs and the | |
| 199 | + | // conclusion. Needs the job token, which stays valid for this RPC for 24 | |
| 200 | + | // hours after the end of the job, however it ended. The call is applied whole | |
| 201 | + | // or not at all. Once the job has ended, the token works for no other RPC | |
| 202 | + | // (they answer failed_precondition with a JobEnded detail). | |
| 203 | + | // | |
| 204 | + | // It is idempotent. The first call that finds the job running ends it and | |
| 205 | + | // answers applied = true. Any later call, a retry after a lost response or a | |
| 206 | + | // call after the server ended the job itself, is not validated and changes | |
| 207 | + | // nothing: it answers OK with the stored conclusion and error and | |
| 208 | + | // applied = false (a runner whose conclusion differs from the stored one | |
| 209 | + | // simply logs it; the outputs it sent are dropped). | |
| 210 | + | // | |
| 211 | + | // Lines sent with AppendLog after this call are rejected: a runner flushes | |
| 212 | + | // all its log lines first. Steps that were not reported COMPLETED when the job | |
| 213 | + | // 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). | |
| 215 | + | // | |
| 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. | |
| 218 | + | CompleteJob(context.Context, *connect.Request[ermite.CompleteJobRequest]) (*connect.Response[ermite.CompleteJobResponse], error) | |
| 219 | + | } | |
| 220 | + | ||
| 221 | + | // NewRunnerServiceClient constructs a client for the ermite.RunnerService service. By default, it | |
| 222 | + | // uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and sends | |
| 223 | + | // uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or | |
| 224 | + | // connect.WithGRPCWeb() options. | |
| 225 | + | // | |
| 226 | + | // The URL supplied here should be the base URL for the Connect or gRPC server (for example, | |
| 227 | + | // http://api.acme.com or https://acme.com/grpc). | |
| 228 | + | func NewRunnerServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) RunnerServiceClient { | |
| 229 | + | baseURL = strings.TrimRight(baseURL, "/") | |
| 230 | + | runnerServiceMethods := ermite.File_ermite_runner_proto.Services().ByName("RunnerService").Methods() | |
| 231 | + | return &runnerServiceClient{ | |
| 232 | + | registerRunner: connect.NewClient[ermite.RegisterRunnerRequest, ermite.RegisterRunnerResponse]( | |
| 233 | + | httpClient, | |
| 234 | + | baseURL+RunnerServiceRegisterRunnerProcedure, | |
| 235 | + | connect.WithSchema(runnerServiceMethods.ByName("RegisterRunner")), | |
| 236 | + | connect.WithClientOptions(opts...), | |
| 237 | + | ), | |
| 238 | + | fetchJob: connect.NewClient[ermite.FetchJobRequest, ermite.FetchJobResponse]( | |
| 239 | + | httpClient, | |
| 240 | + | baseURL+RunnerServiceFetchJobProcedure, | |
| 241 | + | connect.WithSchema(runnerServiceMethods.ByName("FetchJob")), | |
| 242 | + | connect.WithClientOptions(opts...), | |
| 243 | + | ), | |
| 244 | + | updateJob: connect.NewClient[ermite.UpdateJobRequest, ermite.UpdateJobResponse]( | |
| 245 | + | httpClient, | |
| 246 | + | baseURL+RunnerServiceUpdateJobProcedure, | |
| 247 | + | connect.WithSchema(runnerServiceMethods.ByName("UpdateJob")), | |
| 248 | + | connect.WithClientOptions(opts...), | |
| 249 | + | ), | |
| 250 | + | appendLog: connect.NewClient[ermite.AppendLogRequest, ermite.AppendLogResponse]( | |
| 251 | + | httpClient, | |
| 252 | + | baseURL+RunnerServiceAppendLogProcedure, | |
| 253 | + | connect.WithSchema(runnerServiceMethods.ByName("AppendLog")), | |
| 254 | + | connect.WithClientOptions(opts...), | |
| 255 | + | ), | |
| 256 | + | heartbeat: connect.NewClient[ermite.HeartbeatRequest, ermite.HeartbeatResponse]( | |
| 257 | + | httpClient, | |
| 258 | + | baseURL+RunnerServiceHeartbeatProcedure, | |
| 259 | + | connect.WithSchema(runnerServiceMethods.ByName("Heartbeat")), | |
| 260 | + | connect.WithClientOptions(opts...), | |
| 261 | + | ), | |
| 262 | + | watchJob: connect.NewClient[ermite.WatchJobRequest, ermite.WatchJobResponse]( | |
| 263 | + | httpClient, | |
| 264 | + | baseURL+RunnerServiceWatchJobProcedure, | |
| 265 | + | connect.WithSchema(runnerServiceMethods.ByName("WatchJob")), | |
| 266 | + | connect.WithClientOptions(opts...), | |
| 267 | + | ), | |
| 268 | + | completeJob: connect.NewClient[ermite.CompleteJobRequest, ermite.CompleteJobResponse]( | |
| 269 | + | httpClient, | |
| 270 | + | baseURL+RunnerServiceCompleteJobProcedure, | |
| 271 | + | connect.WithSchema(runnerServiceMethods.ByName("CompleteJob")), | |
| 272 | + | connect.WithClientOptions(opts...), | |
| 273 | + | ), | |
| 274 | + | } | |
| 275 | + | } | |
| 276 | + | ||
| 277 | + | // runnerServiceClient implements RunnerServiceClient. | |
| 278 | + | type runnerServiceClient struct { | |
| 279 | + | registerRunner *connect.Client[ermite.RegisterRunnerRequest, ermite.RegisterRunnerResponse] | |
| 280 | + | fetchJob *connect.Client[ermite.FetchJobRequest, ermite.FetchJobResponse] | |
| 281 | + | updateJob *connect.Client[ermite.UpdateJobRequest, ermite.UpdateJobResponse] | |
| 282 | + | appendLog *connect.Client[ermite.AppendLogRequest, ermite.AppendLogResponse] | |
| 283 | + | heartbeat *connect.Client[ermite.HeartbeatRequest, ermite.HeartbeatResponse] | |
| 284 | + | watchJob *connect.Client[ermite.WatchJobRequest, ermite.WatchJobResponse] | |
| 285 | + | completeJob *connect.Client[ermite.CompleteJobRequest, ermite.CompleteJobResponse] | |
| 286 | + | } | |
| 287 | + | ||
| 288 | + | // RegisterRunner calls ermite.RunnerService.RegisterRunner. | |
| 289 | + | func (c *runnerServiceClient) RegisterRunner(ctx context.Context, req *connect.Request[ermite.RegisterRunnerRequest]) (*connect.Response[ermite.RegisterRunnerResponse], error) { | |
| 290 | + | return c.registerRunner.CallUnary(ctx, req) | |
| 291 | + | } | |
| 292 | + | ||
| 293 | + | // FetchJob calls ermite.RunnerService.FetchJob. | |
| 294 | + | func (c *runnerServiceClient) FetchJob(ctx context.Context, req *connect.Request[ermite.FetchJobRequest]) (*connect.Response[ermite.FetchJobResponse], error) { | |
| 295 | + | return c.fetchJob.CallUnary(ctx, req) | |
| 296 | + | } | |
| 297 | + | ||
| 298 | + | // UpdateJob calls ermite.RunnerService.UpdateJob. | |
| 299 | + | func (c *runnerServiceClient) UpdateJob(ctx context.Context, req *connect.Request[ermite.UpdateJobRequest]) (*connect.Response[ermite.UpdateJobResponse], error) { | |
| 300 | + | return c.updateJob.CallUnary(ctx, req) | |
| 301 | + | } | |
| 302 | + | ||
| 303 | + | // AppendLog calls ermite.RunnerService.AppendLog. | |
| 304 | + | func (c *runnerServiceClient) AppendLog(ctx context.Context, req *connect.Request[ermite.AppendLogRequest]) (*connect.Response[ermite.AppendLogResponse], error) { | |
| 305 | + | return c.appendLog.CallUnary(ctx, req) | |
| 306 | + | } | |
| 307 | + | ||
| 308 | + | // Heartbeat calls ermite.RunnerService.Heartbeat. | |
| 309 | + | func (c *runnerServiceClient) Heartbeat(ctx context.Context, req *connect.Request[ermite.HeartbeatRequest]) (*connect.Response[ermite.HeartbeatResponse], error) { | |
| 310 | + | return c.heartbeat.CallUnary(ctx, req) | |
| 311 | + | } | |
| 312 | + | ||
| 313 | + | // WatchJob calls ermite.RunnerService.WatchJob. | |
| 314 | + | func (c *runnerServiceClient) WatchJob(ctx context.Context, req *connect.Request[ermite.WatchJobRequest]) (*connect.ServerStreamForClient[ermite.WatchJobResponse], error) { | |
| 315 | + | return c.watchJob.CallServerStream(ctx, req) | |
| 316 | + | } | |
| 317 | + | ||
| 318 | + | // CompleteJob calls ermite.RunnerService.CompleteJob. | |
| 319 | + | func (c *runnerServiceClient) CompleteJob(ctx context.Context, req *connect.Request[ermite.CompleteJobRequest]) (*connect.Response[ermite.CompleteJobResponse], error) { | |
| 320 | + | return c.completeJob.CallUnary(ctx, req) | |
| 321 | + | } | |
| 322 | + | ||
| 323 | + | // RunnerServiceHandler is an implementation of the ermite.RunnerService service. | |
| 324 | + | type RunnerServiceHandler interface { | |
| 325 | + | // RegisterRunner registers a runner and returns its runner token, which is | |
| 326 | + | // shown only in this response; the runner keeps it. The caller needs the | |
| 327 | + | // runner:register scope on a token that is not restricted to repositories | |
| 328 | + | // (a runner serves every repository), or the root token. Fails with | |
| 329 | + | // unauthenticated without a valid token, permission_denied without the | |
| 330 | + | // scope or with a repository-restricted token, invalid_argument if a field | |
| 331 | + | // breaks a rule, and already_exists if name is taken by another runner and | |
| 332 | + | // replace is not set. | |
| 333 | + | RegisterRunner(context.Context, *connect.Request[ermite.RegisterRunnerRequest]) (*connect.Response[ermite.RegisterRunnerResponse], error) | |
| 334 | + | // FetchJob asks for a job to run (long poll). The server holds the call | |
| 335 | + | // until a queued job matches the runner, or about 50 seconds (never more | |
| 336 | + | // than 50) pass, then answers; without a job the response has no job and the | |
| 337 | + | // runner calls again after retry_after. Clients set a call timeout above 50 | |
| 338 | + | // seconds (60 is suggested) and must not poll when they have no free | |
| 339 | + | // capacity. | |
| 340 | + | // | |
| 341 | + | // A job matches when every label of its runs-on is among the labels of the | |
| 342 | + | // runner (case-insensitive). A job is given to exactly one runner, even when | |
| 343 | + | // many runners and many polls compete; the assignment is recorded before | |
| 344 | + | // the response is sent. The job stays QUEUED for the owner until it is | |
| 345 | + | // acknowledged. | |
| 346 | + | // | |
| 347 | + | // Acknowledgement. The runner's first Heartbeat for the job is the | |
| 348 | + | // acknowledgement, and the runner sends it as soon as it has the job, before | |
| 349 | + | // anything else. If none arrives within 30 seconds of the assignment (the | |
| 350 | + | // response was lost, or the runner died), the server cancels the assignment: | |
| 351 | + | // the job token is revoked, the job goes back to QUEUED with a fresh token | |
| 352 | + | // and can be given to any runner, the same one included; nothing the old | |
| 353 | + | // assignment stored is kept. This is not a failure of the job. A job whose | |
| 354 | + | // assignment is cancelled 3 times in a row is failed with CI_CONCLUSION_FAILURE | |
| 355 | + | // and an error saying so. | |
| 356 | + | // | |
| 357 | + | // Retry after a lost response. poll_id lets a client retry a poll safely: | |
| 358 | + | // a call with the same poll_id as an earlier call of the same runner that | |
| 359 | + | // assigned a job returns that same assignment (same Job, same token) as long | |
| 360 | + | // as it has not been acknowledged or cancelled, instead of taking another. | |
| 361 | + | // | |
| 362 | + | // A runner that crashed or whose host restarted cannot resume a job: it does | |
| 363 | + | // not keep the job token, so the job fails with the lost-runner error after | |
| 364 | + | // the heartbeat timeout (see Heartbeat). | |
| 365 | + | // | |
| 366 | + | // Needs the runner token. Fails with unauthenticated when the runner was | |
| 367 | + | // removed (also ending a call in progress) and invalid_argument if labels | |
| 368 | + | // or poll_id break a rule. | |
| 369 | + | FetchJob(context.Context, *connect.Request[ermite.FetchJobRequest]) (*connect.Response[ermite.FetchJobResponse], error) | |
| 370 | + | // UpdateJob reports the state of the steps of a job while it runs. Needs the | |
| 371 | + | // job token. Each call is applied whole or not at all, and repeating an | |
| 372 | + | // identical call changes nothing, so a runner may retry after a network | |
| 373 | + | // error. To end the job use CompleteJob. | |
| 374 | + | // | |
| 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. | |
| 379 | + | 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. | |
| 381 | + | // | |
| 382 | + | // The log of a job is one sequence of lines, numbered from 0 across all | |
| 383 | + | // steps; offset is the number of lines the server holds when the request is | |
| 384 | + | // applied, that is the number of the first line of the request. Calls must | |
| 385 | + | // go in order, one at a time, and are idempotent: | |
| 386 | + | // | |
| 387 | + | // - offset equal to the server's line count: the lines are appended; | |
| 388 | + | // - offset below it: the first (count - offset) lines were already stored | |
| 389 | + | // (a retry after a lost response) and are skipped, and the rest is | |
| 390 | + | // appended; a request entirely stored changes nothing. The skipped lines | |
| 391 | + | // are not compared with the stored ones: a retry whose lines differ from | |
| 392 | + | // the first attempt is silently dropped, so a client must resend the | |
| 393 | + | // same lines at the same offset; | |
| 394 | + | // - offset above it: out_of_range (a gap is a bug of the client; the | |
| 395 | + | // message gives the line count the server holds). | |
| 396 | + | // | |
| 397 | + | // The response gives the line count after the call, so a client can check | |
| 398 | + | // itself. Offsets resume only after a network error within the same runner | |
| 399 | + | // process; they are not a way to resume a job after a crash. | |
| 400 | + | // | |
| 401 | + | // A request has at most 1,000 lines whose text adds up to at most 64 KiB | |
| 402 | + | // (65,536 bytes), all in the same step; more is invalid_argument. A line | |
| 403 | + | // longer than that is cut by the client into pieces, see LogLine.continued. | |
| 404 | + | // | |
| 405 | + | // Steps. The step of a request must be at least the step of the last line | |
| 406 | + | // the server holds, because the log is ordered by step (applied lines only: | |
| 407 | + | // lines skipped as already stored are not checked); a lower step is | |
| 408 | + | // failed_precondition. Lines for a step that UpdateJob has not announced | |
| 409 | + | // create it: it starts IN_PROGRESS at the time of its first line, with an | |
| 410 | + | // empty name until an UpdateJob gives one. | |
| 411 | + | // | |
| 412 | + | // The log of a job is limited to 512 MiB (536,870,912 bytes) as stored by | |
| 413 | + | // the server (text, timestamps and separators). The call that would exceed | |
| 414 | + | // it appends nothing; the server adds a last line saying that the limit was | |
| 415 | + | // reached, ends the job with CI_CONCLUSION_FAILURE and RunJob.error set, and | |
| 416 | + | // answers failed_precondition with a JobEnded detail like any call after the | |
| 417 | + | // end of a job. The runner then stops the job. | |
| 418 | + | // | |
| 419 | + | // Lines sent after the end of the job are rejected, so a runner flushes all | |
| 420 | + | // its AppendLog lines before it calls CompleteJob. | |
| 421 | + | // | |
| 422 | + | // Fails with invalid_argument if a rule is broken (step number, text with a | |
| 423 | + | // line feed or invalid UTF-8, missing time, a continued first line). | |
| 424 | + | AppendLog(context.Context, *connect.Request[ermite.AppendLogRequest]) (*connect.Response[ermite.AppendLogResponse], error) | |
| 425 | + | // Heartbeat tells the server that the runner is alive and still working on | |
| 426 | + | // the job. Needs the job token. The first one acknowledges the assignment | |
| 427 | + | // (see FetchJob). The runner then calls it every 30 seconds for as long as | |
| 428 | + | // the job runs. The deadline is 2 minutes after the later of the last | |
| 429 | + | // heartbeat and the start of the server (so a server restart does not fail | |
| 430 | + | // the jobs of runners that could not reach it): past it the server ends the | |
| 431 | + | // job with CI_CONCLUSION_FAILURE and the message "The self-hosted runner: | |
| 432 | + | // <name> lost communication with the server...". Only Heartbeat counts: | |
| 433 | + | // other calls do not keep a job alive. | |
| 434 | + | // | |
| 435 | + | // A runner that gets unavailable (or any network error) retries Heartbeat | |
| 436 | + | // every 5 seconds until it succeeds, instead of waiting for its next 30 | |
| 437 | + | // second tick, and keeps the job running meanwhile. | |
| 438 | + | Heartbeat(context.Context, *connect.Request[ermite.HeartbeatRequest]) (*connect.Response[ermite.HeartbeatResponse], error) | |
| 439 | + | // WatchJob streams what the runner must react to. Needs the job token. The | |
| 440 | + | // runner opens it when it starts the job and keeps it open; it reopens it | |
| 441 | + | // after any error other than unauthenticated and the failed_precondition | |
| 442 | + | // that says the job ended, and after a clean close of | |
| 443 | + | // the stream that did not end with a JobEnded message. | |
| 444 | + | // | |
| 445 | + | // When the stream opens and a cancellation has already been requested, the | |
| 446 | + | // first message is that request, so a reconnect never misses one. Only the | |
| 447 | + | // first reason is sent: a job already cancelled by the owner does not get a | |
| 448 | + | // second message when its time budget expires. The server also sends a | |
| 449 | + | // Keepalive at least every 30 seconds, which the client ignores. When the | |
| 450 | + | // job ends the server sends JobEnded, then closes the stream without error. | |
| 451 | + | // On a job that has already ended the call fails like the other RPCs | |
| 452 | + | // (failed_precondition with a JobEnded detail). | |
| 453 | + | // | |
| 454 | + | // On a CancelRequest the runner stops the job as the official runner does | |
| 455 | + | // (interrupt, then terminate, then kill) and reports the conclusion given | |
| 456 | + | // for the reason with CompleteJob. | |
| 457 | + | // | |
| 458 | + | // The server does not trust the runner to obey. If no conclusion arrives | |
| 459 | + | // within 60 seconds after a cancellation request, whatever the reason (user, | |
| 460 | + | // fail-fast, timeout), the server ends the job itself: CI_CONCLUSION_CANCELLED | |
| 461 | + | // for user and fail-fast, CI_CONCLUSION_TIMED_OUT for timeout, with an error | |
| 462 | + | // saying that the runner did not answer, and the steps not completed are | |
| 463 | + | // completed as cancelled. The runner's later CompleteJob returns that result. | |
| 464 | + | WatchJob(context.Context, *connect.Request[ermite.WatchJobRequest], *connect.ServerStream[ermite.WatchJobResponse]) error | |
| 465 | + | // CompleteJob ends a job: it reports the final steps, the outputs and the | |
| 466 | + | // conclusion. Needs the job token, which stays valid for this RPC for 24 | |
| 467 | + | // hours after the end of the job, however it ended. The call is applied whole | |
| 468 | + | // or not at all. Once the job has ended, the token works for no other RPC | |
| 469 | + | // (they answer failed_precondition with a JobEnded detail). | |
| 470 | + | // | |
| 471 | + | // It is idempotent. The first call that finds the job running ends it and | |
| 472 | + | // answers applied = true. Any later call, a retry after a lost response or a | |
| 473 | + | // call after the server ended the job itself, is not validated and changes | |
| 474 | + | // nothing: it answers OK with the stored conclusion and error and | |
| 475 | + | // applied = false (a runner whose conclusion differs from the stored one | |
| 476 | + | // simply logs it; the outputs it sent are dropped). | |
| 477 | + | // | |
| 478 | + | // Lines sent with AppendLog after this call are rejected: a runner flushes | |
| 479 | + | // all its log lines first. Steps that were not reported COMPLETED when the job | |
| 480 | + | // 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). | |
| 482 | + | // | |
| 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. | |
| 485 | + | CompleteJob(context.Context, *connect.Request[ermite.CompleteJobRequest]) (*connect.Response[ermite.CompleteJobResponse], error) | |
| 486 | + | } | |
| 487 | + | ||
| 488 | + | // NewRunnerServiceHandler builds an HTTP handler from the service implementation. It returns the | |
| 489 | + | // path on which to mount the handler and the handler itself. | |
| 490 | + | // | |
| 491 | + | // By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf | |
| 492 | + | // and JSON codecs. They also support gzip compression. | |
| 493 | + | func NewRunnerServiceHandler(svc RunnerServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { | |
| 494 | + | runnerServiceMethods := ermite.File_ermite_runner_proto.Services().ByName("RunnerService").Methods() | |
| 495 | + | runnerServiceRegisterRunnerHandler := connect.NewUnaryHandler( | |
| 496 | + | RunnerServiceRegisterRunnerProcedure, | |
| 497 | + | svc.RegisterRunner, | |
| 498 | + | connect.WithSchema(runnerServiceMethods.ByName("RegisterRunner")), | |
| 499 | + | connect.WithHandlerOptions(opts...), | |
| 500 | + | ) | |
| 501 | + | runnerServiceFetchJobHandler := connect.NewUnaryHandler( | |
| 502 | + | RunnerServiceFetchJobProcedure, | |
| 503 | + | svc.FetchJob, | |
| 504 | + | connect.WithSchema(runnerServiceMethods.ByName("FetchJob")), | |
| 505 | + | connect.WithHandlerOptions(opts...), | |
| 506 | + | ) | |
| 507 | + | runnerServiceUpdateJobHandler := connect.NewUnaryHandler( | |
| 508 | + | RunnerServiceUpdateJobProcedure, | |
| 509 | + | svc.UpdateJob, | |
| 510 | + | connect.WithSchema(runnerServiceMethods.ByName("UpdateJob")), | |
| 511 | + | connect.WithHandlerOptions(opts...), | |
| 512 | + | ) | |
| 513 | + | runnerServiceAppendLogHandler := connect.NewUnaryHandler( | |
| 514 | + | RunnerServiceAppendLogProcedure, | |
| 515 | + | svc.AppendLog, | |
| 516 | + | connect.WithSchema(runnerServiceMethods.ByName("AppendLog")), | |
| 517 | + | connect.WithHandlerOptions(opts...), | |
| 518 | + | ) | |
| 519 | + | runnerServiceHeartbeatHandler := connect.NewUnaryHandler( | |
| 520 | + | RunnerServiceHeartbeatProcedure, | |
| 521 | + | svc.Heartbeat, | |
| 522 | + | connect.WithSchema(runnerServiceMethods.ByName("Heartbeat")), | |
| 523 | + | connect.WithHandlerOptions(opts...), | |
| 524 | + | ) | |
| 525 | + | runnerServiceWatchJobHandler := connect.NewServerStreamHandler( | |
| 526 | + | RunnerServiceWatchJobProcedure, | |
| 527 | + | svc.WatchJob, | |
| 528 | + | connect.WithSchema(runnerServiceMethods.ByName("WatchJob")), | |
| 529 | + | connect.WithHandlerOptions(opts...), | |
| 530 | + | ) | |
| 531 | + | runnerServiceCompleteJobHandler := connect.NewUnaryHandler( | |
| 532 | + | RunnerServiceCompleteJobProcedure, | |
| 533 | + | svc.CompleteJob, | |
| 534 | + | connect.WithSchema(runnerServiceMethods.ByName("CompleteJob")), | |
| 535 | + | connect.WithHandlerOptions(opts...), | |
| 536 | + | ) | |
| 537 | + | return "/ermite.RunnerService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| 538 | + | switch r.URL.Path { | |
| 539 | + | case RunnerServiceRegisterRunnerProcedure: | |
| 540 | + | runnerServiceRegisterRunnerHandler.ServeHTTP(w, r) | |
| 541 | + | case RunnerServiceFetchJobProcedure: | |
| 542 | + | runnerServiceFetchJobHandler.ServeHTTP(w, r) | |
| 543 | + | case RunnerServiceUpdateJobProcedure: | |
| 544 | + | runnerServiceUpdateJobHandler.ServeHTTP(w, r) | |
| 545 | + | case RunnerServiceAppendLogProcedure: | |
| 546 | + | runnerServiceAppendLogHandler.ServeHTTP(w, r) | |
| 547 | + | case RunnerServiceHeartbeatProcedure: | |
| 548 | + | runnerServiceHeartbeatHandler.ServeHTTP(w, r) | |
| 549 | + | case RunnerServiceWatchJobProcedure: | |
| 550 | + | runnerServiceWatchJobHandler.ServeHTTP(w, r) | |
| 551 | + | case RunnerServiceCompleteJobProcedure: | |
| 552 | + | runnerServiceCompleteJobHandler.ServeHTTP(w, r) | |
| 553 | + | default: | |
| 554 | + | http.NotFound(w, r) | |
| 555 | + | } | |
| 556 | + | }) | |
| 557 | + | } | |
| 558 | + | ||
| 559 | + | // UnimplementedRunnerServiceHandler returns CodeUnimplemented from all methods. | |
| 560 | + | type UnimplementedRunnerServiceHandler struct{} | |
| 561 | + | ||
| 562 | + | func (UnimplementedRunnerServiceHandler) RegisterRunner(context.Context, *connect.Request[ermite.RegisterRunnerRequest]) (*connect.Response[ermite.RegisterRunnerResponse], error) { | |
| 563 | + | return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunnerService.RegisterRunner is not implemented")) | |
| 564 | + | } | |
| 565 | + | ||
| 566 | + | func (UnimplementedRunnerServiceHandler) FetchJob(context.Context, *connect.Request[ermite.FetchJobRequest]) (*connect.Response[ermite.FetchJobResponse], error) { | |
| 567 | + | return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunnerService.FetchJob is not implemented")) | |
| 568 | + | } | |
| 569 | + | ||
| 570 | + | func (UnimplementedRunnerServiceHandler) UpdateJob(context.Context, *connect.Request[ermite.UpdateJobRequest]) (*connect.Response[ermite.UpdateJobResponse], error) { | |
| 571 | + | return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunnerService.UpdateJob is not implemented")) | |
| 572 | + | } | |
| 573 | + | ||
| 574 | + | func (UnimplementedRunnerServiceHandler) AppendLog(context.Context, *connect.Request[ermite.AppendLogRequest]) (*connect.Response[ermite.AppendLogResponse], error) { | |
| 575 | + | return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunnerService.AppendLog is not implemented")) | |
| 576 | + | } | |
| 577 | + | ||
| 578 | + | func (UnimplementedRunnerServiceHandler) Heartbeat(context.Context, *connect.Request[ermite.HeartbeatRequest]) (*connect.Response[ermite.HeartbeatResponse], error) { | |
| 579 | + | return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunnerService.Heartbeat is not implemented")) | |
| 580 | + | } | |
| 581 | + | ||
| 582 | + | func (UnimplementedRunnerServiceHandler) WatchJob(context.Context, *connect.Request[ermite.WatchJobRequest], *connect.ServerStream[ermite.WatchJobResponse]) error { | |
| 583 | + | return connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunnerService.WatchJob is not implemented")) | |
| 584 | + | } | |
| 585 | + | ||
| 586 | + | 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")) | |
| 588 | + | } | |
| @@ -0,0 +1,588 @@ | |||||
| 1 | + | // SPDX-License-Identifier: MIT | |||
| 2 | + | ||||
| 3 | + | // Code generated by protoc-gen-connect-go. DO NOT EDIT. | |||
| 4 | + | // | |||
| 5 | + | // Source: ermite/runner.proto | |||
| 6 | + | ||||
| 7 | + | package ermiteconnect | |||
| 8 | + | ||||
| 9 | + | import ( | |||
| 10 | + | ermite "bnema.dev/ermite-api/gen/ermite" | |||
| 11 | + | connect "connectrpc.com/connect" | |||
| 12 | + | context "context" | |||
| 13 | + | errors "errors" | |||
| 14 | + | http "net/http" | |||
| 15 | + | strings "strings" | |||
| 16 | + | ) | |||
| 17 | + | ||||
| 18 | + | // This is a compile-time assertion to ensure that this generated file and the connect package are | |||
| 19 | + | // compatible. If you get a compiler error that this constant is not defined, this code was | |||
| 20 | + | // generated with a version of connect newer than the one compiled into your binary. You can fix the | |||
| 21 | + | // problem by either regenerating this code with an older version of connect or updating the connect | |||
| 22 | + | // version compiled into your binary. | |||
| 23 | + | const _ = connect.IsAtLeastVersion1_13_0 | |||
| 24 | + | ||||
| 25 | + | const ( | |||
| 26 | + | // RunnerServiceName is the fully-qualified name of the RunnerService service. | |||
| 27 | + | RunnerServiceName = "ermite.RunnerService" | |||
| 28 | + | ) | |||
| 29 | + | ||||
| 30 | + | // These constants are the fully-qualified names of the RPCs defined in this package. They're | |||
| 31 | + | // exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. | |||
| 32 | + | // | |||
| 33 | + | // Note that these are different from the fully-qualified method names used by | |||
| 34 | + | // google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to | |||
| 35 | + | // reflection-formatted method names, remove the leading slash and convert the remaining slash to a | |||
| 36 | + | // period. | |||
| 37 | + | const ( | |||
| 38 | + | // RunnerServiceRegisterRunnerProcedure is the fully-qualified name of the RunnerService's | |||
| 39 | + | // RegisterRunner RPC. | |||
| 40 | + | RunnerServiceRegisterRunnerProcedure = "/ermite.RunnerService/RegisterRunner" | |||
| 41 | + | // RunnerServiceFetchJobProcedure is the fully-qualified name of the RunnerService's FetchJob RPC. | |||
| 42 | + | RunnerServiceFetchJobProcedure = "/ermite.RunnerService/FetchJob" | |||
| 43 | + | // RunnerServiceUpdateJobProcedure is the fully-qualified name of the RunnerService's UpdateJob RPC. | |||
| 44 | + | RunnerServiceUpdateJobProcedure = "/ermite.RunnerService/UpdateJob" | |||
| 45 | + | // RunnerServiceAppendLogProcedure is the fully-qualified name of the RunnerService's AppendLog RPC. | |||
| 46 | + | RunnerServiceAppendLogProcedure = "/ermite.RunnerService/AppendLog" | |||
| 47 | + | // RunnerServiceHeartbeatProcedure is the fully-qualified name of the RunnerService's Heartbeat RPC. | |||
| 48 | + | RunnerServiceHeartbeatProcedure = "/ermite.RunnerService/Heartbeat" | |||
| 49 | + | // RunnerServiceWatchJobProcedure is the fully-qualified name of the RunnerService's WatchJob RPC. | |||
| 50 | + | RunnerServiceWatchJobProcedure = "/ermite.RunnerService/WatchJob" | |||
| 51 | + | // RunnerServiceCompleteJobProcedure is the fully-qualified name of the RunnerService's CompleteJob | |||
| 52 | + | // RPC. | |||
| 53 | + | RunnerServiceCompleteJobProcedure = "/ermite.RunnerService/CompleteJob" | |||
| 54 | + | ) | |||
| 55 | + | ||||
| 56 | + | // RunnerServiceClient is a client for the ermite.RunnerService service. | |||
| 57 | + | type RunnerServiceClient interface { | |||
| 58 | + | // RegisterRunner registers a runner and returns its runner token, which is | |||
| 59 | + | // shown only in this response; the runner keeps it. The caller needs the | |||
| 60 | + | // runner:register scope on a token that is not restricted to repositories | |||
| 61 | + | // (a runner serves every repository), or the root token. Fails with | |||
| 62 | + | // unauthenticated without a valid token, permission_denied without the | |||
| 63 | + | // scope or with a repository-restricted token, invalid_argument if a field | |||
| 64 | + | // breaks a rule, and already_exists if name is taken by another runner and | |||
| 65 | + | // replace is not set. | |||
| 66 | + | RegisterRunner(context.Context, *connect.Request[ermite.RegisterRunnerRequest]) (*connect.Response[ermite.RegisterRunnerResponse], error) | |||
| 67 | + | // FetchJob asks for a job to run (long poll). The server holds the call | |||
| 68 | + | // until a queued job matches the runner, or about 50 seconds (never more | |||
| 69 | + | // than 50) pass, then answers; without a job the response has no job and the | |||
| 70 | + | // runner calls again after retry_after. Clients set a call timeout above 50 | |||
| 71 | + | // seconds (60 is suggested) and must not poll when they have no free | |||
| 72 | + | // capacity. | |||
| 73 | + | // | |||
| 74 | + | // A job matches when every label of its runs-on is among the labels of the | |||
| 75 | + | // runner (case-insensitive). A job is given to exactly one runner, even when | |||
| 76 | + | // many runners and many polls compete; the assignment is recorded before | |||
| 77 | + | // the response is sent. The job stays QUEUED for the owner until it is | |||
| 78 | + | // acknowledged. | |||
| 79 | + | // | |||
| 80 | + | // Acknowledgement. The runner's first Heartbeat for the job is the | |||
| 81 | + | // acknowledgement, and the runner sends it as soon as it has the job, before | |||
| 82 | + | // anything else. If none arrives within 30 seconds of the assignment (the | |||
| 83 | + | // response was lost, or the runner died), the server cancels the assignment: | |||
| 84 | + | // the job token is revoked, the job goes back to QUEUED with a fresh token | |||
| 85 | + | // and can be given to any runner, the same one included; nothing the old | |||
| 86 | + | // assignment stored is kept. This is not a failure of the job. A job whose | |||
| 87 | + | // assignment is cancelled 3 times in a row is failed with CI_CONCLUSION_FAILURE | |||
| 88 | + | // and an error saying so. | |||
| 89 | + | // | |||
| 90 | + | // Retry after a lost response. poll_id lets a client retry a poll safely: | |||
| 91 | + | // a call with the same poll_id as an earlier call of the same runner that | |||
| 92 | + | // assigned a job returns that same assignment (same Job, same token) as long | |||
| 93 | + | // as it has not been acknowledged or cancelled, instead of taking another. | |||
| 94 | + | // | |||
| 95 | + | // A runner that crashed or whose host restarted cannot resume a job: it does | |||
| 96 | + | // not keep the job token, so the job fails with the lost-runner error after | |||
| 97 | + | // the heartbeat timeout (see Heartbeat). | |||
| 98 | + | // | |||
| 99 | + | // Needs the runner token. Fails with unauthenticated when the runner was | |||
| 100 | + | // removed (also ending a call in progress) and invalid_argument if labels | |||
| 101 | + | // or poll_id break a rule. | |||
| 102 | + | FetchJob(context.Context, *connect.Request[ermite.FetchJobRequest]) (*connect.Response[ermite.FetchJobResponse], error) | |||
| 103 | + | // UpdateJob reports the state of the steps of a job while it runs. Needs the | |||
| 104 | + | // job token. Each call is applied whole or not at all, and repeating an | |||
| 105 | + | // identical call changes nothing, so a runner may retry after a network | |||
| 106 | + | // error. To end the job use CompleteJob. | |||
| 107 | + | // | |||
| 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. | |||
| 112 | + | 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. | |||
| 114 | + | // | |||
| 115 | + | // The log of a job is one sequence of lines, numbered from 0 across all | |||
| 116 | + | // steps; offset is the number of lines the server holds when the request is | |||
| 117 | + | // applied, that is the number of the first line of the request. Calls must | |||
| 118 | + | // go in order, one at a time, and are idempotent: | |||
| 119 | + | // | |||
| 120 | + | // - offset equal to the server's line count: the lines are appended; | |||
| 121 | + | // - offset below it: the first (count - offset) lines were already stored | |||
| 122 | + | // (a retry after a lost response) and are skipped, and the rest is | |||
| 123 | + | // appended; a request entirely stored changes nothing. The skipped lines | |||
| 124 | + | // are not compared with the stored ones: a retry whose lines differ from | |||
| 125 | + | // the first attempt is silently dropped, so a client must resend the | |||
| 126 | + | // same lines at the same offset; | |||
| 127 | + | // - offset above it: out_of_range (a gap is a bug of the client; the | |||
| 128 | + | // message gives the line count the server holds). | |||
| 129 | + | // | |||
| 130 | + | // The response gives the line count after the call, so a client can check | |||
| 131 | + | // itself. Offsets resume only after a network error within the same runner | |||
| 132 | + | // process; they are not a way to resume a job after a crash. | |||
| 133 | + | // | |||
| 134 | + | // A request has at most 1,000 lines whose text adds up to at most 64 KiB | |||
| 135 | + | // (65,536 bytes), all in the same step; more is invalid_argument. A line | |||
| 136 | + | // longer than that is cut by the client into pieces, see LogLine.continued. | |||
| 137 | + | // | |||
| 138 | + | // Steps. The step of a request must be at least the step of the last line | |||
| 139 | + | // the server holds, because the log is ordered by step (applied lines only: | |||
| 140 | + | // lines skipped as already stored are not checked); a lower step is | |||
| 141 | + | // failed_precondition. Lines for a step that UpdateJob has not announced | |||
| 142 | + | // create it: it starts IN_PROGRESS at the time of its first line, with an | |||
| 143 | + | // empty name until an UpdateJob gives one. | |||
| 144 | + | // | |||
| 145 | + | // The log of a job is limited to 512 MiB (536,870,912 bytes) as stored by | |||
| 146 | + | // the server (text, timestamps and separators). The call that would exceed | |||
| 147 | + | // it appends nothing; the server adds a last line saying that the limit was | |||
| 148 | + | // reached, ends the job with CI_CONCLUSION_FAILURE and RunJob.error set, and | |||
| 149 | + | // answers failed_precondition with a JobEnded detail like any call after the | |||
| 150 | + | // end of a job. The runner then stops the job. | |||
| 151 | + | // | |||
| 152 | + | // Lines sent after the end of the job are rejected, so a runner flushes all | |||
| 153 | + | // its AppendLog lines before it calls CompleteJob. | |||
| 154 | + | // | |||
| 155 | + | // Fails with invalid_argument if a rule is broken (step number, text with a | |||
| 156 | + | // line feed or invalid UTF-8, missing time, a continued first line). | |||
| 157 | + | AppendLog(context.Context, *connect.Request[ermite.AppendLogRequest]) (*connect.Response[ermite.AppendLogResponse], error) | |||
| 158 | + | // Heartbeat tells the server that the runner is alive and still working on | |||
| 159 | + | // the job. Needs the job token. The first one acknowledges the assignment | |||
| 160 | + | // (see FetchJob). The runner then calls it every 30 seconds for as long as | |||
| 161 | + | // the job runs. The deadline is 2 minutes after the later of the last | |||
| 162 | + | // heartbeat and the start of the server (so a server restart does not fail | |||
| 163 | + | // the jobs of runners that could not reach it): past it the server ends the | |||
| 164 | + | // job with CI_CONCLUSION_FAILURE and the message "The self-hosted runner: | |||
| 165 | + | // <name> lost communication with the server...". Only Heartbeat counts: | |||
| 166 | + | // other calls do not keep a job alive. | |||
| 167 | + | // | |||
| 168 | + | // A runner that gets unavailable (or any network error) retries Heartbeat | |||
| 169 | + | // every 5 seconds until it succeeds, instead of waiting for its next 30 | |||
| 170 | + | // second tick, and keeps the job running meanwhile. | |||
| 171 | + | Heartbeat(context.Context, *connect.Request[ermite.HeartbeatRequest]) (*connect.Response[ermite.HeartbeatResponse], error) | |||
| 172 | + | // WatchJob streams what the runner must react to. Needs the job token. The | |||
| 173 | + | // runner opens it when it starts the job and keeps it open; it reopens it | |||
| 174 | + | // after any error other than unauthenticated and the failed_precondition | |||
| 175 | + | // that says the job ended, and after a clean close of | |||
| 176 | + | // the stream that did not end with a JobEnded message. | |||
| 177 | + | // | |||
| 178 | + | // When the stream opens and a cancellation has already been requested, the | |||
| 179 | + | // first message is that request, so a reconnect never misses one. Only the | |||
| 180 | + | // first reason is sent: a job already cancelled by the owner does not get a | |||
| 181 | + | // second message when its time budget expires. The server also sends a | |||
| 182 | + | // Keepalive at least every 30 seconds, which the client ignores. When the | |||
| 183 | + | // job ends the server sends JobEnded, then closes the stream without error. | |||
| 184 | + | // On a job that has already ended the call fails like the other RPCs | |||
| 185 | + | // (failed_precondition with a JobEnded detail). | |||
| 186 | + | // | |||
| 187 | + | // On a CancelRequest the runner stops the job as the official runner does | |||
| 188 | + | // (interrupt, then terminate, then kill) and reports the conclusion given | |||
| 189 | + | // for the reason with CompleteJob. | |||
| 190 | + | // | |||
| 191 | + | // The server does not trust the runner to obey. If no conclusion arrives | |||
| 192 | + | // within 60 seconds after a cancellation request, whatever the reason (user, | |||
| 193 | + | // fail-fast, timeout), the server ends the job itself: CI_CONCLUSION_CANCELLED | |||
| 194 | + | // for user and fail-fast, CI_CONCLUSION_TIMED_OUT for timeout, with an error | |||
| 195 | + | // saying that the runner did not answer, and the steps not completed are | |||
| 196 | + | // completed as cancelled. The runner's later CompleteJob returns that result. | |||
| 197 | + | WatchJob(context.Context, *connect.Request[ermite.WatchJobRequest]) (*connect.ServerStreamForClient[ermite.WatchJobResponse], error) | |||
| 198 | + | // CompleteJob ends a job: it reports the final steps, the outputs and the | |||
| 199 | + | // conclusion. Needs the job token, which stays valid for this RPC for 24 | |||
| 200 | + | // hours after the end of the job, however it ended. The call is applied whole | |||
| 201 | + | // or not at all. Once the job has ended, the token works for no other RPC | |||
| 202 | + | // (they answer failed_precondition with a JobEnded detail). | |||
| 203 | + | // | |||
| 204 | + | // It is idempotent. The first call that finds the job running ends it and | |||
| 205 | + | // answers applied = true. Any later call, a retry after a lost response or a | |||
| 206 | + | // call after the server ended the job itself, is not validated and changes | |||
| 207 | + | // nothing: it answers OK with the stored conclusion and error and | |||
| 208 | + | // applied = false (a runner whose conclusion differs from the stored one | |||
| 209 | + | // simply logs it; the outputs it sent are dropped). | |||
| 210 | + | // | |||
| 211 | + | // Lines sent with AppendLog after this call are rejected: a runner flushes | |||
| 212 | + | // all its log lines first. Steps that were not reported COMPLETED when the job | |||
| 213 | + | // 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). | |||
| 215 | + | // | |||
| 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. | |||
| 218 | + | CompleteJob(context.Context, *connect.Request[ermite.CompleteJobRequest]) (*connect.Response[ermite.CompleteJobResponse], error) | |||
| 219 | + | } | |||
| 220 | + | ||||
| 221 | + | // NewRunnerServiceClient constructs a client for the ermite.RunnerService service. By default, it | |||
| 222 | + | // uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and sends | |||
| 223 | + | // uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or | |||
| 224 | + | // connect.WithGRPCWeb() options. | |||
| 225 | + | // | |||
| 226 | + | // The URL supplied here should be the base URL for the Connect or gRPC server (for example, | |||
| 227 | + | // http://api.acme.com or https://acme.com/grpc). | |||
| 228 | + | func NewRunnerServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) RunnerServiceClient { | |||
| 229 | + | baseURL = strings.TrimRight(baseURL, "/") | |||
| 230 | + | runnerServiceMethods := ermite.File_ermite_runner_proto.Services().ByName("RunnerService").Methods() | |||
| 231 | + | return &runnerServiceClient{ | |||
| 232 | + | registerRunner: connect.NewClient[ermite.RegisterRunnerRequest, ermite.RegisterRunnerResponse]( | |||
| 233 | + | httpClient, | |||
| 234 | + | baseURL+RunnerServiceRegisterRunnerProcedure, | |||
| 235 | + | connect.WithSchema(runnerServiceMethods.ByName("RegisterRunner")), | |||
| 236 | + | connect.WithClientOptions(opts...), | |||
| 237 | + | ), | |||
| 238 | + | fetchJob: connect.NewClient[ermite.FetchJobRequest, ermite.FetchJobResponse]( | |||
| 239 | + | httpClient, | |||
| 240 | + | baseURL+RunnerServiceFetchJobProcedure, | |||
| 241 | + | connect.WithSchema(runnerServiceMethods.ByName("FetchJob")), | |||
| 242 | + | connect.WithClientOptions(opts...), | |||
| 243 | + | ), | |||
| 244 | + | updateJob: connect.NewClient[ermite.UpdateJobRequest, ermite.UpdateJobResponse]( | |||
| 245 | + | httpClient, | |||
| 246 | + | baseURL+RunnerServiceUpdateJobProcedure, | |||
| 247 | + | connect.WithSchema(runnerServiceMethods.ByName("UpdateJob")), | |||
| 248 | + | connect.WithClientOptions(opts...), | |||
| 249 | + | ), | |||
| 250 | + | appendLog: connect.NewClient[ermite.AppendLogRequest, ermite.AppendLogResponse]( | |||
| 251 | + | httpClient, | |||
| 252 | + | baseURL+RunnerServiceAppendLogProcedure, | |||
| 253 | + | connect.WithSchema(runnerServiceMethods.ByName("AppendLog")), | |||
| 254 | + | connect.WithClientOptions(opts...), | |||
| 255 | + | ), | |||
| 256 | + | heartbeat: connect.NewClient[ermite.HeartbeatRequest, ermite.HeartbeatResponse]( | |||
| 257 | + | httpClient, | |||
| 258 | + | baseURL+RunnerServiceHeartbeatProcedure, | |||
| 259 | + | connect.WithSchema(runnerServiceMethods.ByName("Heartbeat")), | |||
| 260 | + | connect.WithClientOptions(opts...), | |||
| 261 | + | ), | |||
| 262 | + | watchJob: connect.NewClient[ermite.WatchJobRequest, ermite.WatchJobResponse]( | |||
| 263 | + | httpClient, | |||
| 264 | + | baseURL+RunnerServiceWatchJobProcedure, | |||
| 265 | + | connect.WithSchema(runnerServiceMethods.ByName("WatchJob")), | |||
| 266 | + | connect.WithClientOptions(opts...), | |||
| 267 | + | ), | |||
| 268 | + | completeJob: connect.NewClient[ermite.CompleteJobRequest, ermite.CompleteJobResponse]( | |||
| 269 | + | httpClient, | |||
| 270 | + | baseURL+RunnerServiceCompleteJobProcedure, | |||
| 271 | + | connect.WithSchema(runnerServiceMethods.ByName("CompleteJob")), | |||
| 272 | + | connect.WithClientOptions(opts...), | |||
| 273 | + | ), | |||
| 274 | + | } | |||
| 275 | + | } | |||
| 276 | + | ||||
| 277 | + | // runnerServiceClient implements RunnerServiceClient. | |||
| 278 | + | type runnerServiceClient struct { | |||
| 279 | + | registerRunner *connect.Client[ermite.RegisterRunnerRequest, ermite.RegisterRunnerResponse] | |||
| 280 | + | fetchJob *connect.Client[ermite.FetchJobRequest, ermite.FetchJobResponse] | |||
| 281 | + | updateJob *connect.Client[ermite.UpdateJobRequest, ermite.UpdateJobResponse] | |||
| 282 | + | appendLog *connect.Client[ermite.AppendLogRequest, ermite.AppendLogResponse] | |||
| 283 | + | heartbeat *connect.Client[ermite.HeartbeatRequest, ermite.HeartbeatResponse] | |||
| 284 | + | watchJob *connect.Client[ermite.WatchJobRequest, ermite.WatchJobResponse] | |||
| 285 | + | completeJob *connect.Client[ermite.CompleteJobRequest, ermite.CompleteJobResponse] | |||
| 286 | + | } | |||
| 287 | + | ||||
| 288 | + | // RegisterRunner calls ermite.RunnerService.RegisterRunner. | |||
| 289 | + | func (c *runnerServiceClient) RegisterRunner(ctx context.Context, req *connect.Request[ermite.RegisterRunnerRequest]) (*connect.Response[ermite.RegisterRunnerResponse], error) { | |||
| 290 | + | return c.registerRunner.CallUnary(ctx, req) | |||
| 291 | + | } | |||
| 292 | + | ||||
| 293 | + | // FetchJob calls ermite.RunnerService.FetchJob. | |||
| 294 | + | func (c *runnerServiceClient) FetchJob(ctx context.Context, req *connect.Request[ermite.FetchJobRequest]) (*connect.Response[ermite.FetchJobResponse], error) { | |||
| 295 | + | return c.fetchJob.CallUnary(ctx, req) | |||
| 296 | + | } | |||
| 297 | + | ||||
| 298 | + | // UpdateJob calls ermite.RunnerService.UpdateJob. | |||
| 299 | + | func (c *runnerServiceClient) UpdateJob(ctx context.Context, req *connect.Request[ermite.UpdateJobRequest]) (*connect.Response[ermite.UpdateJobResponse], error) { | |||
| 300 | + | return c.updateJob.CallUnary(ctx, req) | |||
| 301 | + | } | |||
| 302 | + | ||||
| 303 | + | // AppendLog calls ermite.RunnerService.AppendLog. | |||
| 304 | + | func (c *runnerServiceClient) AppendLog(ctx context.Context, req *connect.Request[ermite.AppendLogRequest]) (*connect.Response[ermite.AppendLogResponse], error) { | |||
| 305 | + | return c.appendLog.CallUnary(ctx, req) | |||
| 306 | + | } | |||
| 307 | + | ||||
| 308 | + | // Heartbeat calls ermite.RunnerService.Heartbeat. | |||
| 309 | + | func (c *runnerServiceClient) Heartbeat(ctx context.Context, req *connect.Request[ermite.HeartbeatRequest]) (*connect.Response[ermite.HeartbeatResponse], error) { | |||
| 310 | + | return c.heartbeat.CallUnary(ctx, req) | |||
| 311 | + | } | |||
| 312 | + | ||||
| 313 | + | // WatchJob calls ermite.RunnerService.WatchJob. | |||
| 314 | + | func (c *runnerServiceClient) WatchJob(ctx context.Context, req *connect.Request[ermite.WatchJobRequest]) (*connect.ServerStreamForClient[ermite.WatchJobResponse], error) { | |||
| 315 | + | return c.watchJob.CallServerStream(ctx, req) | |||
| 316 | + | } | |||
| 317 | + | ||||
| 318 | + | // CompleteJob calls ermite.RunnerService.CompleteJob. | |||
| 319 | + | func (c *runnerServiceClient) CompleteJob(ctx context.Context, req *connect.Request[ermite.CompleteJobRequest]) (*connect.Response[ermite.CompleteJobResponse], error) { | |||
| 320 | + | return c.completeJob.CallUnary(ctx, req) | |||
| 321 | + | } | |||
| 322 | + | ||||
| 323 | + | // RunnerServiceHandler is an implementation of the ermite.RunnerService service. | |||
| 324 | + | type RunnerServiceHandler interface { | |||
| 325 | + | // RegisterRunner registers a runner and returns its runner token, which is | |||
| 326 | + | // shown only in this response; the runner keeps it. The caller needs the | |||
| 327 | + | // runner:register scope on a token that is not restricted to repositories | |||
| 328 | + | // (a runner serves every repository), or the root token. Fails with | |||
| 329 | + | // unauthenticated without a valid token, permission_denied without the | |||
| 330 | + | // scope or with a repository-restricted token, invalid_argument if a field | |||
| 331 | + | // breaks a rule, and already_exists if name is taken by another runner and | |||
| 332 | + | // replace is not set. | |||
| 333 | + | RegisterRunner(context.Context, *connect.Request[ermite.RegisterRunnerRequest]) (*connect.Response[ermite.RegisterRunnerResponse], error) | |||
| 334 | + | // FetchJob asks for a job to run (long poll). The server holds the call | |||
| 335 | + | // until a queued job matches the runner, or about 50 seconds (never more | |||
| 336 | + | // than 50) pass, then answers; without a job the response has no job and the | |||
| 337 | + | // runner calls again after retry_after. Clients set a call timeout above 50 | |||
| 338 | + | // seconds (60 is suggested) and must not poll when they have no free | |||
| 339 | + | // capacity. | |||
| 340 | + | // | |||
| 341 | + | // A job matches when every label of its runs-on is among the labels of the | |||
| 342 | + | // runner (case-insensitive). A job is given to exactly one runner, even when | |||
| 343 | + | // many runners and many polls compete; the assignment is recorded before | |||
| 344 | + | // the response is sent. The job stays QUEUED for the owner until it is | |||
| 345 | + | // acknowledged. | |||
| 346 | + | // | |||
| 347 | + | // Acknowledgement. The runner's first Heartbeat for the job is the | |||
| 348 | + | // acknowledgement, and the runner sends it as soon as it has the job, before | |||
| 349 | + | // anything else. If none arrives within 30 seconds of the assignment (the | |||
| 350 | + | // response was lost, or the runner died), the server cancels the assignment: | |||
| 351 | + | // the job token is revoked, the job goes back to QUEUED with a fresh token | |||
| 352 | + | // and can be given to any runner, the same one included; nothing the old | |||
| 353 | + | // assignment stored is kept. This is not a failure of the job. A job whose | |||
| 354 | + | // assignment is cancelled 3 times in a row is failed with CI_CONCLUSION_FAILURE | |||
| 355 | + | // and an error saying so. | |||
| 356 | + | // | |||
| 357 | + | // Retry after a lost response. poll_id lets a client retry a poll safely: | |||
| 358 | + | // a call with the same poll_id as an earlier call of the same runner that | |||
| 359 | + | // assigned a job returns that same assignment (same Job, same token) as long | |||
| 360 | + | // as it has not been acknowledged or cancelled, instead of taking another. | |||
| 361 | + | // | |||
| 362 | + | // A runner that crashed or whose host restarted cannot resume a job: it does | |||
| 363 | + | // not keep the job token, so the job fails with the lost-runner error after | |||
| 364 | + | // the heartbeat timeout (see Heartbeat). | |||
| 365 | + | // | |||
| 366 | + | // Needs the runner token. Fails with unauthenticated when the runner was | |||
| 367 | + | // removed (also ending a call in progress) and invalid_argument if labels | |||
| 368 | + | // or poll_id break a rule. | |||
| 369 | + | FetchJob(context.Context, *connect.Request[ermite.FetchJobRequest]) (*connect.Response[ermite.FetchJobResponse], error) | |||
| 370 | + | // UpdateJob reports the state of the steps of a job while it runs. Needs the | |||
| 371 | + | // job token. Each call is applied whole or not at all, and repeating an | |||
| 372 | + | // identical call changes nothing, so a runner may retry after a network | |||
| 373 | + | // error. To end the job use CompleteJob. | |||
| 374 | + | // | |||
| 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. | |||
| 379 | + | 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. | |||
| 381 | + | // | |||
| 382 | + | // The log of a job is one sequence of lines, numbered from 0 across all | |||
| 383 | + | // steps; offset is the number of lines the server holds when the request is | |||
| 384 | + | // applied, that is the number of the first line of the request. Calls must | |||
| 385 | + | // go in order, one at a time, and are idempotent: | |||
| 386 | + | // | |||
| 387 | + | // - offset equal to the server's line count: the lines are appended; | |||
| 388 | + | // - offset below it: the first (count - offset) lines were already stored | |||
| 389 | + | // (a retry after a lost response) and are skipped, and the rest is | |||
| 390 | + | // appended; a request entirely stored changes nothing. The skipped lines | |||
| 391 | + | // are not compared with the stored ones: a retry whose lines differ from | |||
| 392 | + | // the first attempt is silently dropped, so a client must resend the | |||
| 393 | + | // same lines at the same offset; | |||
| 394 | + | // - offset above it: out_of_range (a gap is a bug of the client; the | |||
| 395 | + | // message gives the line count the server holds). | |||
| 396 | + | // | |||
| 397 | + | // The response gives the line count after the call, so a client can check | |||
| 398 | + | // itself. Offsets resume only after a network error within the same runner | |||
| 399 | + | // process; they are not a way to resume a job after a crash. | |||
| 400 | + | // | |||
| 401 | + | // A request has at most 1,000 lines whose text adds up to at most 64 KiB | |||
| 402 | + | // (65,536 bytes), all in the same step; more is invalid_argument. A line | |||
| 403 | + | // longer than that is cut by the client into pieces, see LogLine.continued. | |||
| 404 | + | // | |||
| 405 | + | // Steps. The step of a request must be at least the step of the last line | |||
| 406 | + | // the server holds, because the log is ordered by step (applied lines only: | |||
| 407 | + | // lines skipped as already stored are not checked); a lower step is | |||
| 408 | + | // failed_precondition. Lines for a step that UpdateJob has not announced | |||
| 409 | + | // create it: it starts IN_PROGRESS at the time of its first line, with an | |||
| 410 | + | // empty name until an UpdateJob gives one. | |||
| 411 | + | // | |||
| 412 | + | // The log of a job is limited to 512 MiB (536,870,912 bytes) as stored by | |||
| 413 | + | // the server (text, timestamps and separators). The call that would exceed | |||
| 414 | + | // it appends nothing; the server adds a last line saying that the limit was | |||
| 415 | + | // reached, ends the job with CI_CONCLUSION_FAILURE and RunJob.error set, and | |||
| 416 | + | // answers failed_precondition with a JobEnded detail like any call after the | |||
| 417 | + | // end of a job. The runner then stops the job. | |||
| 418 | + | // | |||
| 419 | + | // Lines sent after the end of the job are rejected, so a runner flushes all | |||
| 420 | + | // its AppendLog lines before it calls CompleteJob. | |||
| 421 | + | // | |||
| 422 | + | // Fails with invalid_argument if a rule is broken (step number, text with a | |||
| 423 | + | // line feed or invalid UTF-8, missing time, a continued first line). | |||
| 424 | + | AppendLog(context.Context, *connect.Request[ermite.AppendLogRequest]) (*connect.Response[ermite.AppendLogResponse], error) | |||
| 425 | + | // Heartbeat tells the server that the runner is alive and still working on | |||
| 426 | + | // the job. Needs the job token. The first one acknowledges the assignment | |||
| 427 | + | // (see FetchJob). The runner then calls it every 30 seconds for as long as | |||
| 428 | + | // the job runs. The deadline is 2 minutes after the later of the last | |||
| 429 | + | // heartbeat and the start of the server (so a server restart does not fail | |||
| 430 | + | // the jobs of runners that could not reach it): past it the server ends the | |||
| 431 | + | // job with CI_CONCLUSION_FAILURE and the message "The self-hosted runner: | |||
| 432 | + | // <name> lost communication with the server...". Only Heartbeat counts: | |||
| 433 | + | // other calls do not keep a job alive. | |||
| 434 | + | // | |||
| 435 | + | // A runner that gets unavailable (or any network error) retries Heartbeat | |||
| 436 | + | // every 5 seconds until it succeeds, instead of waiting for its next 30 | |||
| 437 | + | // second tick, and keeps the job running meanwhile. | |||
| 438 | + | Heartbeat(context.Context, *connect.Request[ermite.HeartbeatRequest]) (*connect.Response[ermite.HeartbeatResponse], error) | |||
| 439 | + | // WatchJob streams what the runner must react to. Needs the job token. The | |||
| 440 | + | // runner opens it when it starts the job and keeps it open; it reopens it | |||
| 441 | + | // after any error other than unauthenticated and the failed_precondition | |||
| 442 | + | // that says the job ended, and after a clean close of | |||
| 443 | + | // the stream that did not end with a JobEnded message. | |||
| 444 | + | // | |||
| 445 | + | // When the stream opens and a cancellation has already been requested, the | |||
| 446 | + | // first message is that request, so a reconnect never misses one. Only the | |||
| 447 | + | // first reason is sent: a job already cancelled by the owner does not get a | |||
| 448 | + | // second message when its time budget expires. The server also sends a | |||
| 449 | + | // Keepalive at least every 30 seconds, which the client ignores. When the | |||
| 450 | + | // job ends the server sends JobEnded, then closes the stream without error. | |||
| 451 | + | // On a job that has already ended the call fails like the other RPCs | |||
| 452 | + | // (failed_precondition with a JobEnded detail). | |||
| 453 | + | // | |||
| 454 | + | // On a CancelRequest the runner stops the job as the official runner does | |||
| 455 | + | // (interrupt, then terminate, then kill) and reports the conclusion given | |||
| 456 | + | // for the reason with CompleteJob. | |||
| 457 | + | // | |||
| 458 | + | // The server does not trust the runner to obey. If no conclusion arrives | |||
| 459 | + | // within 60 seconds after a cancellation request, whatever the reason (user, | |||
| 460 | + | // fail-fast, timeout), the server ends the job itself: CI_CONCLUSION_CANCELLED | |||
| 461 | + | // for user and fail-fast, CI_CONCLUSION_TIMED_OUT for timeout, with an error | |||
| 462 | + | // saying that the runner did not answer, and the steps not completed are | |||
| 463 | + | // completed as cancelled. The runner's later CompleteJob returns that result. | |||
| 464 | + | WatchJob(context.Context, *connect.Request[ermite.WatchJobRequest], *connect.ServerStream[ermite.WatchJobResponse]) error | |||
| 465 | + | // CompleteJob ends a job: it reports the final steps, the outputs and the | |||
| 466 | + | // conclusion. Needs the job token, which stays valid for this RPC for 24 | |||
| 467 | + | // hours after the end of the job, however it ended. The call is applied whole | |||
| 468 | + | // or not at all. Once the job has ended, the token works for no other RPC | |||
| 469 | + | // (they answer failed_precondition with a JobEnded detail). | |||
| 470 | + | // | |||
| 471 | + | // It is idempotent. The first call that finds the job running ends it and | |||
| 472 | + | // answers applied = true. Any later call, a retry after a lost response or a | |||
| 473 | + | // call after the server ended the job itself, is not validated and changes | |||
| 474 | + | // nothing: it answers OK with the stored conclusion and error and | |||
| 475 | + | // applied = false (a runner whose conclusion differs from the stored one | |||
| 476 | + | // simply logs it; the outputs it sent are dropped). | |||
| 477 | + | // | |||
| 478 | + | // Lines sent with AppendLog after this call are rejected: a runner flushes | |||
| 479 | + | // all its log lines first. Steps that were not reported COMPLETED when the job | |||
| 480 | + | // 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). | |||
| 482 | + | // | |||
| 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. | |||
| 485 | + | CompleteJob(context.Context, *connect.Request[ermite.CompleteJobRequest]) (*connect.Response[ermite.CompleteJobResponse], error) | |||
| 486 | + | } | |||
| 487 | + | ||||
| 488 | + | // NewRunnerServiceHandler builds an HTTP handler from the service implementation. It returns the | |||
| 489 | + | // path on which to mount the handler and the handler itself. | |||
| 490 | + | // | |||
| 491 | + | // By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf | |||
| 492 | + | // and JSON codecs. They also support gzip compression. | |||
| 493 | + | func NewRunnerServiceHandler(svc RunnerServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { | |||
| 494 | + | runnerServiceMethods := ermite.File_ermite_runner_proto.Services().ByName("RunnerService").Methods() | |||
| 495 | + | runnerServiceRegisterRunnerHandler := connect.NewUnaryHandler( | |||
| 496 | + | RunnerServiceRegisterRunnerProcedure, | |||
| 497 | + | svc.RegisterRunner, | |||
| 498 | + | connect.WithSchema(runnerServiceMethods.ByName("RegisterRunner")), | |||
| 499 | + | connect.WithHandlerOptions(opts...), | |||
| 500 | + | ) | |||
| 501 | + | runnerServiceFetchJobHandler := connect.NewUnaryHandler( | |||
| 502 | + | RunnerServiceFetchJobProcedure, | |||
| 503 | + | svc.FetchJob, | |||
| 504 | + | connect.WithSchema(runnerServiceMethods.ByName("FetchJob")), | |||
| 505 | + | connect.WithHandlerOptions(opts...), | |||
| 506 | + | ) | |||
| 507 | + | runnerServiceUpdateJobHandler := connect.NewUnaryHandler( | |||
| 508 | + | RunnerServiceUpdateJobProcedure, | |||
| 509 | + | svc.UpdateJob, | |||
| 510 | + | connect.WithSchema(runnerServiceMethods.ByName("UpdateJob")), | |||
| 511 | + | connect.WithHandlerOptions(opts...), | |||
| 512 | + | ) | |||
| 513 | + | runnerServiceAppendLogHandler := connect.NewUnaryHandler( | |||
| 514 | + | RunnerServiceAppendLogProcedure, | |||
| 515 | + | svc.AppendLog, | |||
| 516 | + | connect.WithSchema(runnerServiceMethods.ByName("AppendLog")), | |||
| 517 | + | connect.WithHandlerOptions(opts...), | |||
| 518 | + | ) | |||
| 519 | + | runnerServiceHeartbeatHandler := connect.NewUnaryHandler( | |||
| 520 | + | RunnerServiceHeartbeatProcedure, | |||
| 521 | + | svc.Heartbeat, | |||
| 522 | + | connect.WithSchema(runnerServiceMethods.ByName("Heartbeat")), | |||
| 523 | + | connect.WithHandlerOptions(opts...), | |||
| 524 | + | ) | |||
| 525 | + | runnerServiceWatchJobHandler := connect.NewServerStreamHandler( | |||
| 526 | + | RunnerServiceWatchJobProcedure, | |||
| 527 | + | svc.WatchJob, | |||
| 528 | + | connect.WithSchema(runnerServiceMethods.ByName("WatchJob")), | |||
| 529 | + | connect.WithHandlerOptions(opts...), | |||
| 530 | + | ) | |||
| 531 | + | runnerServiceCompleteJobHandler := connect.NewUnaryHandler( | |||
| 532 | + | RunnerServiceCompleteJobProcedure, | |||
| 533 | + | svc.CompleteJob, | |||
| 534 | + | connect.WithSchema(runnerServiceMethods.ByName("CompleteJob")), | |||
| 535 | + | connect.WithHandlerOptions(opts...), | |||
| 536 | + | ) | |||
| 537 | + | return "/ermite.RunnerService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |||
| 538 | + | switch r.URL.Path { | |||
| 539 | + | case RunnerServiceRegisterRunnerProcedure: | |||
| 540 | + | runnerServiceRegisterRunnerHandler.ServeHTTP(w, r) | |||
| 541 | + | case RunnerServiceFetchJobProcedure: | |||
| 542 | + | runnerServiceFetchJobHandler.ServeHTTP(w, r) | |||
| 543 | + | case RunnerServiceUpdateJobProcedure: | |||
| 544 | + | runnerServiceUpdateJobHandler.ServeHTTP(w, r) | |||
| 545 | + | case RunnerServiceAppendLogProcedure: | |||
| 546 | + | runnerServiceAppendLogHandler.ServeHTTP(w, r) | |||
| 547 | + | case RunnerServiceHeartbeatProcedure: | |||
| 548 | + | runnerServiceHeartbeatHandler.ServeHTTP(w, r) | |||
| 549 | + | case RunnerServiceWatchJobProcedure: | |||
| 550 | + | runnerServiceWatchJobHandler.ServeHTTP(w, r) | |||
| 551 | + | case RunnerServiceCompleteJobProcedure: | |||
| 552 | + | runnerServiceCompleteJobHandler.ServeHTTP(w, r) | |||
| 553 | + | default: | |||
| 554 | + | http.NotFound(w, r) | |||
| 555 | + | } | |||
| 556 | + | }) | |||
| 557 | + | } | |||
| 558 | + | ||||
| 559 | + | // UnimplementedRunnerServiceHandler returns CodeUnimplemented from all methods. | |||
| 560 | + | type UnimplementedRunnerServiceHandler struct{} | |||
| 561 | + | ||||
| 562 | + | func (UnimplementedRunnerServiceHandler) RegisterRunner(context.Context, *connect.Request[ermite.RegisterRunnerRequest]) (*connect.Response[ermite.RegisterRunnerResponse], error) { | |||
| 563 | + | return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunnerService.RegisterRunner is not implemented")) | |||
| 564 | + | } | |||
| 565 | + | ||||
| 566 | + | func (UnimplementedRunnerServiceHandler) FetchJob(context.Context, *connect.Request[ermite.FetchJobRequest]) (*connect.Response[ermite.FetchJobResponse], error) { | |||
| 567 | + | return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunnerService.FetchJob is not implemented")) | |||
| 568 | + | } | |||
| 569 | + | ||||
| 570 | + | func (UnimplementedRunnerServiceHandler) UpdateJob(context.Context, *connect.Request[ermite.UpdateJobRequest]) (*connect.Response[ermite.UpdateJobResponse], error) { | |||
| 571 | + | return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunnerService.UpdateJob is not implemented")) | |||
| 572 | + | } | |||
| 573 | + | ||||
| 574 | + | func (UnimplementedRunnerServiceHandler) AppendLog(context.Context, *connect.Request[ermite.AppendLogRequest]) (*connect.Response[ermite.AppendLogResponse], error) { | |||
| 575 | + | return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunnerService.AppendLog is not implemented")) | |||
| 576 | + | } | |||
| 577 | + | ||||
| 578 | + | func (UnimplementedRunnerServiceHandler) Heartbeat(context.Context, *connect.Request[ermite.HeartbeatRequest]) (*connect.Response[ermite.HeartbeatResponse], error) { | |||
| 579 | + | return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunnerService.Heartbeat is not implemented")) | |||
| 580 | + | } | |||
| 581 | + | ||||
| 582 | + | func (UnimplementedRunnerServiceHandler) WatchJob(context.Context, *connect.Request[ermite.WatchJobRequest], *connect.ServerStream[ermite.WatchJobResponse]) error { | |||
| 583 | + | return connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunnerService.WatchJob is not implemented")) | |||
| 584 | + | } | |||
| 585 | + | ||||
| 586 | + | 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")) | |||
| 588 | + | } | |||
M gen/ermite/event.pb.go +175 -26 generated file: collapsed
A gen/ermite/run.pb.go +2318 -0 generated file: collapsed
A gen/ermite/runner.pb.go +2048 -0 generated file: collapsed
M gen/ermite/server.pb.go +5 -1 generated file: collapsed
M gen_test.go +177 -0
| @@ -3,9 +3,13 @@ | |||
| 3 | 3 | package api_test | |
| 4 | 4 | ||
| 5 | 5 | import ( | |
| 6 | + | "errors" | |
| 6 | 7 | "testing" | |
| 7 | 8 | ||
| 9 | + | "connectrpc.com/connect" | |
| 10 | + | ||
| 8 | 11 | "google.golang.org/protobuf/proto" | |
| 12 | + | "google.golang.org/protobuf/reflect/protoreflect" | |
| 9 | 13 | "google.golang.org/protobuf/types/known/fieldmaskpb" | |
| 10 | 14 | ||
| 11 | 15 | "bnema.dev/ermite-api/gen/ermite" | |
| @@ -43,6 +47,22 @@ func TestProcedures(t *testing.T) { | |||
| 43 | 47 | {ermiteconnect.SessionServiceSignOutEverywhereProcedure, "/ermite.SessionService/SignOutEverywhere"}, | |
| 44 | 48 | {ermiteconnect.ServerServiceGetVersionProcedure, "/ermite.ServerService/GetVersion"}, | |
| 45 | 49 | {ermiteconnect.ServerServiceGetSigningKeyProcedure, "/ermite.ServerService/GetSigningKey"}, | |
| 50 | + | {ermiteconnect.RunnerServiceRegisterRunnerProcedure, "/ermite.RunnerService/RegisterRunner"}, | |
| 51 | + | {ermiteconnect.RunnerServiceFetchJobProcedure, "/ermite.RunnerService/FetchJob"}, | |
| 52 | + | {ermiteconnect.RunnerServiceUpdateJobProcedure, "/ermite.RunnerService/UpdateJob"}, | |
| 53 | + | {ermiteconnect.RunnerServiceAppendLogProcedure, "/ermite.RunnerService/AppendLog"}, | |
| 54 | + | {ermiteconnect.RunnerServiceHeartbeatProcedure, "/ermite.RunnerService/Heartbeat"}, | |
| 55 | + | {ermiteconnect.RunnerServiceWatchJobProcedure, "/ermite.RunnerService/WatchJob"}, | |
| 56 | + | {ermiteconnect.RunnerServiceCompleteJobProcedure, "/ermite.RunnerService/CompleteJob"}, | |
| 57 | + | {ermiteconnect.RunServiceListRunsProcedure, "/ermite.RunService/ListRuns"}, | |
| 58 | + | {ermiteconnect.RunServiceGetRunProcedure, "/ermite.RunService/GetRun"}, | |
| 59 | + | {ermiteconnect.RunServiceGetJobProcedure, "/ermite.RunService/GetJob"}, | |
| 60 | + | {ermiteconnect.RunServiceGetJobLogProcedure, "/ermite.RunService/GetJobLog"}, | |
| 61 | + | {ermiteconnect.RunServiceCancelRunProcedure, "/ermite.RunService/CancelRun"}, | |
| 62 | + | {ermiteconnect.RunServiceRerunRunProcedure, "/ermite.RunService/RerunRun"}, | |
| 63 | + | {ermiteconnect.RunServiceListChecksProcedure, "/ermite.RunService/ListChecks"}, | |
| 64 | + | {ermiteconnect.RunServiceListRunnersProcedure, "/ermite.RunService/ListRunners"}, | |
| 65 | + | {ermiteconnect.RunServiceRemoveRunnerProcedure, "/ermite.RunService/RemoveRunner"}, | |
| 46 | 66 | } | |
| 47 | 67 | for _, tt := range tests { | |
| 48 | 68 | if tt.got != tt.want { | |
| @@ -71,6 +91,41 @@ func TestEnumNumbers(t *testing.T) { | |||
| 71 | 91 | if got := ermite.DiffSide_DIFF_SIDE_NEW; got != 2 { | |
| 72 | 92 | t.Errorf("DIFF_SIDE_NEW = %d, want 2", got) | |
| 73 | 93 | } | |
| 94 | + | enums := []struct { | |
| 95 | + | name string | |
| 96 | + | got int32 | |
| 97 | + | want int32 | |
| 98 | + | }{ | |
| 99 | + | {"CI_STATUS_UNSPECIFIED", int32(ermite.CiStatus_CI_STATUS_UNSPECIFIED), 0}, | |
| 100 | + | {"CI_STATUS_QUEUED", int32(ermite.CiStatus_CI_STATUS_QUEUED), 1}, | |
| 101 | + | {"CI_STATUS_IN_PROGRESS", int32(ermite.CiStatus_CI_STATUS_IN_PROGRESS), 2}, | |
| 102 | + | {"CI_STATUS_COMPLETED", int32(ermite.CiStatus_CI_STATUS_COMPLETED), 3}, | |
| 103 | + | {"CI_CONCLUSION_UNSPECIFIED", int32(ermite.CiConclusion_CI_CONCLUSION_UNSPECIFIED), 0}, | |
| 104 | + | {"CI_CONCLUSION_SUCCESS", int32(ermite.CiConclusion_CI_CONCLUSION_SUCCESS), 1}, | |
| 105 | + | {"CI_CONCLUSION_FAILURE", int32(ermite.CiConclusion_CI_CONCLUSION_FAILURE), 2}, | |
| 106 | + | {"CI_CONCLUSION_CANCELLED", int32(ermite.CiConclusion_CI_CONCLUSION_CANCELLED), 3}, | |
| 107 | + | {"CI_CONCLUSION_SKIPPED", int32(ermite.CiConclusion_CI_CONCLUSION_SKIPPED), 4}, | |
| 108 | + | {"CI_CONCLUSION_TIMED_OUT", int32(ermite.CiConclusion_CI_CONCLUSION_TIMED_OUT), 5}, | |
| 109 | + | {"CI_CONCLUSION_NEUTRAL", int32(ermite.CiConclusion_CI_CONCLUSION_NEUTRAL), 6}, | |
| 110 | + | {"CI_CONCLUSION_ACTION_REQUIRED", int32(ermite.CiConclusion_CI_CONCLUSION_ACTION_REQUIRED), 7}, | |
| 111 | + | {"STEP_STATE_UNSPECIFIED", int32(ermite.StepState_STEP_STATE_UNSPECIFIED), 0}, | |
| 112 | + | {"STEP_STATE_PENDING", int32(ermite.StepState_STEP_STATE_PENDING), 1}, | |
| 113 | + | {"STEP_STATE_IN_PROGRESS", int32(ermite.StepState_STEP_STATE_IN_PROGRESS), 2}, | |
| 114 | + | {"STEP_STATE_COMPLETED", int32(ermite.StepState_STEP_STATE_COMPLETED), 3}, | |
| 115 | + | {"RUNNER_ISOLATION_UNSPECIFIED", int32(ermite.RunnerIsolation_RUNNER_ISOLATION_UNSPECIFIED), 0}, | |
| 116 | + | {"RUNNER_ISOLATION_VM", int32(ermite.RunnerIsolation_RUNNER_ISOLATION_VM), 1}, | |
| 117 | + | {"RUNNER_ISOLATION_CONTAINER_SYSBOX", int32(ermite.RunnerIsolation_RUNNER_ISOLATION_CONTAINER_SYSBOX), 2}, | |
| 118 | + | {"RUNNER_ISOLATION_CONTAINER_PRIVILEGED", int32(ermite.RunnerIsolation_RUNNER_ISOLATION_CONTAINER_PRIVILEGED), 3}, | |
| 119 | + | {"CANCEL_REASON_UNSPECIFIED", int32(ermite.CancelReason_CANCEL_REASON_UNSPECIFIED), 0}, | |
| 120 | + | {"CANCEL_REASON_USER", int32(ermite.CancelReason_CANCEL_REASON_USER), 1}, | |
| 121 | + | {"CANCEL_REASON_FAIL_FAST", int32(ermite.CancelReason_CANCEL_REASON_FAIL_FAST), 2}, | |
| 122 | + | {"CANCEL_REASON_TIMEOUT", int32(ermite.CancelReason_CANCEL_REASON_TIMEOUT), 3}, | |
| 123 | + | } | |
| 124 | + | for _, e := range enums { | |
| 125 | + | if e.got != e.want { | |
| 126 | + | t.Errorf("%s = %d, want %d", e.name, e.got, e.want) | |
| 127 | + | } | |
| 128 | + | } | |
| 74 | 129 | } | |
| 75 | 130 | ||
| 76 | 131 | // TestEventRoundTrip checks that an event with a oneof kind survives the wire. | |
| @@ -152,3 +207,125 @@ func TestMergeRequestEventRoundTrip(t *testing.T) { | |||
| 152 | 207 | t.Errorf("modes = %v", got) | |
| 153 | 208 | } | |
| 154 | 209 | } | |
| 210 | + | ||
| 211 | + | // TestCIEventRoundTrip checks that the CI event kinds keep their field numbers | |
| 212 | + | // and survive the wire. | |
| 213 | + | func TestCIEventRoundTrip(t *testing.T) { | |
| 214 | + | in := &ermite.WatchResponse{Event: &ermite.Event{ | |
| 215 | + | Id: "44", | |
| 216 | + | Kind: &ermite.Event_JobUpdated{JobUpdated: &ermite.JobUpdated{Job: &ermite.RunJob{ | |
| 217 | + | Id: "j1", | |
| 218 | + | RunId: 9, | |
| 219 | + | Attempt: 2, | |
| 220 | + | Status: ermite.CiStatus_CI_STATUS_COMPLETED, | |
| 221 | + | Conclusion: ermite.CiConclusion_CI_CONCLUSION_TIMED_OUT, | |
| 222 | + | Isolation: ermite.RunnerIsolation_RUNNER_ISOLATION_VM, | |
| 223 | + | Steps: []*ermite.Step{{Number: 1, Name: "Set up job", LogStart: 0, LogEnd: 3}}, | |
| 224 | + | }}}, | |
| 225 | + | }} | |
| 226 | + | b, err := proto.Marshal(in) | |
| 227 | + | if err != nil { | |
| 228 | + | t.Fatal(err) | |
| 229 | + | } | |
| 230 | + | var out ermite.WatchResponse | |
| 231 | + | if err := proto.Unmarshal(b, &out); err != nil { | |
| 232 | + | t.Fatal(err) | |
| 233 | + | } | |
| 234 | + | if !proto.Equal(in, &out) { | |
| 235 | + | t.Errorf("round trip mismatch: %v != %v", &out, in) | |
| 236 | + | } | |
| 237 | + | f := (&ermite.Event{}).ProtoReflect().Descriptor().Fields() | |
| 238 | + | for name, num := range map[string]int{"run_updated": 12, "job_updated": 13} { | |
| 239 | + | if fd := f.ByName(protoreflect.Name(name)); fd == nil || int(fd.Number()) != num { | |
| 240 | + | t.Errorf("event field %s: %v, want number %d", name, fd, num) | |
| 241 | + | } | |
| 242 | + | } | |
| 243 | + | } | |
| 244 | + | ||
| 245 | + | // TestJobRoundTrip checks the job handed to a runner survives the wire. | |
| 246 | + | func TestJobRoundTrip(t *testing.T) { | |
| 247 | + | in := &ermite.FetchJobResponse{Job: &ermite.Job{ | |
| 248 | + | JobId: "j1", RunId: 9, Attempt: 1, JobToken: "tok", WorkflowPath: ".github/workflows/ci.yml", | |
| 249 | + | WorkflowYaml: "on: push", JobKey: "build", Matrix: "{}", Contexts: "{}", | |
| 250 | + | Labels: []string{"self-hosted"}, TimeoutMinutes: 360, Repo: "ermite", | |
| 251 | + | Sha: "0123456789012345678901234567890123456789", Ref: "refs/heads/main", | |
| 252 | + | }} | |
| 253 | + | b, err := proto.Marshal(in) | |
| 254 | + | if err != nil { | |
| 255 | + | t.Fatal(err) | |
| 256 | + | } | |
| 257 | + | var out ermite.FetchJobResponse | |
| 258 | + | if err := proto.Unmarshal(b, &out); err != nil { | |
| 259 | + | t.Fatal(err) | |
| 260 | + | } | |
| 261 | + | if !proto.Equal(in, &out) { | |
| 262 | + | t.Errorf("round trip mismatch: %v != %v", &out, in) | |
| 263 | + | } | |
| 264 | + | } | |
| 265 | + | ||
| 266 | + | // TestWatchJobRoundTrip checks the three kinds of WatchJob message and the | |
| 267 | + | // JobEnded detail. | |
| 268 | + | func TestWatchJobRoundTrip(t *testing.T) { | |
| 269 | + | msgs := []*ermite.WatchJobResponse{ | |
| 270 | + | {Msg: &ermite.WatchJobResponse_Keepalive{Keepalive: &ermite.Keepalive{}}}, | |
| 271 | + | {Msg: &ermite.WatchJobResponse_Cancel{Cancel: &ermite.CancelRequest{ | |
| 272 | + | Reason: ermite.CancelReason_CANCEL_REASON_FAIL_FAST, Message: "m", | |
| 273 | + | }}}, | |
| 274 | + | {Msg: &ermite.WatchJobResponse_Ended{Ended: &ermite.JobEnded{ | |
| 275 | + | Conclusion: ermite.CiConclusion_CI_CONCLUSION_CANCELLED, Error: "e", | |
| 276 | + | }}}, | |
| 277 | + | } | |
| 278 | + | for _, in := range msgs { | |
| 279 | + | b, err := proto.Marshal(in) | |
| 280 | + | if err != nil { | |
| 281 | + | t.Fatal(err) | |
| 282 | + | } | |
| 283 | + | var out ermite.WatchJobResponse | |
| 284 | + | if err := proto.Unmarshal(b, &out); err != nil { | |
| 285 | + | t.Fatal(err) | |
| 286 | + | } | |
| 287 | + | if !proto.Equal(in, &out) { | |
| 288 | + | t.Errorf("round trip mismatch: %v != %v", &out, in) | |
| 289 | + | } | |
| 290 | + | } | |
| 291 | + | // The detail travels in a Connect error. | |
| 292 | + | d, err := connect.NewErrorDetail(&ermite.JobEnded{Conclusion: ermite.CiConclusion_CI_CONCLUSION_TIMED_OUT}) | |
| 293 | + | if err != nil { | |
| 294 | + | t.Fatal(err) | |
| 295 | + | } | |
| 296 | + | cerr := connect.NewError(connect.CodeFailedPrecondition, errors.New("job ended")) | |
| 297 | + | cerr.AddDetail(d) | |
| 298 | + | v, err := cerr.Details()[0].Value() | |
| 299 | + | if err != nil { | |
| 300 | + | t.Fatal(err) | |
| 301 | + | } | |
| 302 | + | if got := v.(*ermite.JobEnded).GetConclusion(); got != ermite.CiConclusion_CI_CONCLUSION_TIMED_OUT { | |
| 303 | + | t.Errorf("detail conclusion = %v", got) | |
| 304 | + | } | |
| 305 | + | } | |
| 306 | + | ||
| 307 | + | // TestCompleteJobRoundTrip checks the final report of a job and its answer. | |
| 308 | + | func TestCompleteJobRoundTrip(t *testing.T) { | |
| 309 | + | in := &ermite.CompleteJobRequest{ | |
| 310 | + | JobId: "j1", | |
| 311 | + | Conclusion: ermite.CiConclusion_CI_CONCLUSION_CANCELLED, | |
| 312 | + | Outputs: map[string]string{"a": "b"}, | |
| 313 | + | RunnerShutdown: true, | |
| 314 | + | Steps: []*ermite.StepUpdate{{Number: 2, State: ermite.StepState_STEP_STATE_COMPLETED}}, | |
| 315 | + | } | |
| 316 | + | b, err := proto.Marshal(in) | |
| 317 | + | if err != nil { | |
| 318 | + | t.Fatal(err) | |
| 319 | + | } | |
| 320 | + | var out ermite.CompleteJobRequest | |
| 321 | + | if err := proto.Unmarshal(b, &out); err != nil { | |
| 322 | + | t.Fatal(err) | |
| 323 | + | } | |
| 324 | + | if !proto.Equal(in, &out) { | |
| 325 | + | t.Errorf("round trip mismatch: %v != %v", &out, in) | |
| 326 | + | } | |
| 327 | + | r := &ermite.CompleteJobResponse{Applied: false, Conclusion: ermite.CiConclusion_CI_CONCLUSION_TIMED_OUT} | |
| 328 | + | if r.GetApplied() || r.GetConclusion() != ermite.CiConclusion_CI_CONCLUSION_TIMED_OUT { | |
| 329 | + | t.Errorf("unexpected response %v", r) | |
| 330 | + | } | |
| 331 | + | } | |
| @@ -3,9 +3,13 @@ | |||||
| 3 | package api_test | 3 | package api_test | ||
| 4 | 4 | ||||
| 5 | import ( | 5 | import ( | ||
| 6 | + | "errors" | |||
| 6 | "testing" | 7 | "testing" | ||
| 7 | 8 | ||||
| 9 | + | "connectrpc.com/connect" | |||
| 10 | + | ||||
| 8 | "google.golang.org/protobuf/proto" | 11 | "google.golang.org/protobuf/proto" | ||
| 12 | + | "google.golang.org/protobuf/reflect/protoreflect" | |||
| 9 | "google.golang.org/protobuf/types/known/fieldmaskpb" | 13 | "google.golang.org/protobuf/types/known/fieldmaskpb" | ||
| 10 | 14 | ||||
| 11 | "bnema.dev/ermite-api/gen/ermite" | 15 | "bnema.dev/ermite-api/gen/ermite" | ||
| @@ -43,6 +47,22 @@ func TestProcedures(t *testing.T) { | |||||
| 43 | {ermiteconnect.SessionServiceSignOutEverywhereProcedure, "/ermite.SessionService/SignOutEverywhere"}, | 47 | {ermiteconnect.SessionServiceSignOutEverywhereProcedure, "/ermite.SessionService/SignOutEverywhere"}, | ||
| 44 | {ermiteconnect.ServerServiceGetVersionProcedure, "/ermite.ServerService/GetVersion"}, | 48 | {ermiteconnect.ServerServiceGetVersionProcedure, "/ermite.ServerService/GetVersion"}, | ||
| 45 | {ermiteconnect.ServerServiceGetSigningKeyProcedure, "/ermite.ServerService/GetSigningKey"}, | 49 | {ermiteconnect.ServerServiceGetSigningKeyProcedure, "/ermite.ServerService/GetSigningKey"}, | ||
| 50 | + | {ermiteconnect.RunnerServiceRegisterRunnerProcedure, "/ermite.RunnerService/RegisterRunner"}, | |||
| 51 | + | {ermiteconnect.RunnerServiceFetchJobProcedure, "/ermite.RunnerService/FetchJob"}, | |||
| 52 | + | {ermiteconnect.RunnerServiceUpdateJobProcedure, "/ermite.RunnerService/UpdateJob"}, | |||
| 53 | + | {ermiteconnect.RunnerServiceAppendLogProcedure, "/ermite.RunnerService/AppendLog"}, | |||
| 54 | + | {ermiteconnect.RunnerServiceHeartbeatProcedure, "/ermite.RunnerService/Heartbeat"}, | |||
| 55 | + | {ermiteconnect.RunnerServiceWatchJobProcedure, "/ermite.RunnerService/WatchJob"}, | |||
| 56 | + | {ermiteconnect.RunnerServiceCompleteJobProcedure, "/ermite.RunnerService/CompleteJob"}, | |||
| 57 | + | {ermiteconnect.RunServiceListRunsProcedure, "/ermite.RunService/ListRuns"}, | |||
| 58 | + | {ermiteconnect.RunServiceGetRunProcedure, "/ermite.RunService/GetRun"}, | |||
| 59 | + | {ermiteconnect.RunServiceGetJobProcedure, "/ermite.RunService/GetJob"}, | |||
| 60 | + | {ermiteconnect.RunServiceGetJobLogProcedure, "/ermite.RunService/GetJobLog"}, | |||
| 61 | + | {ermiteconnect.RunServiceCancelRunProcedure, "/ermite.RunService/CancelRun"}, | |||
| 62 | + | {ermiteconnect.RunServiceRerunRunProcedure, "/ermite.RunService/RerunRun"}, | |||
| 63 | + | {ermiteconnect.RunServiceListChecksProcedure, "/ermite.RunService/ListChecks"}, | |||
| 64 | + | {ermiteconnect.RunServiceListRunnersProcedure, "/ermite.RunService/ListRunners"}, | |||
| 65 | + | {ermiteconnect.RunServiceRemoveRunnerProcedure, "/ermite.RunService/RemoveRunner"}, | |||
| 46 | } | 66 | } | ||
| 47 | for _, tt := range tests { | 67 | for _, tt := range tests { | ||
| 48 | if tt.got != tt.want { | 68 | if tt.got != tt.want { | ||
| @@ -71,6 +91,41 @@ func TestEnumNumbers(t *testing.T) { | |||||
| 71 | if got := ermite.DiffSide_DIFF_SIDE_NEW; got != 2 { | 91 | if got := ermite.DiffSide_DIFF_SIDE_NEW; got != 2 { | ||
| 72 | t.Errorf("DIFF_SIDE_NEW = %d, want 2", got) | 92 | t.Errorf("DIFF_SIDE_NEW = %d, want 2", got) | ||
| 73 | } | 93 | } | ||
| 94 | + | enums := []struct { | |||
| 95 | + | name string | |||
| 96 | + | got int32 | |||
| 97 | + | want int32 | |||
| 98 | + | }{ | |||
| 99 | + | {"CI_STATUS_UNSPECIFIED", int32(ermite.CiStatus_CI_STATUS_UNSPECIFIED), 0}, | |||
| 100 | + | {"CI_STATUS_QUEUED", int32(ermite.CiStatus_CI_STATUS_QUEUED), 1}, | |||
| 101 | + | {"CI_STATUS_IN_PROGRESS", int32(ermite.CiStatus_CI_STATUS_IN_PROGRESS), 2}, | |||
| 102 | + | {"CI_STATUS_COMPLETED", int32(ermite.CiStatus_CI_STATUS_COMPLETED), 3}, | |||
| 103 | + | {"CI_CONCLUSION_UNSPECIFIED", int32(ermite.CiConclusion_CI_CONCLUSION_UNSPECIFIED), 0}, | |||
| 104 | + | {"CI_CONCLUSION_SUCCESS", int32(ermite.CiConclusion_CI_CONCLUSION_SUCCESS), 1}, | |||
| 105 | + | {"CI_CONCLUSION_FAILURE", int32(ermite.CiConclusion_CI_CONCLUSION_FAILURE), 2}, | |||
| 106 | + | {"CI_CONCLUSION_CANCELLED", int32(ermite.CiConclusion_CI_CONCLUSION_CANCELLED), 3}, | |||
| 107 | + | {"CI_CONCLUSION_SKIPPED", int32(ermite.CiConclusion_CI_CONCLUSION_SKIPPED), 4}, | |||
| 108 | + | {"CI_CONCLUSION_TIMED_OUT", int32(ermite.CiConclusion_CI_CONCLUSION_TIMED_OUT), 5}, | |||
| 109 | + | {"CI_CONCLUSION_NEUTRAL", int32(ermite.CiConclusion_CI_CONCLUSION_NEUTRAL), 6}, | |||
| 110 | + | {"CI_CONCLUSION_ACTION_REQUIRED", int32(ermite.CiConclusion_CI_CONCLUSION_ACTION_REQUIRED), 7}, | |||
| 111 | + | {"STEP_STATE_UNSPECIFIED", int32(ermite.StepState_STEP_STATE_UNSPECIFIED), 0}, | |||
| 112 | + | {"STEP_STATE_PENDING", int32(ermite.StepState_STEP_STATE_PENDING), 1}, | |||
| 113 | + | {"STEP_STATE_IN_PROGRESS", int32(ermite.StepState_STEP_STATE_IN_PROGRESS), 2}, | |||
| 114 | + | {"STEP_STATE_COMPLETED", int32(ermite.StepState_STEP_STATE_COMPLETED), 3}, | |||
| 115 | + | {"RUNNER_ISOLATION_UNSPECIFIED", int32(ermite.RunnerIsolation_RUNNER_ISOLATION_UNSPECIFIED), 0}, | |||
| 116 | + | {"RUNNER_ISOLATION_VM", int32(ermite.RunnerIsolation_RUNNER_ISOLATION_VM), 1}, | |||
| 117 | + | {"RUNNER_ISOLATION_CONTAINER_SYSBOX", int32(ermite.RunnerIsolation_RUNNER_ISOLATION_CONTAINER_SYSBOX), 2}, | |||
| 118 | + | {"RUNNER_ISOLATION_CONTAINER_PRIVILEGED", int32(ermite.RunnerIsolation_RUNNER_ISOLATION_CONTAINER_PRIVILEGED), 3}, | |||
| 119 | + | {"CANCEL_REASON_UNSPECIFIED", int32(ermite.CancelReason_CANCEL_REASON_UNSPECIFIED), 0}, | |||
| 120 | + | {"CANCEL_REASON_USER", int32(ermite.CancelReason_CANCEL_REASON_USER), 1}, | |||
| 121 | + | {"CANCEL_REASON_FAIL_FAST", int32(ermite.CancelReason_CANCEL_REASON_FAIL_FAST), 2}, | |||
| 122 | + | {"CANCEL_REASON_TIMEOUT", int32(ermite.CancelReason_CANCEL_REASON_TIMEOUT), 3}, | |||
| 123 | + | } | |||
| 124 | + | for _, e := range enums { | |||
| 125 | + | if e.got != e.want { | |||
| 126 | + | t.Errorf("%s = %d, want %d", e.name, e.got, e.want) | |||
| 127 | + | } | |||
| 128 | + | } | |||
| 74 | } | 129 | } | ||
| 75 | 130 | ||||
| 76 | // TestEventRoundTrip checks that an event with a oneof kind survives the wire. | 131 | // TestEventRoundTrip checks that an event with a oneof kind survives the wire. | ||
| @@ -152,3 +207,125 @@ func TestMergeRequestEventRoundTrip(t *testing.T) { | |||||
| 152 | t.Errorf("modes = %v", got) | 207 | t.Errorf("modes = %v", got) | ||
| 153 | } | 208 | } | ||
| 154 | } | 209 | } | ||
| 210 | + | ||||
| 211 | + | // TestCIEventRoundTrip checks that the CI event kinds keep their field numbers | |||
| 212 | + | // and survive the wire. | |||
| 213 | + | func TestCIEventRoundTrip(t *testing.T) { | |||
| 214 | + | in := &ermite.WatchResponse{Event: &ermite.Event{ | |||
| 215 | + | Id: "44", | |||
| 216 | + | Kind: &ermite.Event_JobUpdated{JobUpdated: &ermite.JobUpdated{Job: &ermite.RunJob{ | |||
| 217 | + | Id: "j1", | |||
| 218 | + | RunId: 9, | |||
| 219 | + | Attempt: 2, | |||
| 220 | + | Status: ermite.CiStatus_CI_STATUS_COMPLETED, | |||
| 221 | + | Conclusion: ermite.CiConclusion_CI_CONCLUSION_TIMED_OUT, | |||
| 222 | + | Isolation: ermite.RunnerIsolation_RUNNER_ISOLATION_VM, | |||
| 223 | + | Steps: []*ermite.Step{{Number: 1, Name: "Set up job", LogStart: 0, LogEnd: 3}}, | |||
| 224 | + | }}}, | |||
| 225 | + | }} | |||
| 226 | + | b, err := proto.Marshal(in) | |||
| 227 | + | if err != nil { | |||
| 228 | + | t.Fatal(err) | |||
| 229 | + | } | |||
| 230 | + | var out ermite.WatchResponse | |||
| 231 | + | if err := proto.Unmarshal(b, &out); err != nil { | |||
| 232 | + | t.Fatal(err) | |||
| 233 | + | } | |||
| 234 | + | if !proto.Equal(in, &out) { | |||
| 235 | + | t.Errorf("round trip mismatch: %v != %v", &out, in) | |||
| 236 | + | } | |||
| 237 | + | f := (&ermite.Event{}).ProtoReflect().Descriptor().Fields() | |||
| 238 | + | for name, num := range map[string]int{"run_updated": 12, "job_updated": 13} { | |||
| 239 | + | if fd := f.ByName(protoreflect.Name(name)); fd == nil || int(fd.Number()) != num { | |||
| 240 | + | t.Errorf("event field %s: %v, want number %d", name, fd, num) | |||
| 241 | + | } | |||
| 242 | + | } | |||
| 243 | + | } | |||
| 244 | + | ||||
| 245 | + | // TestJobRoundTrip checks the job handed to a runner survives the wire. | |||
| 246 | + | func TestJobRoundTrip(t *testing.T) { | |||
| 247 | + | in := &ermite.FetchJobResponse{Job: &ermite.Job{ | |||
| 248 | + | JobId: "j1", RunId: 9, Attempt: 1, JobToken: "tok", WorkflowPath: ".github/workflows/ci.yml", | |||
| 249 | + | WorkflowYaml: "on: push", JobKey: "build", Matrix: "{}", Contexts: "{}", | |||
| 250 | + | Labels: []string{"self-hosted"}, TimeoutMinutes: 360, Repo: "ermite", | |||
| 251 | + | Sha: "0123456789012345678901234567890123456789", Ref: "refs/heads/main", | |||
| 252 | + | }} | |||
| 253 | + | b, err := proto.Marshal(in) | |||
| 254 | + | if err != nil { | |||
| 255 | + | t.Fatal(err) | |||
| 256 | + | } | |||
| 257 | + | var out ermite.FetchJobResponse | |||
| 258 | + | if err := proto.Unmarshal(b, &out); err != nil { | |||
| 259 | + | t.Fatal(err) | |||
| 260 | + | } | |||
| 261 | + | if !proto.Equal(in, &out) { | |||
| 262 | + | t.Errorf("round trip mismatch: %v != %v", &out, in) | |||
| 263 | + | } | |||
| 264 | + | } | |||
| 265 | + | ||||
| 266 | + | // TestWatchJobRoundTrip checks the three kinds of WatchJob message and the | |||
| 267 | + | // JobEnded detail. | |||
| 268 | + | func TestWatchJobRoundTrip(t *testing.T) { | |||
| 269 | + | msgs := []*ermite.WatchJobResponse{ | |||
| 270 | + | {Msg: &ermite.WatchJobResponse_Keepalive{Keepalive: &ermite.Keepalive{}}}, | |||
| 271 | + | {Msg: &ermite.WatchJobResponse_Cancel{Cancel: &ermite.CancelRequest{ | |||
| 272 | + | Reason: ermite.CancelReason_CANCEL_REASON_FAIL_FAST, Message: "m", | |||
| 273 | + | }}}, | |||
| 274 | + | {Msg: &ermite.WatchJobResponse_Ended{Ended: &ermite.JobEnded{ | |||
| 275 | + | Conclusion: ermite.CiConclusion_CI_CONCLUSION_CANCELLED, Error: "e", | |||
| 276 | + | }}}, | |||
| 277 | + | } | |||
| 278 | + | for _, in := range msgs { | |||
| 279 | + | b, err := proto.Marshal(in) | |||
| 280 | + | if err != nil { | |||
| 281 | + | t.Fatal(err) | |||
| 282 | + | } | |||
| 283 | + | var out ermite.WatchJobResponse | |||
| 284 | + | if err := proto.Unmarshal(b, &out); err != nil { | |||
| 285 | + | t.Fatal(err) | |||
| 286 | + | } | |||
| 287 | + | if !proto.Equal(in, &out) { | |||
| 288 | + | t.Errorf("round trip mismatch: %v != %v", &out, in) | |||
| 289 | + | } | |||
| 290 | + | } | |||
| 291 | + | // The detail travels in a Connect error. | |||
| 292 | + | d, err := connect.NewErrorDetail(&ermite.JobEnded{Conclusion: ermite.CiConclusion_CI_CONCLUSION_TIMED_OUT}) | |||
| 293 | + | if err != nil { | |||
| 294 | + | t.Fatal(err) | |||
| 295 | + | } | |||
| 296 | + | cerr := connect.NewError(connect.CodeFailedPrecondition, errors.New("job ended")) | |||
| 297 | + | cerr.AddDetail(d) | |||
| 298 | + | v, err := cerr.Details()[0].Value() | |||
| 299 | + | if err != nil { | |||
| 300 | + | t.Fatal(err) | |||
| 301 | + | } | |||
| 302 | + | if got := v.(*ermite.JobEnded).GetConclusion(); got != ermite.CiConclusion_CI_CONCLUSION_TIMED_OUT { | |||
| 303 | + | t.Errorf("detail conclusion = %v", got) | |||
| 304 | + | } | |||
| 305 | + | } | |||
| 306 | + | ||||
| 307 | + | // TestCompleteJobRoundTrip checks the final report of a job and its answer. | |||
| 308 | + | func TestCompleteJobRoundTrip(t *testing.T) { | |||
| 309 | + | in := &ermite.CompleteJobRequest{ | |||
| 310 | + | JobId: "j1", | |||
| 311 | + | Conclusion: ermite.CiConclusion_CI_CONCLUSION_CANCELLED, | |||
| 312 | + | Outputs: map[string]string{"a": "b"}, | |||
| 313 | + | RunnerShutdown: true, | |||
| 314 | + | Steps: []*ermite.StepUpdate{{Number: 2, State: ermite.StepState_STEP_STATE_COMPLETED}}, | |||
| 315 | + | } | |||
| 316 | + | b, err := proto.Marshal(in) | |||
| 317 | + | if err != nil { | |||
| 318 | + | t.Fatal(err) | |||
| 319 | + | } | |||
| 320 | + | var out ermite.CompleteJobRequest | |||
| 321 | + | if err := proto.Unmarshal(b, &out); err != nil { | |||
| 322 | + | t.Fatal(err) | |||
| 323 | + | } | |||
| 324 | + | if !proto.Equal(in, &out) { | |||
| 325 | + | t.Errorf("round trip mismatch: %v != %v", &out, in) | |||
| 326 | + | } | |||
| 327 | + | r := &ermite.CompleteJobResponse{Applied: false, Conclusion: ermite.CiConclusion_CI_CONCLUSION_TIMED_OUT} | |||
| 328 | + | if r.GetApplied() || r.GetConclusion() != ermite.CiConclusion_CI_CONCLUSION_TIMED_OUT { | |||
| 329 | + | t.Errorf("unexpected response %v", r) | |||
| 330 | + | } | |||
| 331 | + | } | |||
M proto/ermite/event.proto +31 -2
| @@ -6,6 +6,7 @@ package ermite; | |||
| 6 | 6 | ||
| 7 | 7 | import "ermite/merge_request.proto"; | |
| 8 | 8 | import "ermite/repo.proto"; | |
| 9 | + | import "ermite/run.proto"; | |
| 9 | 10 | import "google/protobuf/timestamp.proto"; | |
| 10 | 11 | ||
| 11 | 12 | // EventService streams what happens on the forge. | |
| @@ -18,7 +19,17 @@ service EventService { | |||
| 18 | 19 | // for merge requests, merge_request_opened, merge_request_merged, | |
| 19 | 20 | // merge_request_closed and comment_added. Each one is delivered on the | |
| 20 | 21 | // "repo:<name>" topic of its repository and on "home", subject to the | |
| 21 | - | // visibility of the repository. There is no event for a change of title or | |
| 22 | + | // visibility of the repository. | |
| 23 | + | // | |
| 24 | + | // Servers with the feature "ci.runs" also publish run_updated and | |
| 25 | + | // job_updated, only on the "repo:<name>" topic of their repository (never on | |
| 26 | + | // "home"), subject to the same visibility. They carry the state after the | |
| 27 | + | // change and are published when a run or a job is created, changes status | |
| 28 | + | // or conclusion, or when one of its steps starts or ends (never per log | |
| 29 | + | // line: follow a log with RunService.GetJobLog). The order of these events | |
| 30 | + | // is not guaranteed, between jobs and runs or between two updates of the same | |
| 31 | + | // one: a client keeps the copy with the higher Run.version or | |
| 32 | + | // RunJob.version and drops the other. There is no event for a change of title or | |
| 22 | 33 | // description, nor for a resolved comment: after a gap, or to see such | |
| 23 | 34 | // changes, clients read the merge request again. A merge publishes | |
| 24 | 35 | // merge_request_merged, then pushed for the target branch. | |
| @@ -87,6 +98,10 @@ message Event { | |||
| 87 | 98 | MergeRequestClosed merge_request_closed = 10; | |
| 88 | 99 | // CommentAdded: a comment was added to a merge request. | |
| 89 | 100 | CommentAdded comment_added = 11; | |
| 101 | + | // RunUpdated: a CI run was created or changed. | |
| 102 | + | RunUpdated run_updated = 12; | |
| 103 | + | // JobUpdated: a CI job was created or changed. | |
| 104 | + | JobUpdated job_updated = 13; | |
| 90 | 105 | } | |
| 91 | 106 | } | |
| 92 | 107 | ||
| @@ -158,5 +173,19 @@ message CommentAdded { | |||
| 158 | 173 | // Gap tells the client that events were lost: the requested resume point is | |
| 159 | 174 | // older than the server's buffer. It has no fields. The client should reload | |
| 160 | 175 | // the state it displays (repositories, refs, merge requests and their | |
| 161 | - | // comments) and carry on with the next events. | |
| 176 | + | // comments, CI runs and their jobs) and carry on with the next events. | |
| 162 | 177 | message Gap {} | |
| 178 | + | ||
| 179 | + | // RunUpdated reports a CI run that was created or whose status, conclusion or | |
| 180 | + | // timestamps changed. It is delivered on the "repo:<name>" topic only. | |
| 181 | + | message RunUpdated { | |
| 182 | + | // Run is the run after the change, as of its latest attempt, without jobs. | |
| 183 | + | Run run = 1; | |
| 184 | + | } | |
| 185 | + | ||
| 186 | + | // JobUpdated reports a CI job that was created or whose status, conclusion, | |
| 187 | + | // runner or steps changed. It is delivered on the "repo:<name>" topic only. | |
| 188 | + | message JobUpdated { | |
| 189 | + | // Job is the job after the change, with its steps. | |
| 190 | + | RunJob job = 1; | |
| 191 | + | } | |
| @@ -6,6 +6,7 @@ package ermite; | |||||
| 6 | 6 | ||||
| 7 | import "ermite/merge_request.proto"; | 7 | import "ermite/merge_request.proto"; | ||
| 8 | import "ermite/repo.proto"; | 8 | import "ermite/repo.proto"; | ||
| 9 | + | import "ermite/run.proto"; | |||
| 9 | import "google/protobuf/timestamp.proto"; | 10 | import "google/protobuf/timestamp.proto"; | ||
| 10 | 11 | ||||
| 11 | // EventService streams what happens on the forge. | 12 | // EventService streams what happens on the forge. | ||
| @@ -18,7 +19,17 @@ service EventService { | |||||
| 18 | // for merge requests, merge_request_opened, merge_request_merged, | 19 | // for merge requests, merge_request_opened, merge_request_merged, | ||
| 19 | // merge_request_closed and comment_added. Each one is delivered on the | 20 | // merge_request_closed and comment_added. Each one is delivered on the | ||
| 20 | // "repo:<name>" topic of its repository and on "home", subject to the | 21 | // "repo:<name>" topic of its repository and on "home", subject to the | ||
| 21 | - | // visibility of the repository. There is no event for a change of title or | 22 | + | // visibility of the repository. |
| 23 | + | // | |||
| 24 | + | // Servers with the feature "ci.runs" also publish run_updated and | |||
| 25 | + | // job_updated, only on the "repo:<name>" topic of their repository (never on | |||
| 26 | + | // "home"), subject to the same visibility. They carry the state after the | |||
| 27 | + | // change and are published when a run or a job is created, changes status | |||
| 28 | + | // or conclusion, or when one of its steps starts or ends (never per log | |||
| 29 | + | // line: follow a log with RunService.GetJobLog). The order of these events | |||
| 30 | + | // is not guaranteed, between jobs and runs or between two updates of the same | |||
| 31 | + | // one: a client keeps the copy with the higher Run.version or | |||
| 32 | + | // RunJob.version and drops the other. There is no event for a change of title or | |||
| 22 | // description, nor for a resolved comment: after a gap, or to see such | 33 | // description, nor for a resolved comment: after a gap, or to see such | ||
| 23 | // changes, clients read the merge request again. A merge publishes | 34 | // changes, clients read the merge request again. A merge publishes | ||
| 24 | // merge_request_merged, then pushed for the target branch. | 35 | // merge_request_merged, then pushed for the target branch. | ||
| @@ -87,6 +98,10 @@ message Event { | |||||
| 87 | MergeRequestClosed merge_request_closed = 10; | 98 | MergeRequestClosed merge_request_closed = 10; | ||
| 88 | // CommentAdded: a comment was added to a merge request. | 99 | // CommentAdded: a comment was added to a merge request. | ||
| 89 | CommentAdded comment_added = 11; | 100 | CommentAdded comment_added = 11; | ||
| 101 | + | // RunUpdated: a CI run was created or changed. | |||
| 102 | + | RunUpdated run_updated = 12; | |||
| 103 | + | // JobUpdated: a CI job was created or changed. | |||
| 104 | + | JobUpdated job_updated = 13; | |||
| 90 | } | 105 | } | ||
| 91 | } | 106 | } | ||
| 92 | 107 | ||||
| @@ -158,5 +173,19 @@ message CommentAdded { | |||||
| 158 | // Gap tells the client that events were lost: the requested resume point is | 173 | // Gap tells the client that events were lost: the requested resume point is | ||
| 159 | // older than the server's buffer. It has no fields. The client should reload | 174 | // older than the server's buffer. It has no fields. The client should reload | ||
| 160 | // the state it displays (repositories, refs, merge requests and their | 175 | // the state it displays (repositories, refs, merge requests and their | ||
| 161 | - | // comments) and carry on with the next events. | 176 | + | // comments, CI runs and their jobs) and carry on with the next events. |
| 162 | message Gap {} | 177 | message Gap {} | ||
| 178 | + | ||||
| 179 | + | // RunUpdated reports a CI run that was created or whose status, conclusion or | |||
| 180 | + | // timestamps changed. It is delivered on the "repo:<name>" topic only. | |||
| 181 | + | message RunUpdated { | |||
| 182 | + | // Run is the run after the change, as of its latest attempt, without jobs. | |||
| 183 | + | Run run = 1; | |||
| 184 | + | } | |||
| 185 | + | ||||
| 186 | + | // JobUpdated reports a CI job that was created or whose status, conclusion, | |||
| 187 | + | // runner or steps changed. It is delivered on the "repo:<name>" topic only. | |||
| 188 | + | message JobUpdated { | |||
| 189 | + | // Job is the job after the change, with its steps. | |||
| 190 | + | RunJob job = 1; | |||
| 191 | + | } | |||
A proto/ermite/run.proto +503 -0
| @@ -0,0 +1,503 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | syntax = "proto3"; | |
| 4 | + | ||
| 5 | + | package ermite; | |
| 6 | + | ||
| 7 | + | import "ermite/runner.proto"; | |
| 8 | + | import "google/protobuf/timestamp.proto"; | |
| 9 | + | ||
| 10 | + | // Run is one execution of one workflow file for one event, like a GitHub | |
| 11 | + | // workflow run. A re-run keeps the run and adds an attempt. | |
| 12 | + | message Run { | |
| 13 | + | // Repo is the name of the repository. | |
| 14 | + | string repo = 1; | |
| 15 | + | // Id identifies the run on the whole server: it is the "github.run_id". | |
| 16 | + | // Increasing, never reused. | |
| 17 | + | int64 id = 2; | |
| 18 | + | // Number counts the runs of the same workflow file in the repository, | |
| 19 | + | // starting at 1, never reused: it is the "github.run_number". It does not | |
| 20 | + | // change on a re-run. | |
| 21 | + | int64 number = 3; | |
| 22 | + | // Attempt is the attempt this message describes: 1 for the first, one more | |
| 23 | + | // after each RerunRun. ListRuns and events describe the latest attempt; | |
| 24 | + | // GetRun can ask for another one. | |
| 25 | + | int32 attempt = 4; | |
| 26 | + | // WorkflowPath is the path of the workflow file at sha, for example | |
| 27 | + | // ".github/workflows/ci.yml". | |
| 28 | + | string workflow_path = 5; | |
| 29 | + | // WorkflowName is the "name" of the workflow, or workflow_path when it has | |
| 30 | + | // none (as GitHub does). | |
| 31 | + | string workflow_name = 6; | |
| 32 | + | // Title is the first line of the message of the commit sha, at most 256 | |
| 33 | + | // characters (GitHub's display title). | |
| 34 | + | string title = 7; | |
| 35 | + | // Event is the event that triggered the run, in GitHub's name: "push" (the | |
| 36 | + | // only one for now; push to a branch or a tag, and merge of a merge | |
| 37 | + | // request). New values may be added; clients show the ones they do not | |
| 38 | + | // know as they are. | |
| 39 | + | string event = 8; | |
| 40 | + | // Ref is the full ref name that was pushed, for example "refs/heads/main" | |
| 41 | + | // or "refs/tags/v1.0.0". | |
| 42 | + | string ref = 9; | |
| 43 | + | // Sha is the full id of the commit the run is for. | |
| 44 | + | string sha = 10; | |
| 45 | + | // Actor names who triggered the run: the label of the token that pushed, or | |
| 46 | + | // "root", or "owner" (a web session, a merge of the web UI), as | |
| 47 | + | // MergeRequest.author. It is informational. | |
| 48 | + | string actor = 11; | |
| 49 | + | // Status is where this attempt is: QUEUED until a job starts, IN_PROGRESS | |
| 50 | + | // while jobs run or wait, COMPLETED when every job is over. | |
| 51 | + | CiStatus status = 12; | |
| 52 | + | // Conclusion is how this attempt ended, from its jobs' conclusions like | |
| 53 | + | // GitHub: CANCELLED if it was cancelled, else FAILURE if a job failed or | |
| 54 | + | // timed out, else SUCCESS (also when jobs were skipped). Unset until | |
| 55 | + | // status is COMPLETED. | |
| 56 | + | CiConclusion conclusion = 13; | |
| 57 | + | // Error is set when the run failed without running a job because the | |
| 58 | + | // workflow could not be used: an invalid workflow file (the run then has | |
| 59 | + | // no jobs and conclusion FAILURE), as GitHub's "Invalid workflow file". | |
| 60 | + | // It holds the errors with their positions, one per line, at most 8 KiB. | |
| 61 | + | // Empty otherwise. | |
| 62 | + | string error = 14; | |
| 63 | + | // CreatedAt is when this attempt was created: for attempt 1 when the | |
| 64 | + | // trigger was planned, for later ones when RerunRun was called. | |
| 65 | + | google.protobuf.Timestamp created_at = 15; | |
| 66 | + | // StartedAt is when the first job of this attempt was given to a runner. | |
| 67 | + | // Unset until then. | |
| 68 | + | google.protobuf.Timestamp started_at = 16; | |
| 69 | + | // CompletedAt is when this attempt completed. Unset until then. | |
| 70 | + | google.protobuf.Timestamp completed_at = 17; | |
| 71 | + | // Version counts the changes of this run on the server: it grows by at least | |
| 72 | + | // one at every change and never goes back, also across attempts. Events are | |
| 73 | + | // not delivered in order, and a client that holds two copies of a run keeps | |
| 74 | + | // the one with the higher version and drops the other. | |
| 75 | + | int64 version = 18; | |
| 76 | + | } | |
| 77 | + | ||
| 78 | + | // RunJob is a job of a run as people see it: the state of one job instance | |
| 79 | + | // (one matrix combination is one job) in one attempt. It is not Job, which is | |
| 80 | + | // what a runner receives. | |
| 81 | + | message RunJob { | |
| 82 | + | // Id is the Job.job_id. It is the same in every attempt. | |
| 83 | + | string id = 1; | |
| 84 | + | // RunId is the run the job belongs to. | |
| 85 | + | int64 run_id = 2; | |
| 86 | + | // Attempt is the attempt of the run in which this job executed. It is lower | |
| 87 | + | // than the attempt of the run when the job was kept from an earlier attempt | |
| 88 | + | // by a re-run of the failed jobs only; the log and steps are then those of | |
| 89 | + | // that earlier attempt. Pass it to GetJob and GetJobLog. | |
| 90 | + | int32 attempt = 3; | |
| 91 | + | // Repo is the name of the repository. | |
| 92 | + | string repo = 4; | |
| 93 | + | // Key is the key of the job under "jobs:" in the workflow (the | |
| 94 | + | // "github.job"). Matrix instances of one job share it. | |
| 95 | + | string key = 5; | |
| 96 | + | // Name is the display name of the job, evaluated, with the matrix suffix as | |
| 97 | + | // GitHub names them, for example "build (linux, 1.27)". It is also the | |
| 98 | + | // name of the job's check. | |
| 99 | + | string name = 6; | |
| 100 | + | // Status is where the job is. A job waits QUEUED for a runner and for the | |
| 101 | + | // jobs it needs. | |
| 102 | + | CiStatus status = 7; | |
| 103 | + | // Conclusion is how the job ended. Unset until status is COMPLETED. | |
| 104 | + | CiConclusion conclusion = 8; | |
| 105 | + | // Error explains a FAILURE, CANCELLED or TIMED_OUT that no step explains, in | |
| 106 | + | // GitHub's words where it has them: the runner was lost ("The self-hosted | |
| 107 | + | // runner: <name> lost communication with the server..."), it received a | |
| 108 | + | // shutdown signal, no runner took the job within 24 hours, the log limit | |
| 109 | + | // was reached, the workflow uses a feature that is not supported yet | |
| 110 | + | // ("not supported yet: <feature>"). At most 4 KiB. Empty otherwise. | |
| 111 | + | string error = 9; | |
| 112 | + | // Labels are the runs-on labels of the job, evaluated. Empty while the job | |
| 113 | + | // is not planned yet. | |
| 114 | + | repeated string labels = 10; | |
| 115 | + | // Needs are the keys of the jobs this job needs, for drawing the graph. | |
| 116 | + | repeated string needs = 11; | |
| 117 | + | // RunnerId is the id of the runner that executed or is executing the job. | |
| 118 | + | // Empty while queued, and when the runner has since been removed (runner_name | |
| 119 | + | // and isolation are kept). | |
| 120 | + | string runner_id = 12; | |
| 121 | + | // RunnerName is the name of that runner. Empty while queued. | |
| 122 | + | string runner_name = 13; | |
| 123 | + | // Isolation is how that runner isolated the job. UNSPECIFIED while queued. | |
| 124 | + | RunnerIsolation isolation = 14; | |
| 125 | + | // Steps are the steps reported so far, ordered by number, including the | |
| 126 | + | // implicit "Set up job" and "Complete job". Empty while queued. Always | |
| 127 | + | // set, including in JobUpdated events. | |
| 128 | + | repeated Step steps = 15; | |
| 129 | + | // CreatedAt is when the job was created (planned). | |
| 130 | + | google.protobuf.Timestamp created_at = 16; | |
| 131 | + | // StartedAt is when the job was given to a runner. Unset until then. | |
| 132 | + | google.protobuf.Timestamp started_at = 17; | |
| 133 | + | // CompletedAt is when the job completed. Unset until then. | |
| 134 | + | google.protobuf.Timestamp completed_at = 18; | |
| 135 | + | // Version counts the changes of this job on the server (status, conclusion, | |
| 136 | + | // runner, steps, with any attempt): it grows by at least one at every change | |
| 137 | + | // and never goes back. A client that holds two copies of a job keeps the | |
| 138 | + | // one with the higher version and drops the other, as for Run.version. | |
| 139 | + | int64 version = 19; | |
| 140 | + | } | |
| 141 | + | ||
| 142 | + | // Step is one step of a job, with where its lines are in the job log. | |
| 143 | + | message Step { | |
| 144 | + | // Number is the 1-based number of the step in the job, the one the runner | |
| 145 | + | // uses in UpdateJob and AppendLog. | |
| 146 | + | int32 number = 1; | |
| 147 | + | // Name is the display name of the step. | |
| 148 | + | string name = 2; | |
| 149 | + | // State is where the step is. | |
| 150 | + | StepState state = 3; | |
| 151 | + | // Conclusion is how the step ended. Unset until state is COMPLETED. | |
| 152 | + | CiConclusion conclusion = 4; | |
| 153 | + | // StartedAt is when the step started. Unset while PENDING. | |
| 154 | + | google.protobuf.Timestamp started_at = 5; | |
| 155 | + | // CompletedAt is when the step ended. Unset until COMPLETED. | |
| 156 | + | google.protobuf.Timestamp completed_at = 6; | |
| 157 | + | // LogStart is the number of the first line of the step in the job log (the | |
| 158 | + | // offset of GetJobLogRequest and AppendLogRequest), counted from 0. | |
| 159 | + | int64 log_start = 7; | |
| 160 | + | // LogEnd is the number of the line after the last one of the step. For an | |
| 161 | + | // IN_PROGRESS step it is the number of lines held at the time of the | |
| 162 | + | // call and grows; for a step without a line it equals log_start. | |
| 163 | + | int64 log_end = 8; | |
| 164 | + | } | |
| 165 | + | ||
| 166 | + | // Check is the result of one job on one commit, the unit shown next to a | |
| 167 | + | // commit or a merge request and counted by the merge rules. There is one per | |
| 168 | + | // job instance, named like GitHub's checks. | |
| 169 | + | message Check { | |
| 170 | + | // Id identifies the check. It is opaque and stable across attempts. | |
| 171 | + | string id = 1; | |
| 172 | + | // Repo is the name of the repository. | |
| 173 | + | string repo = 2; | |
| 174 | + | // Sha is the full id of the commit. | |
| 175 | + | string sha = 3; | |
| 176 | + | // Name is the name of the check: the RunJob.name of the job. | |
| 177 | + | string name = 4; | |
| 178 | + | // Status is where the check is. | |
| 179 | + | CiStatus status = 5; | |
| 180 | + | // Conclusion is how the check ended. Unset until status is COMPLETED. The | |
| 181 | + | // value "stale" of GitHub is never used. | |
| 182 | + | CiConclusion conclusion = 6; | |
| 183 | + | // RunId is the run of the job. | |
| 184 | + | int64 run_id = 7; | |
| 185 | + | // JobId is the RunJob.id of the job, to open its page and log. | |
| 186 | + | string job_id = 8; | |
| 187 | + | // Attempt is the attempt the check describes: the latest attempt of the run. | |
| 188 | + | int32 attempt = 9; | |
| 189 | + | // WorkflowName is the Run.workflow_name of the run. | |
| 190 | + | string workflow_name = 10; | |
| 191 | + | // StartedAt is when the job was given to a runner. Unset while queued. | |
| 192 | + | google.protobuf.Timestamp started_at = 11; | |
| 193 | + | // CompletedAt is when the job completed. Unset until then. | |
| 194 | + | google.protobuf.Timestamp completed_at = 12; | |
| 195 | + | } | |
| 196 | + | ||
| 197 | + | // Runner is a registered CI runner. It never contains the runner token nor its | |
| 198 | + | // hash. | |
| 199 | + | message Runner { | |
| 200 | + | // Id identifies the runner in RemoveRunner. It is not a secret. | |
| 201 | + | string id = 1; | |
| 202 | + | // Name is the unique name chosen at registration. | |
| 203 | + | string name = 2; | |
| 204 | + | // Labels are all the labels the runner can match: the ones chosen at | |
| 205 | + | // registration plus the implicit ones ("self-hosted", the os and the arch), | |
| 206 | + | // lowercase, sorted. | |
| 207 | + | repeated string labels = 3; | |
| 208 | + | // Isolation is how the runner isolates jobs. | |
| 209 | + | RunnerIsolation isolation = 4; | |
| 210 | + | // Version is the version of the runner software. | |
| 211 | + | string version = 5; | |
| 212 | + | // Arch is "x64" or "arm64". | |
| 213 | + | string arch = 6; | |
| 214 | + | // Os is "linux". | |
| 215 | + | string os = 7; | |
| 216 | + | // RegisteredAt is when the runner was registered. | |
| 217 | + | google.protobuf.Timestamp registered_at = 8; | |
| 218 | + | // LastSeen is when the server last heard from the runner: a FetchJob poll or | |
| 219 | + | // a heartbeat of one of its jobs. Unset if never. It may lag behind the real | |
| 220 | + | // time by a short delay. A runner not seen for more than about 2 minutes is | |
| 221 | + | // offline. | |
| 222 | + | google.protobuf.Timestamp last_seen = 9; | |
| 223 | + | // Busy is true while the runner has a job in progress. | |
| 224 | + | bool busy = 10; | |
| 225 | + | } | |
| 226 | + | ||
| 227 | + | // RunService reads and controls CI runs, like `gh run` does on GitHub. It is for | |
| 228 | + | // owners and their tools; runners use RunnerService. | |
| 229 | + | // | |
| 230 | + | // Runs, jobs, checks and logs belong to a repository, and reading them needs | |
| 231 | + | // the same access as reading it: nothing for a public repository, the | |
| 232 | + | // repo:read scope for a private one. A repository that does not exist or that | |
| 233 | + | // the caller cannot read gives not_found; so does a run or a job that belongs | |
| 234 | + | // to another repository than the one named (ids are global but are always | |
| 235 | + | // checked against the repository). Cancelling and re-running need the ci:write | |
| 236 | + | // scope on the repository (a token restricted to other repositories gets | |
| 237 | + | // permission_denied). The runner RPCs (ListRunners, RemoveRunner) are not about | |
| 238 | + | // a repository and need the runner:register scope on a token that is not | |
| 239 | + | // restricted to repositories, or the root token. A caller without the needed | |
| 240 | + | // scope gets permission_denied. | |
| 241 | + | // | |
| 242 | + | // Page sizes and page tokens work as in the other services: page_size 0 is the | |
| 243 | + | // server default, larger values are lowered to the server maximum, and a | |
| 244 | + | // page_token is the next_page_token of a previous response for the same | |
| 245 | + | // request (other fields must be unchanged). | |
| 246 | + | // | |
| 247 | + | // Servers that implement the runs side (every RPC but ListRunners and | |
| 248 | + | // RemoveRunner) list "ci.runs" in GetVersionResponse.features; those that | |
| 249 | + | // implement the runners side list "ci.runners", as RunnerService does. A server | |
| 250 | + | // that honours the filters of ListRunsRequest also lists "ci.runs.filters". | |
| 251 | + | service RunService { | |
| 252 | + | // ListRuns lists the runs of a repository, highest id (newest) first. Each | |
| 253 | + | // run is described by its latest attempt. The filters narrow the result; a | |
| 254 | + | // server without the feature "ci.runs.filters" ignores them and returns | |
| 255 | + | // every run, so a client that needs them checks the feature. Fails with | |
| 256 | + | // invalid_argument if a filter is malformed. | |
| 257 | + | rpc ListRuns(ListRunsRequest) returns (ListRunsResponse); | |
| 258 | + | // GetRun returns a run with its jobs and their steps, for the latest attempt | |
| 259 | + | // or for the one asked. Fails with not_found if the run or the attempt does | |
| 260 | + | // not exist. | |
| 261 | + | rpc GetRun(GetRunRequest) returns (GetRunResponse); | |
| 262 | + | // GetJob returns one job with its steps. Fails with not_found if the job | |
| 263 | + | // or the attempt does not exist. | |
| 264 | + | rpc GetJob(GetJobRequest) returns (GetJobResponse); | |
| 265 | + | // GetJobLog streams the lines of the log of a job, from a line number. The | |
| 266 | + | // server sends them in order in messages of at most 1,000 lines and 64 KiB of | |
| 267 | + | // text each. | |
| 268 | + | // | |
| 269 | + | // Without follow, the stream ends after the last line stored when the call | |
| 270 | + | // was made. With follow, it goes on as lines arrive, with at most 1 second of | |
| 271 | + | // delay after the runner's AppendLog, and ends without error once the job has | |
| 272 | + | // completed and all its lines were sent; on a completed job follow changes | |
| 273 | + | // nothing. A client that loses the stream calls again with the offset after | |
| 274 | + | // the last line it received. The client may cancel at any time. | |
| 275 | + | // | |
| 276 | + | // Fails with not_found if the job, the attempt or its log does not exist (the | |
| 277 | + | // log is removed by the retention sweep, and a queued job has none: with | |
| 278 | + | // follow, the call waits for the first line instead), with out_of_range if | |
| 279 | + | // offset is beyond the end of the log of a job without follow, and with | |
| 280 | + | // resource_exhausted when there are too many concurrent log streams, in | |
| 281 | + | // total or from one client. Like Watch, a stream ends with unauthenticated | |
| 282 | + | // when the token was revoked or expired. | |
| 283 | + | rpc GetJobLog(GetJobLogRequest) returns (stream GetJobLogResponse); | |
| 284 | + | // CancelRun asks to cancel the latest attempt of a run. Needs the ci:write | |
| 285 | + | // scope. Jobs that have not started are cancelled at once; jobs in progress | |
| 286 | + | // get a CANCEL_REASON_USER request through WatchJob, and the run completes | |
| 287 | + | // when they have ended, so the returned run may still be IN_PROGRESS. | |
| 288 | + | // Cancelling a run that already has a cancellation requested succeeds and | |
| 289 | + | // changes nothing. Fails with failed_precondition if the run has completed. | |
| 290 | + | rpc CancelRun(CancelRunRequest) returns (CancelRunResponse); | |
| 291 | + | // RerunRun starts a new attempt of a completed run: same run id and number, | |
| 292 | + | // attempt + 1, same commit and same workflow file as at the original run (the | |
| 293 | + | // file of the repository is not read again). Needs the ci:write scope. Fails | |
| 294 | + | // with failed_precondition if the latest attempt has not completed, or if | |
| 295 | + | // failed_only is set and no job failed. The new attempt is returned QUEUED. | |
| 296 | + | rpc RerunRun(RerunRunRequest) returns (RerunRunResponse); | |
| 297 | + | // ListChecks lists the checks of a commit: for each job of each run on that | |
| 298 | + | // sha, the one of the latest attempt, ordered by creation time then name. | |
| 299 | + | // A commit without a run has no check: the list is empty, which is not an | |
| 300 | + | // error, and the server does not verify that the commit exists. Returns at | |
| 301 | + | // most 1,000 checks (truncated tells). | |
| 302 | + | rpc ListChecks(ListChecksRequest) returns (ListChecksResponse); | |
| 303 | + | // ListRunners lists the registered runners, ordered by name. Needs the | |
| 304 | + | // runner:register scope. | |
| 305 | + | rpc ListRunners(ListRunnersRequest) returns (ListRunnersResponse); | |
| 306 | + | // RemoveRunner removes a runner: its runner token stops working at once (a | |
| 307 | + | // FetchJob in progress ends with unauthenticated) and it no longer gets | |
| 308 | + | // jobs. A job it is running is not affected: it ends normally, or fails as | |
| 309 | + | // a lost runner without heartbeats. Needs the runner:register scope. Fails | |
| 310 | + | // with not_found if the id is unknown. | |
| 311 | + | rpc RemoveRunner(RemoveRunnerRequest) returns (RemoveRunnerResponse); | |
| 312 | + | } | |
| 313 | + | ||
| 314 | + | // ListRunsRequest selects a page of runs. | |
| 315 | + | message ListRunsRequest { | |
| 316 | + | // Repo is the repository name. | |
| 317 | + | string repo = 1; | |
| 318 | + | // PageSize is the maximum number of runs to return. | |
| 319 | + | int32 page_size = 2; | |
| 320 | + | // PageToken is the next_page_token of a previous response. Empty for the | |
| 321 | + | // first page. | |
| 322 | + | string page_token = 3; | |
| 323 | + | // Workflow restricts the result to one workflow file: its path | |
| 324 | + | // (".github/workflows/ci.yml") or its file name ("ci.yml"). Empty means | |
| 325 | + | // every workflow. Ignored by servers without "ci.runs.filters". | |
| 326 | + | string workflow = 4; | |
| 327 | + | // Branch restricts the result to the runs whose ref is this branch, by | |
| 328 | + | // short name ("main"). Empty means every ref. Ignored by servers without | |
| 329 | + | // "ci.runs.filters". | |
| 330 | + | string branch = 5; | |
| 331 | + | // Status restricts the result to the runs whose latest attempt has this | |
| 332 | + | // status. CI_STATUS_UNSPECIFIED means every status. Ignored by servers | |
| 333 | + | // without "ci.runs.filters". | |
| 334 | + | CiStatus status = 6; | |
| 335 | + | } | |
| 336 | + | ||
| 337 | + | // ListRunsResponse is one page of runs. | |
| 338 | + | message ListRunsResponse { | |
| 339 | + | // Runs are the runs of this page, highest id first, without jobs. | |
| 340 | + | repeated Run runs = 1; | |
| 341 | + | // NextPageToken is passed as page_token to get the next page. Empty when | |
| 342 | + | // there is no next page. | |
| 343 | + | string next_page_token = 2; | |
| 344 | + | } | |
| 345 | + | ||
| 346 | + | // GetRunRequest selects a run. | |
| 347 | + | message GetRunRequest { | |
| 348 | + | // Repo is the repository name. | |
| 349 | + | string repo = 1; | |
| 350 | + | // RunId is the Run.id. | |
| 351 | + | int64 run_id = 2; | |
| 352 | + | // Attempt is the attempt to read. 0 means the latest. | |
| 353 | + | int32 attempt = 3; | |
| 354 | + | } | |
| 355 | + | ||
| 356 | + | // GetRunResponse holds a run with its jobs. | |
| 357 | + | message GetRunResponse { | |
| 358 | + | // Run is the run, as of the attempt asked. | |
| 359 | + | Run run = 1; | |
| 360 | + | // Jobs are the jobs of that attempt, with their steps, in the order of the | |
| 361 | + | // workflow file (matrix instances in the order of the matrix). Jobs kept from | |
| 362 | + | // an earlier attempt by a re-run of failed jobs are included, with their own | |
| 363 | + | // RunJob.attempt. Empty for a run that failed on an invalid workflow file. | |
| 364 | + | repeated RunJob jobs = 2; | |
| 365 | + | // LatestAttempt is the latest attempt of the run, to offer the other ones. | |
| 366 | + | int32 latest_attempt = 3; | |
| 367 | + | } | |
| 368 | + | ||
| 369 | + | // GetJobRequest selects a job. | |
| 370 | + | message GetJobRequest { | |
| 371 | + | // Repo is the repository name. | |
| 372 | + | string repo = 1; | |
| 373 | + | // JobId is the RunJob.id. | |
| 374 | + | string job_id = 2; | |
| 375 | + | // Attempt is the RunJob.attempt to read. 0 means the latest. | |
| 376 | + | int32 attempt = 3; | |
| 377 | + | } | |
| 378 | + | ||
| 379 | + | // GetJobResponse holds a job. | |
| 380 | + | message GetJobResponse { | |
| 381 | + | // Job is the job with its steps. | |
| 382 | + | RunJob job = 1; | |
| 383 | + | } | |
| 384 | + | ||
| 385 | + | // GetJobLogRequest selects the lines to stream. | |
| 386 | + | message GetJobLogRequest { | |
| 387 | + | // Repo is the repository name. | |
| 388 | + | string repo = 1; | |
| 389 | + | // JobId is the RunJob.id. | |
| 390 | + | string job_id = 2; | |
| 391 | + | // Attempt is the RunJob.attempt whose log to read. 0 means the latest. | |
| 392 | + | int32 attempt = 3; | |
| 393 | + | // Offset is the number of the first line to send, counted from 0 across all | |
| 394 | + | // steps (see Step.log_start). Negative is invalid_argument. | |
| 395 | + | int64 offset = 4; | |
| 396 | + | // Follow keeps the stream open while the job runs. See GetJobLog. | |
| 397 | + | bool follow = 5; | |
| 398 | + | } | |
| 399 | + | ||
| 400 | + | // GetJobLogResponse is one message of the log stream. | |
| 401 | + | message GetJobLogResponse { | |
| 402 | + | // Offset is the number of the first line of lines. | |
| 403 | + | int64 offset = 1; | |
| 404 | + | // Lines are consecutive lines of the log, at most 1,000, at most 64 KiB of | |
| 405 | + | // text in total. Never empty. | |
| 406 | + | repeated JobLogLine lines = 2; | |
| 407 | + | } | |
| 408 | + | ||
| 409 | + | // JobLogLine is one line of a job log, as the runner sent it. | |
| 410 | + | message JobLogLine { | |
| 411 | + | // Step is the number of the step the line belongs to. | |
| 412 | + | int32 step = 1; | |
| 413 | + | // Time is when the runner produced the line, with nanosecond precision. | |
| 414 | + | google.protobuf.Timestamp time = 2; | |
| 415 | + | // Text is the line without its terminator, as in LogLine.text, including the | |
| 416 | + | // "##[group]" and "##[endgroup]" markers. Clients show it escaped. | |
| 417 | + | string text = 3; | |
| 418 | + | // Continued is true when the line continues the line before it, which was | |
| 419 | + | // too long for the runner to send whole (see LogLine.continued). Pieces keep | |
| 420 | + | // their own line numbers, so offsets are the same as the runner's; a client | |
| 421 | + | // that shows lines joins a continued line to the previous one. The raw log | |
| 422 | + | // file already has them joined. | |
| 423 | + | bool continued = 4; | |
| 424 | + | } | |
| 425 | + | ||
| 426 | + | // CancelRunRequest names the run to cancel. | |
| 427 | + | message CancelRunRequest { | |
| 428 | + | // Repo is the repository name. | |
| 429 | + | string repo = 1; | |
| 430 | + | // RunId is the Run.id. | |
| 431 | + | int64 run_id = 2; | |
| 432 | + | } | |
| 433 | + | ||
| 434 | + | // CancelRunResponse holds the run after the request. | |
| 435 | + | message CancelRunResponse { | |
| 436 | + | // Run is the latest attempt of the run. | |
| 437 | + | Run run = 1; | |
| 438 | + | } | |
| 439 | + | ||
| 440 | + | // RerunRunRequest names the run to run again. | |
| 441 | + | message RerunRunRequest { | |
| 442 | + | // Repo is the repository name. | |
| 443 | + | string repo = 1; | |
| 444 | + | // RunId is the Run.id. | |
| 445 | + | int64 run_id = 2; | |
| 446 | + | // FailedOnly runs again only the jobs of the latest attempt that did not | |
| 447 | + | // succeed (conclusion FAILURE, CANCELLED, TIMED_OUT or ACTION_REQUIRED) and | |
| 448 | + | // the jobs that need them, directly or not, even if they were skipped. The | |
| 449 | + | // other jobs keep their result, steps and log from the earlier attempt. When | |
| 450 | + | // false, every job runs again. | |
| 451 | + | bool failed_only = 3; | |
| 452 | + | } | |
| 453 | + | ||
| 454 | + | // RerunRunResponse holds the new attempt. | |
| 455 | + | message RerunRunResponse { | |
| 456 | + | // Run is the run as of the new attempt, QUEUED. | |
| 457 | + | Run run = 1; | |
| 458 | + | } | |
| 459 | + | ||
| 460 | + | // ListChecksRequest names the commit. | |
| 461 | + | message ListChecksRequest { | |
| 462 | + | // Repo is the repository name. | |
| 463 | + | string repo = 1; | |
| 464 | + | // Sha is the full commit id (40 lowercase hexadecimal characters for SHA-1). | |
| 465 | + | // Abbreviations and names are invalid_argument. | |
| 466 | + | string sha = 2; | |
| 467 | + | } | |
| 468 | + | ||
| 469 | + | // ListChecksResponse holds the checks of a commit. | |
| 470 | + | message ListChecksResponse { | |
| 471 | + | // Checks are the checks of the commit, one per job of the latest attempt of | |
| 472 | + | // each run, ordered by creation time then name. | |
| 473 | + | repeated Check checks = 1; | |
| 474 | + | // Truncated is true when the commit has more checks than checks holds. | |
| 475 | + | bool truncated = 2; | |
| 476 | + | } | |
| 477 | + | ||
| 478 | + | // ListRunnersRequest selects a page of runners. | |
| 479 | + | message ListRunnersRequest { | |
| 480 | + | // PageSize is the maximum number of runners to return. | |
| 481 | + | int32 page_size = 1; | |
| 482 | + | // PageToken is the next_page_token of a previous response. Empty for the | |
| 483 | + | // first page. | |
| 484 | + | string page_token = 2; | |
| 485 | + | } | |
| 486 | + | ||
| 487 | + | // ListRunnersResponse is one page of runners. | |
| 488 | + | message ListRunnersResponse { | |
| 489 | + | // Runners are the runners of this page, ordered by name. | |
| 490 | + | repeated Runner runners = 1; | |
| 491 | + | // NextPageToken is passed as page_token to get the next page. Empty when | |
| 492 | + | // there is no next page. | |
| 493 | + | string next_page_token = 2; | |
| 494 | + | } | |
| 495 | + | ||
| 496 | + | // RemoveRunnerRequest names the runner to remove. | |
| 497 | + | message RemoveRunnerRequest { | |
| 498 | + | // RunnerId is the Runner.id. | |
| 499 | + | string runner_id = 1; | |
| 500 | + | } | |
| 501 | + | ||
| 502 | + | // RemoveRunnerResponse is empty. | |
| 503 | + | message RemoveRunnerResponse {} | |
| @@ -0,0 +1,503 @@ | |||||
| 1 | + | // SPDX-License-Identifier: MIT | |||
| 2 | + | ||||
| 3 | + | syntax = "proto3"; | |||
| 4 | + | ||||
| 5 | + | package ermite; | |||
| 6 | + | ||||
| 7 | + | import "ermite/runner.proto"; | |||
| 8 | + | import "google/protobuf/timestamp.proto"; | |||
| 9 | + | ||||
| 10 | + | // Run is one execution of one workflow file for one event, like a GitHub | |||
| 11 | + | // workflow run. A re-run keeps the run and adds an attempt. | |||
| 12 | + | message Run { | |||
| 13 | + | // Repo is the name of the repository. | |||
| 14 | + | string repo = 1; | |||
| 15 | + | // Id identifies the run on the whole server: it is the "github.run_id". | |||
| 16 | + | // Increasing, never reused. | |||
| 17 | + | int64 id = 2; | |||
| 18 | + | // Number counts the runs of the same workflow file in the repository, | |||
| 19 | + | // starting at 1, never reused: it is the "github.run_number". It does not | |||
| 20 | + | // change on a re-run. | |||
| 21 | + | int64 number = 3; | |||
| 22 | + | // Attempt is the attempt this message describes: 1 for the first, one more | |||
| 23 | + | // after each RerunRun. ListRuns and events describe the latest attempt; | |||
| 24 | + | // GetRun can ask for another one. | |||
| 25 | + | int32 attempt = 4; | |||
| 26 | + | // WorkflowPath is the path of the workflow file at sha, for example | |||
| 27 | + | // ".github/workflows/ci.yml". | |||
| 28 | + | string workflow_path = 5; | |||
| 29 | + | // WorkflowName is the "name" of the workflow, or workflow_path when it has | |||
| 30 | + | // none (as GitHub does). | |||
| 31 | + | string workflow_name = 6; | |||
| 32 | + | // Title is the first line of the message of the commit sha, at most 256 | |||
| 33 | + | // characters (GitHub's display title). | |||
| 34 | + | string title = 7; | |||
| 35 | + | // Event is the event that triggered the run, in GitHub's name: "push" (the | |||
| 36 | + | // only one for now; push to a branch or a tag, and merge of a merge | |||
| 37 | + | // request). New values may be added; clients show the ones they do not | |||
| 38 | + | // know as they are. | |||
| 39 | + | string event = 8; | |||
| 40 | + | // Ref is the full ref name that was pushed, for example "refs/heads/main" | |||
| 41 | + | // or "refs/tags/v1.0.0". | |||
| 42 | + | string ref = 9; | |||
| 43 | + | // Sha is the full id of the commit the run is for. | |||
| 44 | + | string sha = 10; | |||
| 45 | + | // Actor names who triggered the run: the label of the token that pushed, or | |||
| 46 | + | // "root", or "owner" (a web session, a merge of the web UI), as | |||
| 47 | + | // MergeRequest.author. It is informational. | |||
| 48 | + | string actor = 11; | |||
| 49 | + | // Status is where this attempt is: QUEUED until a job starts, IN_PROGRESS | |||
| 50 | + | // while jobs run or wait, COMPLETED when every job is over. | |||
| 51 | + | CiStatus status = 12; | |||
| 52 | + | // Conclusion is how this attempt ended, from its jobs' conclusions like | |||
| 53 | + | // GitHub: CANCELLED if it was cancelled, else FAILURE if a job failed or | |||
| 54 | + | // timed out, else SUCCESS (also when jobs were skipped). Unset until | |||
| 55 | + | // status is COMPLETED. | |||
| 56 | + | CiConclusion conclusion = 13; | |||
| 57 | + | // Error is set when the run failed without running a job because the | |||
| 58 | + | // workflow could not be used: an invalid workflow file (the run then has | |||
| 59 | + | // no jobs and conclusion FAILURE), as GitHub's "Invalid workflow file". | |||
| 60 | + | // It holds the errors with their positions, one per line, at most 8 KiB. | |||
| 61 | + | // Empty otherwise. | |||
| 62 | + | string error = 14; | |||
| 63 | + | // CreatedAt is when this attempt was created: for attempt 1 when the | |||
| 64 | + | // trigger was planned, for later ones when RerunRun was called. | |||
| 65 | + | google.protobuf.Timestamp created_at = 15; | |||
| 66 | + | // StartedAt is when the first job of this attempt was given to a runner. | |||
| 67 | + | // Unset until then. | |||
| 68 | + | google.protobuf.Timestamp started_at = 16; | |||
| 69 | + | // CompletedAt is when this attempt completed. Unset until then. | |||
| 70 | + | google.protobuf.Timestamp completed_at = 17; | |||
| 71 | + | // Version counts the changes of this run on the server: it grows by at least | |||
| 72 | + | // one at every change and never goes back, also across attempts. Events are | |||
| 73 | + | // not delivered in order, and a client that holds two copies of a run keeps | |||
| 74 | + | // the one with the higher version and drops the other. | |||
| 75 | + | int64 version = 18; | |||
| 76 | + | } | |||
| 77 | + | ||||
| 78 | + | // RunJob is a job of a run as people see it: the state of one job instance | |||
| 79 | + | // (one matrix combination is one job) in one attempt. It is not Job, which is | |||
| 80 | + | // what a runner receives. | |||
| 81 | + | message RunJob { | |||
| 82 | + | // Id is the Job.job_id. It is the same in every attempt. | |||
| 83 | + | string id = 1; | |||
| 84 | + | // RunId is the run the job belongs to. | |||
| 85 | + | int64 run_id = 2; | |||
| 86 | + | // Attempt is the attempt of the run in which this job executed. It is lower | |||
| 87 | + | // than the attempt of the run when the job was kept from an earlier attempt | |||
| 88 | + | // by a re-run of the failed jobs only; the log and steps are then those of | |||
| 89 | + | // that earlier attempt. Pass it to GetJob and GetJobLog. | |||
| 90 | + | int32 attempt = 3; | |||
| 91 | + | // Repo is the name of the repository. | |||
| 92 | + | string repo = 4; | |||
| 93 | + | // Key is the key of the job under "jobs:" in the workflow (the | |||
| 94 | + | // "github.job"). Matrix instances of one job share it. | |||
| 95 | + | string key = 5; | |||
| 96 | + | // Name is the display name of the job, evaluated, with the matrix suffix as | |||
| 97 | + | // GitHub names them, for example "build (linux, 1.27)". It is also the | |||
| 98 | + | // name of the job's check. | |||
| 99 | + | string name = 6; | |||
| 100 | + | // Status is where the job is. A job waits QUEUED for a runner and for the | |||
| 101 | + | // jobs it needs. | |||
| 102 | + | CiStatus status = 7; | |||
| 103 | + | // Conclusion is how the job ended. Unset until status is COMPLETED. | |||
| 104 | + | CiConclusion conclusion = 8; | |||
| 105 | + | // Error explains a FAILURE, CANCELLED or TIMED_OUT that no step explains, in | |||
| 106 | + | // GitHub's words where it has them: the runner was lost ("The self-hosted | |||
| 107 | + | // runner: <name> lost communication with the server..."), it received a | |||
| 108 | + | // shutdown signal, no runner took the job within 24 hours, the log limit | |||
| 109 | + | // was reached, the workflow uses a feature that is not supported yet | |||
| 110 | + | // ("not supported yet: <feature>"). At most 4 KiB. Empty otherwise. | |||
| 111 | + | string error = 9; | |||
| 112 | + | // Labels are the runs-on labels of the job, evaluated. Empty while the job | |||
| 113 | + | // is not planned yet. | |||
| 114 | + | repeated string labels = 10; | |||
| 115 | + | // Needs are the keys of the jobs this job needs, for drawing the graph. | |||
| 116 | + | repeated string needs = 11; | |||
| 117 | + | // RunnerId is the id of the runner that executed or is executing the job. | |||
| 118 | + | // Empty while queued, and when the runner has since been removed (runner_name | |||
| 119 | + | // and isolation are kept). | |||
| 120 | + | string runner_id = 12; | |||
| 121 | + | // RunnerName is the name of that runner. Empty while queued. | |||
| 122 | + | string runner_name = 13; | |||
| 123 | + | // Isolation is how that runner isolated the job. UNSPECIFIED while queued. | |||
| 124 | + | RunnerIsolation isolation = 14; | |||
| 125 | + | // Steps are the steps reported so far, ordered by number, including the | |||
| 126 | + | // implicit "Set up job" and "Complete job". Empty while queued. Always | |||
| 127 | + | // set, including in JobUpdated events. | |||
| 128 | + | repeated Step steps = 15; | |||
| 129 | + | // CreatedAt is when the job was created (planned). | |||
| 130 | + | google.protobuf.Timestamp created_at = 16; | |||
| 131 | + | // StartedAt is when the job was given to a runner. Unset until then. | |||
| 132 | + | google.protobuf.Timestamp started_at = 17; | |||
| 133 | + | // CompletedAt is when the job completed. Unset until then. | |||
| 134 | + | google.protobuf.Timestamp completed_at = 18; | |||
| 135 | + | // Version counts the changes of this job on the server (status, conclusion, | |||
| 136 | + | // runner, steps, with any attempt): it grows by at least one at every change | |||
| 137 | + | // and never goes back. A client that holds two copies of a job keeps the | |||
| 138 | + | // one with the higher version and drops the other, as for Run.version. | |||
| 139 | + | int64 version = 19; | |||
| 140 | + | } | |||
| 141 | + | ||||
| 142 | + | // Step is one step of a job, with where its lines are in the job log. | |||
| 143 | + | message Step { | |||
| 144 | + | // Number is the 1-based number of the step in the job, the one the runner | |||
| 145 | + | // uses in UpdateJob and AppendLog. | |||
| 146 | + | int32 number = 1; | |||
| 147 | + | // Name is the display name of the step. | |||
| 148 | + | string name = 2; | |||
| 149 | + | // State is where the step is. | |||
| 150 | + | StepState state = 3; | |||
| 151 | + | // Conclusion is how the step ended. Unset until state is COMPLETED. | |||
| 152 | + | CiConclusion conclusion = 4; | |||
| 153 | + | // StartedAt is when the step started. Unset while PENDING. | |||
| 154 | + | google.protobuf.Timestamp started_at = 5; | |||
| 155 | + | // CompletedAt is when the step ended. Unset until COMPLETED. | |||
| 156 | + | google.protobuf.Timestamp completed_at = 6; | |||
| 157 | + | // LogStart is the number of the first line of the step in the job log (the | |||
| 158 | + | // offset of GetJobLogRequest and AppendLogRequest), counted from 0. | |||
| 159 | + | int64 log_start = 7; | |||
| 160 | + | // LogEnd is the number of the line after the last one of the step. For an | |||
| 161 | + | // IN_PROGRESS step it is the number of lines held at the time of the | |||
| 162 | + | // call and grows; for a step without a line it equals log_start. | |||
| 163 | + | int64 log_end = 8; | |||
| 164 | + | } | |||
| 165 | + | ||||
| 166 | + | // Check is the result of one job on one commit, the unit shown next to a | |||
| 167 | + | // commit or a merge request and counted by the merge rules. There is one per | |||
| 168 | + | // job instance, named like GitHub's checks. | |||
| 169 | + | message Check { | |||
| 170 | + | // Id identifies the check. It is opaque and stable across attempts. | |||
| 171 | + | string id = 1; | |||
| 172 | + | // Repo is the name of the repository. | |||
| 173 | + | string repo = 2; | |||
| 174 | + | // Sha is the full id of the commit. | |||
| 175 | + | string sha = 3; | |||
| 176 | + | // Name is the name of the check: the RunJob.name of the job. | |||
| 177 | + | string name = 4; | |||
| 178 | + | // Status is where the check is. | |||
| 179 | + | CiStatus status = 5; | |||
| 180 | + | // Conclusion is how the check ended. Unset until status is COMPLETED. The | |||
| 181 | + | // value "stale" of GitHub is never used. | |||
| 182 | + | CiConclusion conclusion = 6; | |||
| 183 | + | // RunId is the run of the job. | |||
| 184 | + | int64 run_id = 7; | |||
| 185 | + | // JobId is the RunJob.id of the job, to open its page and log. | |||
| 186 | + | string job_id = 8; | |||
| 187 | + | // Attempt is the attempt the check describes: the latest attempt of the run. | |||
| 188 | + | int32 attempt = 9; | |||
| 189 | + | // WorkflowName is the Run.workflow_name of the run. | |||
| 190 | + | string workflow_name = 10; | |||
| 191 | + | // StartedAt is when the job was given to a runner. Unset while queued. | |||
| 192 | + | google.protobuf.Timestamp started_at = 11; | |||
| 193 | + | // CompletedAt is when the job completed. Unset until then. | |||
| 194 | + | google.protobuf.Timestamp completed_at = 12; | |||
| 195 | + | } | |||
| 196 | + | ||||
| 197 | + | // Runner is a registered CI runner. It never contains the runner token nor its | |||
| 198 | + | // hash. | |||
| 199 | + | message Runner { | |||
| 200 | + | // Id identifies the runner in RemoveRunner. It is not a secret. | |||
| 201 | + | string id = 1; | |||
| 202 | + | // Name is the unique name chosen at registration. | |||
| 203 | + | string name = 2; | |||
| 204 | + | // Labels are all the labels the runner can match: the ones chosen at | |||
| 205 | + | // registration plus the implicit ones ("self-hosted", the os and the arch), | |||
| 206 | + | // lowercase, sorted. | |||
| 207 | + | repeated string labels = 3; | |||
| 208 | + | // Isolation is how the runner isolates jobs. | |||
| 209 | + | RunnerIsolation isolation = 4; | |||
| 210 | + | // Version is the version of the runner software. | |||
| 211 | + | string version = 5; | |||
| 212 | + | // Arch is "x64" or "arm64". | |||
| 213 | + | string arch = 6; | |||
| 214 | + | // Os is "linux". | |||
| 215 | + | string os = 7; | |||
| 216 | + | // RegisteredAt is when the runner was registered. | |||
| 217 | + | google.protobuf.Timestamp registered_at = 8; | |||
| 218 | + | // LastSeen is when the server last heard from the runner: a FetchJob poll or | |||
| 219 | + | // a heartbeat of one of its jobs. Unset if never. It may lag behind the real | |||
| 220 | + | // time by a short delay. A runner not seen for more than about 2 minutes is | |||
| 221 | + | // offline. | |||
| 222 | + | google.protobuf.Timestamp last_seen = 9; | |||
| 223 | + | // Busy is true while the runner has a job in progress. | |||
| 224 | + | bool busy = 10; | |||
| 225 | + | } | |||
| 226 | + | ||||
| 227 | + | // RunService reads and controls CI runs, like `gh run` does on GitHub. It is for | |||
| 228 | + | // owners and their tools; runners use RunnerService. | |||
| 229 | + | // | |||
| 230 | + | // Runs, jobs, checks and logs belong to a repository, and reading them needs | |||
| 231 | + | // the same access as reading it: nothing for a public repository, the | |||
| 232 | + | // repo:read scope for a private one. A repository that does not exist or that | |||
| 233 | + | // the caller cannot read gives not_found; so does a run or a job that belongs | |||
| 234 | + | // to another repository than the one named (ids are global but are always | |||
| 235 | + | // checked against the repository). Cancelling and re-running need the ci:write | |||
| 236 | + | // scope on the repository (a token restricted to other repositories gets | |||
| 237 | + | // permission_denied). The runner RPCs (ListRunners, RemoveRunner) are not about | |||
| 238 | + | // a repository and need the runner:register scope on a token that is not | |||
| 239 | + | // restricted to repositories, or the root token. A caller without the needed | |||
| 240 | + | // scope gets permission_denied. | |||
| 241 | + | // | |||
| 242 | + | // Page sizes and page tokens work as in the other services: page_size 0 is the | |||
| 243 | + | // server default, larger values are lowered to the server maximum, and a | |||
| 244 | + | // page_token is the next_page_token of a previous response for the same | |||
| 245 | + | // request (other fields must be unchanged). | |||
| 246 | + | // | |||
| 247 | + | // Servers that implement the runs side (every RPC but ListRunners and | |||
| 248 | + | // RemoveRunner) list "ci.runs" in GetVersionResponse.features; those that | |||
| 249 | + | // implement the runners side list "ci.runners", as RunnerService does. A server | |||
| 250 | + | // that honours the filters of ListRunsRequest also lists "ci.runs.filters". | |||
| 251 | + | service RunService { | |||
| 252 | + | // ListRuns lists the runs of a repository, highest id (newest) first. Each | |||
| 253 | + | // run is described by its latest attempt. The filters narrow the result; a | |||
| 254 | + | // server without the feature "ci.runs.filters" ignores them and returns | |||
| 255 | + | // every run, so a client that needs them checks the feature. Fails with | |||
| 256 | + | // invalid_argument if a filter is malformed. | |||
| 257 | + | rpc ListRuns(ListRunsRequest) returns (ListRunsResponse); | |||
| 258 | + | // GetRun returns a run with its jobs and their steps, for the latest attempt | |||
| 259 | + | // or for the one asked. Fails with not_found if the run or the attempt does | |||
| 260 | + | // not exist. | |||
| 261 | + | rpc GetRun(GetRunRequest) returns (GetRunResponse); | |||
| 262 | + | // GetJob returns one job with its steps. Fails with not_found if the job | |||
| 263 | + | // or the attempt does not exist. | |||
| 264 | + | rpc GetJob(GetJobRequest) returns (GetJobResponse); | |||
| 265 | + | // GetJobLog streams the lines of the log of a job, from a line number. The | |||
| 266 | + | // server sends them in order in messages of at most 1,000 lines and 64 KiB of | |||
| 267 | + | // text each. | |||
| 268 | + | // | |||
| 269 | + | // Without follow, the stream ends after the last line stored when the call | |||
| 270 | + | // was made. With follow, it goes on as lines arrive, with at most 1 second of | |||
| 271 | + | // delay after the runner's AppendLog, and ends without error once the job has | |||
| 272 | + | // completed and all its lines were sent; on a completed job follow changes | |||
| 273 | + | // nothing. A client that loses the stream calls again with the offset after | |||
| 274 | + | // the last line it received. The client may cancel at any time. | |||
| 275 | + | // | |||
| 276 | + | // Fails with not_found if the job, the attempt or its log does not exist (the | |||
| 277 | + | // log is removed by the retention sweep, and a queued job has none: with | |||
| 278 | + | // follow, the call waits for the first line instead), with out_of_range if | |||
| 279 | + | // offset is beyond the end of the log of a job without follow, and with | |||
| 280 | + | // resource_exhausted when there are too many concurrent log streams, in | |||
| 281 | + | // total or from one client. Like Watch, a stream ends with unauthenticated | |||
| 282 | + | // when the token was revoked or expired. | |||
| 283 | + | rpc GetJobLog(GetJobLogRequest) returns (stream GetJobLogResponse); | |||
| 284 | + | // CancelRun asks to cancel the latest attempt of a run. Needs the ci:write | |||
| 285 | + | // scope. Jobs that have not started are cancelled at once; jobs in progress | |||
| 286 | + | // get a CANCEL_REASON_USER request through WatchJob, and the run completes | |||
| 287 | + | // when they have ended, so the returned run may still be IN_PROGRESS. | |||
| 288 | + | // Cancelling a run that already has a cancellation requested succeeds and | |||
| 289 | + | // changes nothing. Fails with failed_precondition if the run has completed. | |||
| 290 | + | rpc CancelRun(CancelRunRequest) returns (CancelRunResponse); | |||
| 291 | + | // RerunRun starts a new attempt of a completed run: same run id and number, | |||
| 292 | + | // attempt + 1, same commit and same workflow file as at the original run (the | |||
| 293 | + | // file of the repository is not read again). Needs the ci:write scope. Fails | |||
| 294 | + | // with failed_precondition if the latest attempt has not completed, or if | |||
| 295 | + | // failed_only is set and no job failed. The new attempt is returned QUEUED. | |||
| 296 | + | rpc RerunRun(RerunRunRequest) returns (RerunRunResponse); | |||
| 297 | + | // ListChecks lists the checks of a commit: for each job of each run on that | |||
| 298 | + | // sha, the one of the latest attempt, ordered by creation time then name. | |||
| 299 | + | // A commit without a run has no check: the list is empty, which is not an | |||
| 300 | + | // error, and the server does not verify that the commit exists. Returns at | |||
| 301 | + | // most 1,000 checks (truncated tells). | |||
| 302 | + | rpc ListChecks(ListChecksRequest) returns (ListChecksResponse); | |||
| 303 | + | // ListRunners lists the registered runners, ordered by name. Needs the | |||
| 304 | + | // runner:register scope. | |||
| 305 | + | rpc ListRunners(ListRunnersRequest) returns (ListRunnersResponse); | |||
| 306 | + | // RemoveRunner removes a runner: its runner token stops working at once (a | |||
| 307 | + | // FetchJob in progress ends with unauthenticated) and it no longer gets | |||
| 308 | + | // jobs. A job it is running is not affected: it ends normally, or fails as | |||
| 309 | + | // a lost runner without heartbeats. Needs the runner:register scope. Fails | |||
| 310 | + | // with not_found if the id is unknown. | |||
| 311 | + | rpc RemoveRunner(RemoveRunnerRequest) returns (RemoveRunnerResponse); | |||
| 312 | + | } | |||
| 313 | + | ||||
| 314 | + | // ListRunsRequest selects a page of runs. | |||
| 315 | + | message ListRunsRequest { | |||
| 316 | + | // Repo is the repository name. | |||
| 317 | + | string repo = 1; | |||
| 318 | + | // PageSize is the maximum number of runs to return. | |||
| 319 | + | int32 page_size = 2; | |||
| 320 | + | // PageToken is the next_page_token of a previous response. Empty for the | |||
| 321 | + | // first page. | |||
| 322 | + | string page_token = 3; | |||
| 323 | + | // Workflow restricts the result to one workflow file: its path | |||
| 324 | + | // (".github/workflows/ci.yml") or its file name ("ci.yml"). Empty means | |||
| 325 | + | // every workflow. Ignored by servers without "ci.runs.filters". | |||
| 326 | + | string workflow = 4; | |||
| 327 | + | // Branch restricts the result to the runs whose ref is this branch, by | |||
| 328 | + | // short name ("main"). Empty means every ref. Ignored by servers without | |||
| 329 | + | // "ci.runs.filters". | |||
| 330 | + | string branch = 5; | |||
| 331 | + | // Status restricts the result to the runs whose latest attempt has this | |||
| 332 | + | // status. CI_STATUS_UNSPECIFIED means every status. Ignored by servers | |||
| 333 | + | // without "ci.runs.filters". | |||
| 334 | + | CiStatus status = 6; | |||
| 335 | + | } | |||
| 336 | + | ||||
| 337 | + | // ListRunsResponse is one page of runs. | |||
| 338 | + | message ListRunsResponse { | |||
| 339 | + | // Runs are the runs of this page, highest id first, without jobs. | |||
| 340 | + | repeated Run runs = 1; | |||
| 341 | + | // NextPageToken is passed as page_token to get the next page. Empty when | |||
| 342 | + | // there is no next page. | |||
| 343 | + | string next_page_token = 2; | |||
| 344 | + | } | |||
| 345 | + | ||||
| 346 | + | // GetRunRequest selects a run. | |||
| 347 | + | message GetRunRequest { | |||
| 348 | + | // Repo is the repository name. | |||
| 349 | + | string repo = 1; | |||
| 350 | + | // RunId is the Run.id. | |||
| 351 | + | int64 run_id = 2; | |||
| 352 | + | // Attempt is the attempt to read. 0 means the latest. | |||
| 353 | + | int32 attempt = 3; | |||
| 354 | + | } | |||
| 355 | + | ||||
| 356 | + | // GetRunResponse holds a run with its jobs. | |||
| 357 | + | message GetRunResponse { | |||
| 358 | + | // Run is the run, as of the attempt asked. | |||
| 359 | + | Run run = 1; | |||
| 360 | + | // Jobs are the jobs of that attempt, with their steps, in the order of the | |||
| 361 | + | // workflow file (matrix instances in the order of the matrix). Jobs kept from | |||
| 362 | + | // an earlier attempt by a re-run of failed jobs are included, with their own | |||
| 363 | + | // RunJob.attempt. Empty for a run that failed on an invalid workflow file. | |||
| 364 | + | repeated RunJob jobs = 2; | |||
| 365 | + | // LatestAttempt is the latest attempt of the run, to offer the other ones. | |||
| 366 | + | int32 latest_attempt = 3; | |||
| 367 | + | } | |||
| 368 | + | ||||
| 369 | + | // GetJobRequest selects a job. | |||
| 370 | + | message GetJobRequest { | |||
| 371 | + | // Repo is the repository name. | |||
| 372 | + | string repo = 1; | |||
| 373 | + | // JobId is the RunJob.id. | |||
| 374 | + | string job_id = 2; | |||
| 375 | + | // Attempt is the RunJob.attempt to read. 0 means the latest. | |||
| 376 | + | int32 attempt = 3; | |||
| 377 | + | } | |||
| 378 | + | ||||
| 379 | + | // GetJobResponse holds a job. | |||
| 380 | + | message GetJobResponse { | |||
| 381 | + | // Job is the job with its steps. | |||
| 382 | + | RunJob job = 1; | |||
| 383 | + | } | |||
| 384 | + | ||||
| 385 | + | // GetJobLogRequest selects the lines to stream. | |||
| 386 | + | message GetJobLogRequest { | |||
| 387 | + | // Repo is the repository name. | |||
| 388 | + | string repo = 1; | |||
| 389 | + | // JobId is the RunJob.id. | |||
| 390 | + | string job_id = 2; | |||
| 391 | + | // Attempt is the RunJob.attempt whose log to read. 0 means the latest. | |||
| 392 | + | int32 attempt = 3; | |||
| 393 | + | // Offset is the number of the first line to send, counted from 0 across all | |||
| 394 | + | // steps (see Step.log_start). Negative is invalid_argument. | |||
| 395 | + | int64 offset = 4; | |||
| 396 | + | // Follow keeps the stream open while the job runs. See GetJobLog. | |||
| 397 | + | bool follow = 5; | |||
| 398 | + | } | |||
| 399 | + | ||||
| 400 | + | // GetJobLogResponse is one message of the log stream. | |||
| 401 | + | message GetJobLogResponse { | |||
| 402 | + | // Offset is the number of the first line of lines. | |||
| 403 | + | int64 offset = 1; | |||
| 404 | + | // Lines are consecutive lines of the log, at most 1,000, at most 64 KiB of | |||
| 405 | + | // text in total. Never empty. | |||
| 406 | + | repeated JobLogLine lines = 2; | |||
| 407 | + | } | |||
| 408 | + | ||||
| 409 | + | // JobLogLine is one line of a job log, as the runner sent it. | |||
| 410 | + | message JobLogLine { | |||
| 411 | + | // Step is the number of the step the line belongs to. | |||
| 412 | + | int32 step = 1; | |||
| 413 | + | // Time is when the runner produced the line, with nanosecond precision. | |||
| 414 | + | google.protobuf.Timestamp time = 2; | |||
| 415 | + | // Text is the line without its terminator, as in LogLine.text, including the | |||
| 416 | + | // "##[group]" and "##[endgroup]" markers. Clients show it escaped. | |||
| 417 | + | string text = 3; | |||
| 418 | + | // Continued is true when the line continues the line before it, which was | |||
| 419 | + | // too long for the runner to send whole (see LogLine.continued). Pieces keep | |||
| 420 | + | // their own line numbers, so offsets are the same as the runner's; a client | |||
| 421 | + | // that shows lines joins a continued line to the previous one. The raw log | |||
| 422 | + | // file already has them joined. | |||
| 423 | + | bool continued = 4; | |||
| 424 | + | } | |||
| 425 | + | ||||
| 426 | + | // CancelRunRequest names the run to cancel. | |||
| 427 | + | message CancelRunRequest { | |||
| 428 | + | // Repo is the repository name. | |||
| 429 | + | string repo = 1; | |||
| 430 | + | // RunId is the Run.id. | |||
| 431 | + | int64 run_id = 2; | |||
| 432 | + | } | |||
| 433 | + | ||||
| 434 | + | // CancelRunResponse holds the run after the request. | |||
| 435 | + | message CancelRunResponse { | |||
| 436 | + | // Run is the latest attempt of the run. | |||
| 437 | + | Run run = 1; | |||
| 438 | + | } | |||
| 439 | + | ||||
| 440 | + | // RerunRunRequest names the run to run again. | |||
| 441 | + | message RerunRunRequest { | |||
| 442 | + | // Repo is the repository name. | |||
| 443 | + | string repo = 1; | |||
| 444 | + | // RunId is the Run.id. | |||
| 445 | + | int64 run_id = 2; | |||
| 446 | + | // FailedOnly runs again only the jobs of the latest attempt that did not | |||
| 447 | + | // succeed (conclusion FAILURE, CANCELLED, TIMED_OUT or ACTION_REQUIRED) and | |||
| 448 | + | // the jobs that need them, directly or not, even if they were skipped. The | |||
| 449 | + | // other jobs keep their result, steps and log from the earlier attempt. When | |||
| 450 | + | // false, every job runs again. | |||
| 451 | + | bool failed_only = 3; | |||
| 452 | + | } | |||
| 453 | + | ||||
| 454 | + | // RerunRunResponse holds the new attempt. | |||
| 455 | + | message RerunRunResponse { | |||
| 456 | + | // Run is the run as of the new attempt, QUEUED. | |||
| 457 | + | Run run = 1; | |||
| 458 | + | } | |||
| 459 | + | ||||
| 460 | + | // ListChecksRequest names the commit. | |||
| 461 | + | message ListChecksRequest { | |||
| 462 | + | // Repo is the repository name. | |||
| 463 | + | string repo = 1; | |||
| 464 | + | // Sha is the full commit id (40 lowercase hexadecimal characters for SHA-1). | |||
| 465 | + | // Abbreviations and names are invalid_argument. | |||
| 466 | + | string sha = 2; | |||
| 467 | + | } | |||
| 468 | + | ||||
| 469 | + | // ListChecksResponse holds the checks of a commit. | |||
| 470 | + | message ListChecksResponse { | |||
| 471 | + | // Checks are the checks of the commit, one per job of the latest attempt of | |||
| 472 | + | // each run, ordered by creation time then name. | |||
| 473 | + | repeated Check checks = 1; | |||
| 474 | + | // Truncated is true when the commit has more checks than checks holds. | |||
| 475 | + | bool truncated = 2; | |||
| 476 | + | } | |||
| 477 | + | ||||
| 478 | + | // ListRunnersRequest selects a page of runners. | |||
| 479 | + | message ListRunnersRequest { | |||
| 480 | + | // PageSize is the maximum number of runners to return. | |||
| 481 | + | int32 page_size = 1; | |||
| 482 | + | // PageToken is the next_page_token of a previous response. Empty for the | |||
| 483 | + | // first page. | |||
| 484 | + | string page_token = 2; | |||
| 485 | + | } | |||
| 486 | + | ||||
| 487 | + | // ListRunnersResponse is one page of runners. | |||
| 488 | + | message ListRunnersResponse { | |||
| 489 | + | // Runners are the runners of this page, ordered by name. | |||
| 490 | + | repeated Runner runners = 1; | |||
| 491 | + | // NextPageToken is passed as page_token to get the next page. Empty when | |||
| 492 | + | // there is no next page. | |||
| 493 | + | string next_page_token = 2; | |||
| 494 | + | } | |||
| 495 | + | ||||
| 496 | + | // RemoveRunnerRequest names the runner to remove. | |||
| 497 | + | message RemoveRunnerRequest { | |||
| 498 | + | // RunnerId is the Runner.id. | |||
| 499 | + | string runner_id = 1; | |||
| 500 | + | } | |||
| 501 | + | ||||
| 502 | + | // RemoveRunnerResponse is empty. | |||
| 503 | + | message RemoveRunnerResponse {} | |||
A proto/ermite/runner.proto +622 -0
| @@ -0,0 +1,622 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | syntax = "proto3"; | |
| 4 | + | ||
| 5 | + | package ermite; | |
| 6 | + | ||
| 7 | + | import "google/protobuf/duration.proto"; | |
| 8 | + | import "google/protobuf/timestamp.proto"; | |
| 9 | + | ||
| 10 | + | // CiStatus is where a run, a job or a check is in its life. It is the same | |
| 11 | + | // as GitHub's status, without the rarely used values. | |
| 12 | + | enum CiStatus { | |
| 13 | + | // Not set. In a filter it means every status. Never returned by the server. | |
| 14 | + | CI_STATUS_UNSPECIFIED = 0; | |
| 15 | + | // Waiting: for a runner, for the jobs it needs, or for its turn | |
| 16 | + | // (max-parallel). | |
| 17 | + | CI_STATUS_QUEUED = 1; | |
| 18 | + | // At least one job has been given to a runner and has not ended. | |
| 19 | + | CI_STATUS_IN_PROGRESS = 2; | |
| 20 | + | // Over. The conclusion says how it ended. | |
| 21 | + | CI_STATUS_COMPLETED = 3; | |
| 22 | + | } | |
| 23 | + | ||
| 24 | + | // CiConclusion is how a run, a job, a step or a check ended, with GitHub's | |
| 25 | + | // values and meaning. It is unset (CI_CONCLUSION_UNSPECIFIED) until the status is | |
| 26 | + | // COMPLETED. | |
| 27 | + | enum CiConclusion { | |
| 28 | + | // Not completed yet. Never returned by the server for a completed item. | |
| 29 | + | CI_CONCLUSION_UNSPECIFIED = 0; | |
| 30 | + | // Everything it ran succeeded. | |
| 31 | + | CI_CONCLUSION_SUCCESS = 1; | |
| 32 | + | // A step failed, or the job failed for another reason described in | |
| 33 | + | // RunJob.error (runner lost, queue timeout, log limit, feature not | |
| 34 | + | // supported yet, invalid workflow file). | |
| 35 | + | CI_CONCLUSION_FAILURE = 2; | |
| 36 | + | // Cancelled by the owner, or by fail-fast, or because the runner shut down. | |
| 37 | + | CI_CONCLUSION_CANCELLED = 3; | |
| 38 | + | // Never ran: its "if" was false, or a job it needs did not succeed, or it was | |
| 39 | + | // not selected. Never reported by a runner. | |
| 40 | + | CI_CONCLUSION_SKIPPED = 4; | |
| 41 | + | // Exceeded its timeout. | |
| 42 | + | CI_CONCLUSION_TIMED_OUT = 5; | |
| 43 | + | // Neither a success nor a failure. Defined for parity with GitHub; no | |
| 44 | + | // workflow job ends this way yet. For merging it counts as a success. | |
| 45 | + | CI_CONCLUSION_NEUTRAL = 6; | |
| 46 | + | // Waits for a human action (for example the approval of an environment). | |
| 47 | + | // Defined for parity with GitHub; the server does not produce it before | |
| 48 | + | // environments exist. For merging it counts as a failure. | |
| 49 | + | CI_CONCLUSION_ACTION_REQUIRED = 7; | |
| 50 | + | } | |
| 51 | + | ||
| 52 | + | // StepState is where a step is in its life. | |
| 53 | + | enum StepState { | |
| 54 | + | // Not set. Rejected in requests, never returned by the server. | |
| 55 | + | STEP_STATE_UNSPECIFIED = 0; | |
| 56 | + | // Known but not started. | |
| 57 | + | STEP_STATE_PENDING = 1; | |
| 58 | + | // Running. | |
| 59 | + | STEP_STATE_IN_PROGRESS = 2; | |
| 60 | + | // Over. The conclusion says how it ended. A step that did not run because | |
| 61 | + | // of its "if" or an earlier failure is COMPLETED with CI_CONCLUSION_SKIPPED. | |
| 62 | + | STEP_STATE_COMPLETED = 3; | |
| 63 | + | } | |
| 64 | + | ||
| 65 | + | // RunnerIsolation is how a runner isolates each job from the host and from | |
| 66 | + | // other jobs. The runner chooses it (auto-detection included) and reports it | |
| 67 | + | // at registration; the job page shows it. | |
| 68 | + | enum RunnerIsolation { | |
| 69 | + | // Not set. Rejected at registration, never returned by the server. | |
| 70 | + | RUNNER_ISOLATION_UNSPECIFIED = 0; | |
| 71 | + | // One micro-VM per job, destroyed at the end. | |
| 72 | + | RUNNER_ISOLATION_VM = 1; | |
| 73 | + | // One container per job with its own Docker daemon, run by the sysbox | |
| 74 | + | // runtime. Shares the host kernel. | |
| 75 | + | RUNNER_ISOLATION_CONTAINER_SYSBOX = 2; | |
| 76 | + | // One privileged container per job with its own Docker daemon. Shares the | |
| 77 | + | // host kernel and is equivalent to root on the host: the weakest mode. | |
| 78 | + | RUNNER_ISOLATION_CONTAINER_PRIVILEGED = 3; | |
| 79 | + | } | |
| 80 | + | ||
| 81 | + | // CancelReason says why the server asks a runner to stop a job. | |
| 82 | + | enum CancelReason { | |
| 83 | + | // Not set. Never sent in a CancelRequest. | |
| 84 | + | CANCEL_REASON_UNSPECIFIED = 0; | |
| 85 | + | // The owner cancelled the run (RunService.CancelRun, web or CLI). The job | |
| 86 | + | // ends CI_CONCLUSION_CANCELLED. | |
| 87 | + | CANCEL_REASON_USER = 1; | |
| 88 | + | // Another job of the same matrix failed and the strategy has fail-fast. The | |
| 89 | + | // job ends CI_CONCLUSION_CANCELLED. | |
| 90 | + | CANCEL_REASON_FAIL_FAST = 2; | |
| 91 | + | // The job exceeded its time budget (see Job.timeout_minutes). The job ends | |
| 92 | + | // CI_CONCLUSION_TIMED_OUT. | |
| 93 | + | CANCEL_REASON_TIMEOUT = 3; | |
| 94 | + | } | |
| 95 | + | ||
| 96 | + | // 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. | |
| 98 | + | // | |
| 99 | + | // Three credentials are used, each by different RPCs. All are sent as | |
| 100 | + | // "Authorization: Bearer <secret>": | |
| 101 | + | // | |
| 102 | + | // - RegisterRunner: a stored token with the runner:register scope, or the | |
| 103 | + | // root token. | |
| 104 | + | // - FetchJob: the runner token returned by RegisterRunner. It identifies the | |
| 105 | + | // runner and only works for FetchJob. It is "ermiterun_" followed by 43 | |
| 106 | + | // 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 | |
| 109 | + | // 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 | |
| 111 | + | // one of the token. | |
| 112 | + | // | |
| 113 | + | // Errors. unauthenticated means only that the credential is bad: missing, | |
| 114 | + | // unknown, expired, revoked, of the wrong kind for the RPC, or a job token | |
| 115 | + | // that was replaced (the job was given again, see FetchJob) or is older than | |
| 116 | + | // the 24 hours below. A runner that gets it for a job abandons the job at | |
| 117 | + | // once. A job token that is valid but belongs to another job than the job_id | |
| 118 | + | // of the request gets permission_denied. Repeated failed authentications are | |
| 119 | + | // rate limited per client address. | |
| 120 | + | // | |
| 121 | + | // 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 | |
| 123 | + | // timeout). From then on UpdateJob, AppendLog, Heartbeat and WatchJob answer | |
| 124 | + | // 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 | |
| 126 | + | // 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 | |
| 128 | + | // call that still does something, returns the stored result to a runner that | |
| 129 | + | // lost the answer of its last call or that was cut off by the server. After | |
| 130 | + | // the 24 hours every call gets unauthenticated. | |
| 131 | + | // | |
| 132 | + | // 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 | |
| 134 | + | // 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 | |
| 136 | + | // 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 | |
| 138 | + | // and the secrets of every repository, private ones included. Give it only to | |
| 139 | + | // runners you operate. | |
| 140 | + | // | |
| 141 | + | // Servers that implement this service list "ci.runners" in | |
| 142 | + | // GetVersionResponse.features. | |
| 143 | + | service RunnerService { | |
| 144 | + | // 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 | |
| 146 | + | // runner:register scope on a token that is not restricted to repositories | |
| 147 | + | // (a runner serves every repository), or the root token. Fails with | |
| 148 | + | // unauthenticated without a valid token, permission_denied without the | |
| 149 | + | // scope or with a repository-restricted token, invalid_argument if a field | |
| 150 | + | // breaks a rule, and already_exists if name is taken by another runner and | |
| 151 | + | // replace is not set. | |
| 152 | + | rpc RegisterRunner(RegisterRunnerRequest) returns (RegisterRunnerResponse); | |
| 153 | + | // FetchJob asks for a job to run (long poll). The server holds the call | |
| 154 | + | // until a queued job matches the runner, or about 50 seconds (never more | |
| 155 | + | // than 50) pass, then answers; without a job the response has no job and the | |
| 156 | + | // runner calls again after retry_after. Clients set a call timeout above 50 | |
| 157 | + | // seconds (60 is suggested) and must not poll when they have no free | |
| 158 | + | // capacity. | |
| 159 | + | // | |
| 160 | + | // A job matches when every label of its runs-on is among the labels of the | |
| 161 | + | // runner (case-insensitive). A job is given to exactly one runner, even when | |
| 162 | + | // many runners and many polls compete; the assignment is recorded before | |
| 163 | + | // the response is sent. The job stays QUEUED for the owner until it is | |
| 164 | + | // acknowledged. | |
| 165 | + | // | |
| 166 | + | // Acknowledgement. The runner's first Heartbeat for the job is the | |
| 167 | + | // acknowledgement, and the runner sends it as soon as it has the job, before | |
| 168 | + | // anything else. If none arrives within 30 seconds of the assignment (the | |
| 169 | + | // response was lost, or the runner died), the server cancels the assignment: | |
| 170 | + | // the job token is revoked, the job goes back to QUEUED with a fresh token | |
| 171 | + | // and can be given to any runner, the same one included; nothing the old | |
| 172 | + | // assignment stored is kept. This is not a failure of the job. A job whose | |
| 173 | + | // assignment is cancelled 3 times in a row is failed with CI_CONCLUSION_FAILURE | |
| 174 | + | // and an error saying so. | |
| 175 | + | // | |
| 176 | + | // Retry after a lost response. poll_id lets a client retry a poll safely: | |
| 177 | + | // a call with the same poll_id as an earlier call of the same runner that | |
| 178 | + | // assigned a job returns that same assignment (same Job, same token) as long | |
| 179 | + | // as it has not been acknowledged or cancelled, instead of taking another. | |
| 180 | + | // | |
| 181 | + | // A runner that crashed or whose host restarted cannot resume a job: it does | |
| 182 | + | // not keep the job token, so the job fails with the lost-runner error after | |
| 183 | + | // the heartbeat timeout (see Heartbeat). | |
| 184 | + | // | |
| 185 | + | // Needs the runner token. Fails with unauthenticated when the runner was | |
| 186 | + | // removed (also ending a call in progress) and invalid_argument if labels | |
| 187 | + | // or poll_id break a rule. | |
| 188 | + | rpc FetchJob(FetchJobRequest) returns (FetchJobResponse); | |
| 189 | + | // UpdateJob reports the state of the steps of a job while it runs. Needs the | |
| 190 | + | // job token. Each call is applied whole or not at all, and repeating an | |
| 191 | + | // identical call changes nothing, so a runner may retry after a network | |
| 192 | + | // error. To end the job use CompleteJob. | |
| 193 | + | // | |
| 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. | |
| 198 | + | rpc UpdateJob(UpdateJobRequest) returns (UpdateJobResponse); | |
| 199 | + | // AppendLog appends lines to the log of a job. Needs the job token. | |
| 200 | + | // | |
| 201 | + | // The log of a job is one sequence of lines, numbered from 0 across all | |
| 202 | + | // steps; offset is the number of lines the server holds when the request is | |
| 203 | + | // applied, that is the number of the first line of the request. Calls must | |
| 204 | + | // go in order, one at a time, and are idempotent: | |
| 205 | + | // | |
| 206 | + | // - offset equal to the server's line count: the lines are appended; | |
| 207 | + | // - offset below it: the first (count - offset) lines were already stored | |
| 208 | + | // (a retry after a lost response) and are skipped, and the rest is | |
| 209 | + | // appended; a request entirely stored changes nothing. The skipped lines | |
| 210 | + | // are not compared with the stored ones: a retry whose lines differ from | |
| 211 | + | // the first attempt is silently dropped, so a client must resend the | |
| 212 | + | // same lines at the same offset; | |
| 213 | + | // - offset above it: out_of_range (a gap is a bug of the client; the | |
| 214 | + | // message gives the line count the server holds). | |
| 215 | + | // | |
| 216 | + | // The response gives the line count after the call, so a client can check | |
| 217 | + | // itself. Offsets resume only after a network error within the same runner | |
| 218 | + | // process; they are not a way to resume a job after a crash. | |
| 219 | + | // | |
| 220 | + | // A request has at most 1,000 lines whose text adds up to at most 64 KiB | |
| 221 | + | // (65,536 bytes), all in the same step; more is invalid_argument. A line | |
| 222 | + | // longer than that is cut by the client into pieces, see LogLine.continued. | |
| 223 | + | // | |
| 224 | + | // Steps. The step of a request must be at least the step of the last line | |
| 225 | + | // the server holds, because the log is ordered by step (applied lines only: | |
| 226 | + | // lines skipped as already stored are not checked); a lower step is | |
| 227 | + | // failed_precondition. Lines for a step that UpdateJob has not announced | |
| 228 | + | // create it: it starts IN_PROGRESS at the time of its first line, with an | |
| 229 | + | // empty name until an UpdateJob gives one. | |
| 230 | + | // | |
| 231 | + | // The log of a job is limited to 512 MiB (536,870,912 bytes) as stored by | |
| 232 | + | // the server (text, timestamps and separators). The call that would exceed | |
| 233 | + | // it appends nothing; the server adds a last line saying that the limit was | |
| 234 | + | // reached, ends the job with CI_CONCLUSION_FAILURE and RunJob.error set, and | |
| 235 | + | // answers failed_precondition with a JobEnded detail like any call after the | |
| 236 | + | // end of a job. The runner then stops the job. | |
| 237 | + | // | |
| 238 | + | // Lines sent after the end of the job are rejected, so a runner flushes all | |
| 239 | + | // its AppendLog lines before it calls CompleteJob. | |
| 240 | + | // | |
| 241 | + | // Fails with invalid_argument if a rule is broken (step number, text with a | |
| 242 | + | // line feed or invalid UTF-8, missing time, a continued first line). | |
| 243 | + | rpc AppendLog(AppendLogRequest) returns (AppendLogResponse); | |
| 244 | + | // Heartbeat tells the server that the runner is alive and still working on | |
| 245 | + | // the job. Needs the job token. The first one acknowledges the assignment | |
| 246 | + | // (see FetchJob). The runner then calls it every 30 seconds for as long as | |
| 247 | + | // the job runs. The deadline is 2 minutes after the later of the last | |
| 248 | + | // heartbeat and the start of the server (so a server restart does not fail | |
| 249 | + | // the jobs of runners that could not reach it): past it the server ends the | |
| 250 | + | // job with CI_CONCLUSION_FAILURE and the message "The self-hosted runner: | |
| 251 | + | // <name> lost communication with the server...". Only Heartbeat counts: | |
| 252 | + | // other calls do not keep a job alive. | |
| 253 | + | // | |
| 254 | + | // A runner that gets unavailable (or any network error) retries Heartbeat | |
| 255 | + | // every 5 seconds until it succeeds, instead of waiting for its next 30 | |
| 256 | + | // second tick, and keeps the job running meanwhile. | |
| 257 | + | rpc Heartbeat(HeartbeatRequest) returns (HeartbeatResponse); | |
| 258 | + | // WatchJob streams what the runner must react to. Needs the job token. The | |
| 259 | + | // runner opens it when it starts the job and keeps it open; it reopens it | |
| 260 | + | // after any error other than unauthenticated and the failed_precondition | |
| 261 | + | // that says the job ended, and after a clean close of | |
| 262 | + | // the stream that did not end with a JobEnded message. | |
| 263 | + | // | |
| 264 | + | // When the stream opens and a cancellation has already been requested, the | |
| 265 | + | // first message is that request, so a reconnect never misses one. Only the | |
| 266 | + | // first reason is sent: a job already cancelled by the owner does not get a | |
| 267 | + | // second message when its time budget expires. The server also sends a | |
| 268 | + | // Keepalive at least every 30 seconds, which the client ignores. When the | |
| 269 | + | // job ends the server sends JobEnded, then closes the stream without error. | |
| 270 | + | // On a job that has already ended the call fails like the other RPCs | |
| 271 | + | // (failed_precondition with a JobEnded detail). | |
| 272 | + | // | |
| 273 | + | // On a CancelRequest the runner stops the job as the official runner does | |
| 274 | + | // (interrupt, then terminate, then kill) and reports the conclusion given | |
| 275 | + | // for the reason with CompleteJob. | |
| 276 | + | // | |
| 277 | + | // The server does not trust the runner to obey. If no conclusion arrives | |
| 278 | + | // within 60 seconds after a cancellation request, whatever the reason (user, | |
| 279 | + | // fail-fast, timeout), the server ends the job itself: CI_CONCLUSION_CANCELLED | |
| 280 | + | // for user and fail-fast, CI_CONCLUSION_TIMED_OUT for timeout, with an error | |
| 281 | + | // saying that the runner did not answer, and the steps not completed are | |
| 282 | + | // completed as cancelled. The runner's later CompleteJob returns that result. | |
| 283 | + | rpc WatchJob(WatchJobRequest) returns (stream WatchJobResponse); | |
| 284 | + | // CompleteJob ends a job: it reports the final steps, the outputs and the | |
| 285 | + | // conclusion. Needs the job token, which stays valid for this RPC for 24 | |
| 286 | + | // hours after the end of the job, however it ended. The call is applied whole | |
| 287 | + | // or not at all. Once the job has ended, the token works for no other RPC | |
| 288 | + | // (they answer failed_precondition with a JobEnded detail). | |
| 289 | + | // | |
| 290 | + | // It is idempotent. The first call that finds the job running ends it and | |
| 291 | + | // answers applied = true. Any later call, a retry after a lost response or a | |
| 292 | + | // call after the server ended the job itself, is not validated and changes | |
| 293 | + | // nothing: it answers OK with the stored conclusion and error and | |
| 294 | + | // applied = false (a runner whose conclusion differs from the stored one | |
| 295 | + | // simply logs it; the outputs it sent are dropped). | |
| 296 | + | // | |
| 297 | + | // Lines sent with AppendLog after this call are rejected: a runner flushes | |
| 298 | + | // all its log lines first. Steps that were not reported COMPLETED when the job | |
| 299 | + | // 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). | |
| 301 | + | // | |
| 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. | |
| 304 | + | rpc CompleteJob(CompleteJobRequest) returns (CompleteJobResponse); | |
| 305 | + | } | |
| 306 | + | ||
| 307 | + | // RegisterRunnerRequest describes the runner to register. | |
| 308 | + | message RegisterRunnerRequest { | |
| 309 | + | // Name identifies the runner for people (job page, logs, ListRunners) and is | |
| 310 | + | // unique: 1 to 64 characters among letters, digits, ".", "_" and "-". It is | |
| 311 | + | // not a label and never matches runs-on. | |
| 312 | + | string name = 1; | |
| 313 | + | // Labels are the labels the runner adds to its implicit ones: at most 64, | |
| 314 | + | // each 1 to 64 characters among letters, digits, ".", "_" and "-", for | |
| 315 | + | // example "ubuntu-latest". Comparison is case-insensitive and duplicates | |
| 316 | + | // are removed. The server adds the implicit labels "self-hosted", the os | |
| 317 | + | // and the arch ("linux", "x64" or "arm64"); the runner name is not a label. | |
| 318 | + | repeated string labels = 2; | |
| 319 | + | // Version is the release of the runner, for example "v0.1.0", or "devel". | |
| 320 | + | // Informational, at most 64 characters. | |
| 321 | + | string version = 3; | |
| 322 | + | // Isolation is how the runner isolates jobs. Required. | |
| 323 | + | RunnerIsolation isolation = 4; | |
| 324 | + | // Arch is the CPU architecture, in GitHub's names: "x64" or "arm64". Other | |
| 325 | + | // values are invalid_argument (the set may grow with compatible additions). | |
| 326 | + | string arch = 5; | |
| 327 | + | // Os is the operating system: "linux". Other values are invalid_argument | |
| 328 | + | // (the set may grow with compatible additions). | |
| 329 | + | string os = 6; | |
| 330 | + | // Replace removes the runner that already has this name, if any, before | |
| 331 | + | // registering this one: its runner token is revoked at once, as in | |
| 332 | + | // RunService.RemoveRunner, and a job it is running is not affected. The new | |
| 333 | + | // runner gets a new runner_id. Needs the same scope as registering. Without | |
| 334 | + | // replace, a name in use is already_exists. It is how a runner re-registers | |
| 335 | + | // after losing its state. | |
| 336 | + | bool replace = 7; | |
| 337 | + | } | |
| 338 | + | ||
| 339 | + | // RegisterRunnerResponse holds the new runner and its secret. | |
| 340 | + | message RegisterRunnerResponse { | |
| 341 | + | // RunnerId identifies the runner in ListRunners and RemoveRunner. It is not | |
| 342 | + | // a secret. | |
| 343 | + | string runner_id = 1; | |
| 344 | + | // RunnerToken is the secret of the runner, to present as "Authorization: | |
| 345 | + | // Bearer <runner_token>" to FetchJob: "ermiterun_" and 43 base64url | |
| 346 | + | // characters (32 random bytes, no padding). It is returned once: the server | |
| 347 | + | // keeps only its SHA-256, the runner must store it (mode 0600). It stops | |
| 348 | + | // working when the runner is removed or replaced. | |
| 349 | + | string runner_token = 2; | |
| 350 | + | } | |
| 351 | + | ||
| 352 | + | // FetchJobRequest asks for a job. | |
| 353 | + | message FetchJobRequest { | |
| 354 | + | // Labels narrows the labels offered for this call. Empty means all the | |
| 355 | + | // labels of the runner, which is the normal case. Otherwise each one must be | |
| 356 | + | // a label of the runner (invalid_argument if not) and only jobs whose runs-on | |
| 357 | + | // labels are all among them are offered. | |
| 358 | + | repeated string labels = 1; | |
| 359 | + | // PollId identifies this poll, chosen by the client (for example 16 random | |
| 360 | + | // bytes in base64url), 1 to 64 characters among letters, digits, "_" and | |
| 361 | + | // "-". A client that retries a call whose response it did not get sends the | |
| 362 | + | // same poll_id, and gets the assignment the first call made, if it made one | |
| 363 | + | // (see FetchJob). A new poll uses a new id. Empty means no retry protection: | |
| 364 | + | // a lost assignment is then recovered by the acknowledgement timeout alone. | |
| 365 | + | string poll_id = 2; | |
| 366 | + | } | |
| 367 | + | ||
| 368 | + | // FetchJobResponse holds the job given to the runner, if any. | |
| 369 | + | message FetchJobResponse { | |
| 370 | + | // Job is the job to run. Unset when no job was available before the end of | |
| 371 | + | // the poll. | |
| 372 | + | Job job = 1; | |
| 373 | + | // RetryAfter is how long the client waits before its next FetchJob. Unset or | |
| 374 | + | // zero means call again at once. The server sets it when it has no job for | |
| 375 | + | // the runner and wants to slow polling down (load, shutdown in progress); | |
| 376 | + | // clients obey it and may wait longer, never less. Unset when job is set. | |
| 377 | + | google.protobuf.Duration retry_after = 2; | |
| 378 | + | } | |
| 379 | + | ||
| 380 | + | // Job is a job given to a runner: everything needed to run it, nothing more. | |
| 381 | + | // The runner parses workflow_yaml itself; the protocol does not carry the | |
| 382 | + | // workflow model, so it does not change when the model grows. | |
| 383 | + | message Job { | |
| 384 | + | // JobId identifies the job on the server. It is opaque, unique across | |
| 385 | + | // repositories and runs, and the same for every attempt of the job. With | |
| 386 | + | // attempt it names one execution. | |
| 387 | + | string job_id = 1; | |
| 388 | + | // RunId is the run the job belongs to, the "github.run_id". It is global to | |
| 389 | + | // the server. | |
| 390 | + | int64 run_id = 2; | |
| 391 | + | // Attempt is the attempt of the run this job executes: 1 for the first, and | |
| 392 | + | // one more after each RunService.RerunRun. It is the "github.run_attempt". | |
| 393 | + | int32 attempt = 3; | |
| 394 | + | // JobToken is the secret that authenticates this job's UpdateJob, | |
| 395 | + | // AppendLog, Heartbeat, WatchJob and CompleteJob calls ("Authorization: | |
| 396 | + | // Bearer <job_token>"): "ermitejob_" and 43 base64url characters (32 random | |
| 397 | + | // bytes, no padding). It is random, valid from the assignment until the job | |
| 398 | + | // ends (and for CompleteJob 24 hours more), survives a server restart and | |
| 399 | + | // is shown only in this response, or in a repeat of the same poll_id. It | |
| 400 | + | // must stay in the runner host process and never reach the job's code. A new | |
| 401 | + | // assignment of the same job has a new token. | |
| 402 | + | string job_token = 4; | |
| 403 | + | // WorkflowPath is the path of the workflow file in the repository, for | |
| 404 | + | // example ".github/workflows/ci.yml". | |
| 405 | + | string workflow_path = 5; | |
| 406 | + | // WorkflowYaml is the content of that file at sha, as is, at most 1 MiB | |
| 407 | + | // (larger files fail at planning). | |
| 408 | + | string workflow_yaml = 6; | |
| 409 | + | // JobKey is the key of the job under "jobs:" in the workflow, the | |
| 410 | + | // "github.job". | |
| 411 | + | string job_key = 7; | |
| 412 | + | // Matrix is the JSON object of the matrix values of this instance of the | |
| 413 | + | // job, for example {"os":"linux","go":"1.27"}. "{}" when the job has no | |
| 414 | + | // matrix. It is the "matrix" context. | |
| 415 | + | string matrix = 8; | |
| 416 | + | // Contexts is a JSON object with the contexts that the server computed for | |
| 417 | + | // this job: "github", "needs", "strategy", "matrix", "inputs" and "vars", in | |
| 418 | + | // the shape GitHub gives them. The contexts "job", "runner", "steps" and | |
| 419 | + | // "env" belong to the runner and are not in it. Other keys may be added | |
| 420 | + | // later; clients ignore those they do not know. | |
| 421 | + | string contexts = 9; | |
| 422 | + | // Labels are the runs-on labels of the job, evaluated. | |
| 423 | + | repeated string labels = 10; | |
| 424 | + | // TimeoutMinutes is the timeout of the job in minutes: the value of the | |
| 425 | + | // workflow's timeout-minutes, or 360 when it has none. At least 1. The | |
| 426 | + | // runner enforces it from the moment it receives the job. The server counts | |
| 427 | + | // from the moment of the assignment and adds 60 seconds of slack, so the | |
| 428 | + | // runner's own timeout always comes first: when the server's deadline | |
| 429 | + | // passes it announces CANCEL_REASON_TIMEOUT, and ends the job itself after | |
| 430 | + | // 60 more seconds without a conclusion (see WatchJob). | |
| 431 | + | int32 timeout_minutes = 11; | |
| 432 | + | // Repo is the name of the repository. | |
| 433 | + | string repo = 12; | |
| 434 | + | // Sha is the full id of the commit the run is for, 40 lowercase | |
| 435 | + | // hexadecimal characters for SHA-1. The runner reads the code at it. | |
| 436 | + | string sha = 13; | |
| 437 | + | // Ref is the full ref name that triggered the run, for example | |
| 438 | + | // "refs/heads/main" or "refs/tags/v1.0.0". | |
| 439 | + | string ref = 14; | |
| 440 | + | } | |
| 441 | + | ||
| 442 | + | // UpdateJobRequest reports the steps of a running job. | |
| 443 | + | message UpdateJobRequest { | |
| 444 | + | // JobId is the Job.job_id. It must be the job of the token. | |
| 445 | + | string job_id = 1; | |
| 446 | + | // Steps are the steps whose state changed or that are new. Each one is a | |
| 447 | + | // full snapshot of its step, applied by number. At most 1,000 steps per job | |
| 448 | + | // (invalid_argument beyond). Steps are numbered like GitHub's log: the | |
| 449 | + | // implicit "Set up job" is 1, the steps of the workflow follow in order, and | |
| 450 | + | // the implicit "Complete job" is last. | |
| 451 | + | repeated StepUpdate steps = 2; | |
| 452 | + | } | |
| 453 | + | ||
| 454 | + | // StepUpdate is the state of one step as the runner knows it. | |
| 455 | + | message StepUpdate { | |
| 456 | + | // Number is the 1-based number of the step in the job. It is the number | |
| 457 | + | // used by AppendLog. | |
| 458 | + | int32 number = 1; | |
| 459 | + | // Name is the display name of the step, evaluated: the "name" of the | |
| 460 | + | // workflow step, or "Run <first line of the script>", as GitHub shows it. | |
| 461 | + | // At most 1,024 characters; the server cuts longer names. | |
| 462 | + | string name = 2; | |
| 463 | + | // State is where the step is. | |
| 464 | + | StepState state = 3; | |
| 465 | + | // Conclusion is how the step ended. Required (not UNSPECIFIED) when state is | |
| 466 | + | // COMPLETED, must be UNSPECIFIED otherwise. Allowed values: success, | |
| 467 | + | // failure, cancelled, skipped. | |
| 468 | + | CiConclusion conclusion = 4; | |
| 469 | + | // StartedAt is when the step started, by the clock of the runner. Required | |
| 470 | + | // from IN_PROGRESS on. | |
| 471 | + | google.protobuf.Timestamp started_at = 5; | |
| 472 | + | // CompletedAt is when the step ended, by the clock of the runner. Required | |
| 473 | + | // when state is COMPLETED. | |
| 474 | + | google.protobuf.Timestamp completed_at = 6; | |
| 475 | + | } | |
| 476 | + | ||
| 477 | + | // UpdateJobResponse is empty. | |
| 478 | + | message UpdateJobResponse {} | |
| 479 | + | ||
| 480 | + | // CompleteJobRequest ends a job. | |
| 481 | + | message CompleteJobRequest { | |
| 482 | + | // JobId is the Job.job_id. It must be the job of the token. | |
| 483 | + | string job_id = 1; | |
| 484 | + | // Conclusion is how the job ended. Required: a runner reports | |
| 485 | + | // CI_CONCLUSION_SUCCESS, CI_CONCLUSION_FAILURE, CI_CONCLUSION_CANCELLED or | |
| 486 | + | // CI_CONCLUSION_TIMED_OUT; any other value is invalid_argument. Skipped is | |
| 487 | + | // decided by the server and never reported by a runner. | |
| 488 | + | CiConclusion conclusion = 2; | |
| 489 | + | // Outputs are the outputs of the job, evaluated by the runner. Names are 1 | |
| 490 | + | // to 255 characters among letters, digits, "_" and "-" and begin with a | |
| 491 | + | // letter or "_" and are case-insensitive: two names that differ only by case | |
| 492 | + | // are invalid_argument. Each value is at most 1 MiB, and all names and | |
| 493 | + | // values together at most 1 MiB (1,048,576 bytes); beyond that the whole call | |
| 494 | + | // is rejected with invalid_argument and the runner reports the failure of | |
| 495 | + | // the job without outputs, with a line in the log. | |
| 496 | + | map<string, string> outputs = 3; | |
| 497 | + | // RunnerShutdown is true when the runner ends the job because it was told to | |
| 498 | + | // stop (service stopped, signal). Only allowed with CI_CONCLUSION_CANCELLED. | |
| 499 | + | // The server then annotates the job with GitHub's message "The runner has | |
| 500 | + | // received a shutdown signal. This can happen when the runner service is | |
| 501 | + | // stopped, or a manually started runner is canceled." The conclusion | |
| 502 | + | // CANCELLED for this case is to be verified against a recording of GitHub. | |
| 503 | + | bool runner_shutdown = 4; | |
| 504 | + | // Steps are the final steps, with the same meaning as UpdateJobRequest.steps, | |
| 505 | + | // applied in the same atomic step as the conclusion. | |
| 506 | + | repeated StepUpdate steps = 5; | |
| 507 | + | } | |
| 508 | + | ||
| 509 | + | // CompleteJobResponse is the stored result of a job. | |
| 510 | + | message CompleteJobResponse { | |
| 511 | + | // Conclusion is the conclusion of the job as the server stored it. It is the | |
| 512 | + | // one of the request when applied is true. | |
| 513 | + | CiConclusion conclusion = 1; | |
| 514 | + | // Error is the error text stored with the job, as RunJob.error. Empty when | |
| 515 | + | // there is none. | |
| 516 | + | string error = 2; | |
| 517 | + | // Applied is true when this call ended the job, false when the job had | |
| 518 | + | // already ended (a retry, or the server ended it first) and nothing changed. | |
| 519 | + | bool applied = 3; | |
| 520 | + | } | |
| 521 | + | ||
| 522 | + | // JobEnded says that a job has ended. It is sent in WatchJob and attached, as | |
| 523 | + | // an error detail, to the failed_precondition errors that RunnerService returns | |
| 524 | + | // for a job that has ended. | |
| 525 | + | message JobEnded { | |
| 526 | + | // Conclusion is how the job ended. | |
| 527 | + | CiConclusion conclusion = 1; | |
| 528 | + | // Error is the error text stored with the job, as RunJob.error. Empty when | |
| 529 | + | // there is none. | |
| 530 | + | string error = 2; | |
| 531 | + | } | |
| 532 | + | ||
| 533 | + | // AppendLogRequest carries log lines of one step. | |
| 534 | + | message AppendLogRequest { | |
| 535 | + | // JobId is the Job.job_id. It must be the job of the token. | |
| 536 | + | string job_id = 1; | |
| 537 | + | // Step is the number of the step the lines belong to (see | |
| 538 | + | // UpdateJobRequest.steps), at least 1, and at least the step of the last | |
| 539 | + | // stored line. A step that UpdateJob has not announced yet is created by the | |
| 540 | + | // first lines. See the rules on AppendLog. | |
| 541 | + | int32 step = 2; | |
| 542 | + | // Offset is the number of the first line of the request in the log of the | |
| 543 | + | // job, counted from 0 across all steps. See the rules on AppendLog. | |
| 544 | + | int64 offset = 3; | |
| 545 | + | // Lines are the lines to append, in order: at most 1,000, at most 64 KiB of | |
| 546 | + | // text in total. | |
| 547 | + | repeated LogLine lines = 4; | |
| 548 | + | } | |
| 549 | + | ||
| 550 | + | // LogLine is one line of a job log. | |
| 551 | + | message LogLine { | |
| 552 | + | // Time is when the line was produced, by the clock of the runner. The server | |
| 553 | + | // keeps it with nanosecond precision (RFC 3339) and does not reorder lines. | |
| 554 | + | google.protobuf.Timestamp time = 1; | |
| 555 | + | // Text is the line without its line terminator, valid UTF-8 (the client | |
| 556 | + | // replaces invalid bytes with U+FFFD). It must not contain a line feed; a | |
| 557 | + | // carriage return is kept. It is the line as GitHub's raw log has it, | |
| 558 | + | // including "##[group]" and "##[endgroup]" markers and "##[error]" lines. | |
| 559 | + | string text = 2; | |
| 560 | + | // Continued is true when this line is the continuation of the line before | |
| 561 | + | // it, which was too long to send whole. A runner cuts a line of more than | |
| 562 | + | // 64 KiB into pieces at UTF-8 character boundaries, each piece of at most | |
| 563 | + | // 64 KiB, and sets continued on every piece but the first. Each piece is one | |
| 564 | + | // line for numbering and offsets; the server and the raw log join the | |
| 565 | + | // pieces back, so the original line is rebuilt exactly. The first line of a | |
| 566 | + | // job cannot be continued (invalid_argument) and a continued line is in | |
| 567 | + | // the same step as the line before it (failed_precondition otherwise). | |
| 568 | + | bool continued = 3; | |
| 569 | + | } | |
| 570 | + | ||
| 571 | + | // AppendLogResponse tells how much of the log the server holds. | |
| 572 | + | message AppendLogResponse { | |
| 573 | + | // NextOffset is the number of lines of the job log after the call, which is | |
| 574 | + | // the offset of the next request. | |
| 575 | + | int64 next_offset = 1; | |
| 576 | + | } | |
| 577 | + | ||
| 578 | + | // HeartbeatRequest names the job that is alive. | |
| 579 | + | message HeartbeatRequest { | |
| 580 | + | // JobId is the Job.job_id. It must be the job of the token. | |
| 581 | + | string job_id = 1; | |
| 582 | + | } | |
| 583 | + | ||
| 584 | + | // HeartbeatResponse is empty. | |
| 585 | + | message HeartbeatResponse {} | |
| 586 | + | ||
| 587 | + | // WatchJobRequest names the job to watch. | |
| 588 | + | message WatchJobRequest { | |
| 589 | + | // JobId is the Job.job_id. It must be the job of the token. | |
| 590 | + | string job_id = 1; | |
| 591 | + | } | |
| 592 | + | ||
| 593 | + | // WatchJobResponse is one message of the stream. | |
| 594 | + | message WatchJobResponse { | |
| 595 | + | // Msg is what the message carries. New kinds may be added: a client ignores | |
| 596 | + | // the ones it does not know. | |
| 597 | + | oneof msg { | |
| 598 | + | // Keepalive: nothing to do, the stream is alive. | |
| 599 | + | Keepalive keepalive = 1; | |
| 600 | + | // Cancel: the job must stop. | |
| 601 | + | CancelRequest cancel = 2; | |
| 602 | + | // Ended: the job has ended. The last message of a stream: a close without | |
| 603 | + | // it means the client reconnects. | |
| 604 | + | JobEnded ended = 3; | |
| 605 | + | } | |
| 606 | + | } | |
| 607 | + | ||
| 608 | + | // Keepalive has no fields. | |
| 609 | + | message Keepalive {} | |
| 610 | + | ||
| 611 | + | // CancelRequest asks the runner to stop a job. | |
| 612 | + | message CancelRequest { | |
| 613 | + | // Reason is why the job must stop. New reasons may be added; a runner that | |
| 614 | + | // does not know one cancels the job and reports CI_CONCLUSION_CANCELLED. | |
| 615 | + | CancelReason reason = 1; | |
| 616 | + | // Message is a short human-readable explanation, for the job log, for | |
| 617 | + | // example "Canceled by the owner". At most 1,024 characters. | |
| 618 | + | string message = 2; | |
| 619 | + | // RequestedAt is when the cancellation was requested, by the clock of the | |
| 620 | + | // server. | |
| 621 | + | google.protobuf.Timestamp requested_at = 3; | |
| 622 | + | } | |
| @@ -0,0 +1,622 @@ | |||||
| 1 | + | // SPDX-License-Identifier: MIT | |||
| 2 | + | ||||
| 3 | + | syntax = "proto3"; | |||
| 4 | + | ||||
| 5 | + | package ermite; | |||
| 6 | + | ||||
| 7 | + | import "google/protobuf/duration.proto"; | |||
| 8 | + | import "google/protobuf/timestamp.proto"; | |||
| 9 | + | ||||
| 10 | + | // CiStatus is where a run, a job or a check is in its life. It is the same | |||
| 11 | + | // as GitHub's status, without the rarely used values. | |||
| 12 | + | enum CiStatus { | |||
| 13 | + | // Not set. In a filter it means every status. Never returned by the server. | |||
| 14 | + | CI_STATUS_UNSPECIFIED = 0; | |||
| 15 | + | // Waiting: for a runner, for the jobs it needs, or for its turn | |||
| 16 | + | // (max-parallel). | |||
| 17 | + | CI_STATUS_QUEUED = 1; | |||
| 18 | + | // At least one job has been given to a runner and has not ended. | |||
| 19 | + | CI_STATUS_IN_PROGRESS = 2; | |||
| 20 | + | // Over. The conclusion says how it ended. | |||
| 21 | + | CI_STATUS_COMPLETED = 3; | |||
| 22 | + | } | |||
| 23 | + | ||||
| 24 | + | // CiConclusion is how a run, a job, a step or a check ended, with GitHub's | |||
| 25 | + | // values and meaning. It is unset (CI_CONCLUSION_UNSPECIFIED) until the status is | |||
| 26 | + | // COMPLETED. | |||
| 27 | + | enum CiConclusion { | |||
| 28 | + | // Not completed yet. Never returned by the server for a completed item. | |||
| 29 | + | CI_CONCLUSION_UNSPECIFIED = 0; | |||
| 30 | + | // Everything it ran succeeded. | |||
| 31 | + | CI_CONCLUSION_SUCCESS = 1; | |||
| 32 | + | // A step failed, or the job failed for another reason described in | |||
| 33 | + | // RunJob.error (runner lost, queue timeout, log limit, feature not | |||
| 34 | + | // supported yet, invalid workflow file). | |||
| 35 | + | CI_CONCLUSION_FAILURE = 2; | |||
| 36 | + | // Cancelled by the owner, or by fail-fast, or because the runner shut down. | |||
| 37 | + | CI_CONCLUSION_CANCELLED = 3; | |||
| 38 | + | // Never ran: its "if" was false, or a job it needs did not succeed, or it was | |||
| 39 | + | // not selected. Never reported by a runner. | |||
| 40 | + | CI_CONCLUSION_SKIPPED = 4; | |||
| 41 | + | // Exceeded its timeout. | |||
| 42 | + | CI_CONCLUSION_TIMED_OUT = 5; | |||
| 43 | + | // Neither a success nor a failure. Defined for parity with GitHub; no | |||
| 44 | + | // workflow job ends this way yet. For merging it counts as a success. | |||
| 45 | + | CI_CONCLUSION_NEUTRAL = 6; | |||
| 46 | + | // Waits for a human action (for example the approval of an environment). | |||
| 47 | + | // Defined for parity with GitHub; the server does not produce it before | |||
| 48 | + | // environments exist. For merging it counts as a failure. | |||
| 49 | + | CI_CONCLUSION_ACTION_REQUIRED = 7; | |||
| 50 | + | } | |||
| 51 | + | ||||
| 52 | + | // StepState is where a step is in its life. | |||
| 53 | + | enum StepState { | |||
| 54 | + | // Not set. Rejected in requests, never returned by the server. | |||
| 55 | + | STEP_STATE_UNSPECIFIED = 0; | |||
| 56 | + | // Known but not started. | |||
| 57 | + | STEP_STATE_PENDING = 1; | |||
| 58 | + | // Running. | |||
| 59 | + | STEP_STATE_IN_PROGRESS = 2; | |||
| 60 | + | // Over. The conclusion says how it ended. A step that did not run because | |||
| 61 | + | // of its "if" or an earlier failure is COMPLETED with CI_CONCLUSION_SKIPPED. | |||
| 62 | + | STEP_STATE_COMPLETED = 3; | |||
| 63 | + | } | |||
| 64 | + | ||||
| 65 | + | // RunnerIsolation is how a runner isolates each job from the host and from | |||
| 66 | + | // other jobs. The runner chooses it (auto-detection included) and reports it | |||
| 67 | + | // at registration; the job page shows it. | |||
| 68 | + | enum RunnerIsolation { | |||
| 69 | + | // Not set. Rejected at registration, never returned by the server. | |||
| 70 | + | RUNNER_ISOLATION_UNSPECIFIED = 0; | |||
| 71 | + | // One micro-VM per job, destroyed at the end. | |||
| 72 | + | RUNNER_ISOLATION_VM = 1; | |||
| 73 | + | // One container per job with its own Docker daemon, run by the sysbox | |||
| 74 | + | // runtime. Shares the host kernel. | |||
| 75 | + | RUNNER_ISOLATION_CONTAINER_SYSBOX = 2; | |||
| 76 | + | // One privileged container per job with its own Docker daemon. Shares the | |||
| 77 | + | // host kernel and is equivalent to root on the host: the weakest mode. | |||
| 78 | + | RUNNER_ISOLATION_CONTAINER_PRIVILEGED = 3; | |||
| 79 | + | } | |||
| 80 | + | ||||
| 81 | + | // CancelReason says why the server asks a runner to stop a job. | |||
| 82 | + | enum CancelReason { | |||
| 83 | + | // Not set. Never sent in a CancelRequest. | |||
| 84 | + | CANCEL_REASON_UNSPECIFIED = 0; | |||
| 85 | + | // The owner cancelled the run (RunService.CancelRun, web or CLI). The job | |||
| 86 | + | // ends CI_CONCLUSION_CANCELLED. | |||
| 87 | + | CANCEL_REASON_USER = 1; | |||
| 88 | + | // Another job of the same matrix failed and the strategy has fail-fast. The | |||
| 89 | + | // job ends CI_CONCLUSION_CANCELLED. | |||
| 90 | + | CANCEL_REASON_FAIL_FAST = 2; | |||
| 91 | + | // The job exceeded its time budget (see Job.timeout_minutes). The job ends | |||
| 92 | + | // CI_CONCLUSION_TIMED_OUT. | |||
| 93 | + | CANCEL_REASON_TIMEOUT = 3; | |||
| 94 | + | } | |||
| 95 | + | ||||
| 96 | + | // 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. | |||
| 98 | + | // | |||
| 99 | + | // Three credentials are used, each by different RPCs. All are sent as | |||
| 100 | + | // "Authorization: Bearer <secret>": | |||
| 101 | + | // | |||
| 102 | + | // - RegisterRunner: a stored token with the runner:register scope, or the | |||
| 103 | + | // root token. | |||
| 104 | + | // - FetchJob: the runner token returned by RegisterRunner. It identifies the | |||
| 105 | + | // runner and only works for FetchJob. It is "ermiterun_" followed by 43 | |||
| 106 | + | // 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 | |||
| 109 | + | // 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 | |||
| 111 | + | // one of the token. | |||
| 112 | + | // | |||
| 113 | + | // Errors. unauthenticated means only that the credential is bad: missing, | |||
| 114 | + | // unknown, expired, revoked, of the wrong kind for the RPC, or a job token | |||
| 115 | + | // that was replaced (the job was given again, see FetchJob) or is older than | |||
| 116 | + | // the 24 hours below. A runner that gets it for a job abandons the job at | |||
| 117 | + | // once. A job token that is valid but belongs to another job than the job_id | |||
| 118 | + | // of the request gets permission_denied. Repeated failed authentications are | |||
| 119 | + | // rate limited per client address. | |||
| 120 | + | // | |||
| 121 | + | // 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 | |||
| 123 | + | // timeout). From then on UpdateJob, AppendLog, Heartbeat and WatchJob answer | |||
| 124 | + | // 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 | |||
| 126 | + | // 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 | |||
| 128 | + | // call that still does something, returns the stored result to a runner that | |||
| 129 | + | // lost the answer of its last call or that was cut off by the server. After | |||
| 130 | + | // the 24 hours every call gets unauthenticated. | |||
| 131 | + | // | |||
| 132 | + | // 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 | |||
| 134 | + | // 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 | |||
| 136 | + | // 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 | |||
| 138 | + | // and the secrets of every repository, private ones included. Give it only to | |||
| 139 | + | // runners you operate. | |||
| 140 | + | // | |||
| 141 | + | // Servers that implement this service list "ci.runners" in | |||
| 142 | + | // GetVersionResponse.features. | |||
| 143 | + | service RunnerService { | |||
| 144 | + | // 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 | |||
| 146 | + | // runner:register scope on a token that is not restricted to repositories | |||
| 147 | + | // (a runner serves every repository), or the root token. Fails with | |||
| 148 | + | // unauthenticated without a valid token, permission_denied without the | |||
| 149 | + | // scope or with a repository-restricted token, invalid_argument if a field | |||
| 150 | + | // breaks a rule, and already_exists if name is taken by another runner and | |||
| 151 | + | // replace is not set. | |||
| 152 | + | rpc RegisterRunner(RegisterRunnerRequest) returns (RegisterRunnerResponse); | |||
| 153 | + | // FetchJob asks for a job to run (long poll). The server holds the call | |||
| 154 | + | // until a queued job matches the runner, or about 50 seconds (never more | |||
| 155 | + | // than 50) pass, then answers; without a job the response has no job and the | |||
| 156 | + | // runner calls again after retry_after. Clients set a call timeout above 50 | |||
| 157 | + | // seconds (60 is suggested) and must not poll when they have no free | |||
| 158 | + | // capacity. | |||
| 159 | + | // | |||
| 160 | + | // A job matches when every label of its runs-on is among the labels of the | |||
| 161 | + | // runner (case-insensitive). A job is given to exactly one runner, even when | |||
| 162 | + | // many runners and many polls compete; the assignment is recorded before | |||
| 163 | + | // the response is sent. The job stays QUEUED for the owner until it is | |||
| 164 | + | // acknowledged. | |||
| 165 | + | // | |||
| 166 | + | // Acknowledgement. The runner's first Heartbeat for the job is the | |||
| 167 | + | // acknowledgement, and the runner sends it as soon as it has the job, before | |||
| 168 | + | // anything else. If none arrives within 30 seconds of the assignment (the | |||
| 169 | + | // response was lost, or the runner died), the server cancels the assignment: | |||
| 170 | + | // the job token is revoked, the job goes back to QUEUED with a fresh token | |||
| 171 | + | // and can be given to any runner, the same one included; nothing the old | |||
| 172 | + | // assignment stored is kept. This is not a failure of the job. A job whose | |||
| 173 | + | // assignment is cancelled 3 times in a row is failed with CI_CONCLUSION_FAILURE | |||
| 174 | + | // and an error saying so. | |||
| 175 | + | // | |||
| 176 | + | // Retry after a lost response. poll_id lets a client retry a poll safely: | |||
| 177 | + | // a call with the same poll_id as an earlier call of the same runner that | |||
| 178 | + | // assigned a job returns that same assignment (same Job, same token) as long | |||
| 179 | + | // as it has not been acknowledged or cancelled, instead of taking another. | |||
| 180 | + | // | |||
| 181 | + | // A runner that crashed or whose host restarted cannot resume a job: it does | |||
| 182 | + | // not keep the job token, so the job fails with the lost-runner error after | |||
| 183 | + | // the heartbeat timeout (see Heartbeat). | |||
| 184 | + | // | |||
| 185 | + | // Needs the runner token. Fails with unauthenticated when the runner was | |||
| 186 | + | // removed (also ending a call in progress) and invalid_argument if labels | |||
| 187 | + | // or poll_id break a rule. | |||
| 188 | + | rpc FetchJob(FetchJobRequest) returns (FetchJobResponse); | |||
| 189 | + | // UpdateJob reports the state of the steps of a job while it runs. Needs the | |||
| 190 | + | // job token. Each call is applied whole or not at all, and repeating an | |||
| 191 | + | // identical call changes nothing, so a runner may retry after a network | |||
| 192 | + | // error. To end the job use CompleteJob. | |||
| 193 | + | // | |||
| 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. | |||
| 198 | + | rpc UpdateJob(UpdateJobRequest) returns (UpdateJobResponse); | |||
| 199 | + | // AppendLog appends lines to the log of a job. Needs the job token. | |||
| 200 | + | // | |||
| 201 | + | // The log of a job is one sequence of lines, numbered from 0 across all | |||
| 202 | + | // steps; offset is the number of lines the server holds when the request is | |||
| 203 | + | // applied, that is the number of the first line of the request. Calls must | |||
| 204 | + | // go in order, one at a time, and are idempotent: | |||
| 205 | + | // | |||
| 206 | + | // - offset equal to the server's line count: the lines are appended; | |||
| 207 | + | // - offset below it: the first (count - offset) lines were already stored | |||
| 208 | + | // (a retry after a lost response) and are skipped, and the rest is | |||
| 209 | + | // appended; a request entirely stored changes nothing. The skipped lines | |||
| 210 | + | // are not compared with the stored ones: a retry whose lines differ from | |||
| 211 | + | // the first attempt is silently dropped, so a client must resend the | |||
| 212 | + | // same lines at the same offset; | |||
| 213 | + | // - offset above it: out_of_range (a gap is a bug of the client; the | |||
| 214 | + | // message gives the line count the server holds). | |||
| 215 | + | // | |||
| 216 | + | // The response gives the line count after the call, so a client can check | |||
| 217 | + | // itself. Offsets resume only after a network error within the same runner | |||
| 218 | + | // process; they are not a way to resume a job after a crash. | |||
| 219 | + | // | |||
| 220 | + | // A request has at most 1,000 lines whose text adds up to at most 64 KiB | |||
| 221 | + | // (65,536 bytes), all in the same step; more is invalid_argument. A line | |||
| 222 | + | // longer than that is cut by the client into pieces, see LogLine.continued. | |||
| 223 | + | // | |||
| 224 | + | // Steps. The step of a request must be at least the step of the last line | |||
| 225 | + | // the server holds, because the log is ordered by step (applied lines only: | |||
| 226 | + | // lines skipped as already stored are not checked); a lower step is | |||
| 227 | + | // failed_precondition. Lines for a step that UpdateJob has not announced | |||
| 228 | + | // create it: it starts IN_PROGRESS at the time of its first line, with an | |||
| 229 | + | // empty name until an UpdateJob gives one. | |||
| 230 | + | // | |||
| 231 | + | // The log of a job is limited to 512 MiB (536,870,912 bytes) as stored by | |||
| 232 | + | // the server (text, timestamps and separators). The call that would exceed | |||
| 233 | + | // it appends nothing; the server adds a last line saying that the limit was | |||
| 234 | + | // reached, ends the job with CI_CONCLUSION_FAILURE and RunJob.error set, and | |||
| 235 | + | // answers failed_precondition with a JobEnded detail like any call after the | |||
| 236 | + | // end of a job. The runner then stops the job. | |||
| 237 | + | // | |||
| 238 | + | // Lines sent after the end of the job are rejected, so a runner flushes all | |||
| 239 | + | // its AppendLog lines before it calls CompleteJob. | |||
| 240 | + | // | |||
| 241 | + | // Fails with invalid_argument if a rule is broken (step number, text with a | |||
| 242 | + | // line feed or invalid UTF-8, missing time, a continued first line). | |||
| 243 | + | rpc AppendLog(AppendLogRequest) returns (AppendLogResponse); | |||
| 244 | + | // Heartbeat tells the server that the runner is alive and still working on | |||
| 245 | + | // the job. Needs the job token. The first one acknowledges the assignment | |||
| 246 | + | // (see FetchJob). The runner then calls it every 30 seconds for as long as | |||
| 247 | + | // the job runs. The deadline is 2 minutes after the later of the last | |||
| 248 | + | // heartbeat and the start of the server (so a server restart does not fail | |||
| 249 | + | // the jobs of runners that could not reach it): past it the server ends the | |||
| 250 | + | // job with CI_CONCLUSION_FAILURE and the message "The self-hosted runner: | |||
| 251 | + | // <name> lost communication with the server...". Only Heartbeat counts: | |||
| 252 | + | // other calls do not keep a job alive. | |||
| 253 | + | // | |||
| 254 | + | // A runner that gets unavailable (or any network error) retries Heartbeat | |||
| 255 | + | // every 5 seconds until it succeeds, instead of waiting for its next 30 | |||
| 256 | + | // second tick, and keeps the job running meanwhile. | |||
| 257 | + | rpc Heartbeat(HeartbeatRequest) returns (HeartbeatResponse); | |||
| 258 | + | // WatchJob streams what the runner must react to. Needs the job token. The | |||
| 259 | + | // runner opens it when it starts the job and keeps it open; it reopens it | |||
| 260 | + | // after any error other than unauthenticated and the failed_precondition | |||
| 261 | + | // that says the job ended, and after a clean close of | |||
| 262 | + | // the stream that did not end with a JobEnded message. | |||
| 263 | + | // | |||
| 264 | + | // When the stream opens and a cancellation has already been requested, the | |||
| 265 | + | // first message is that request, so a reconnect never misses one. Only the | |||
| 266 | + | // first reason is sent: a job already cancelled by the owner does not get a | |||
| 267 | + | // second message when its time budget expires. The server also sends a | |||
| 268 | + | // Keepalive at least every 30 seconds, which the client ignores. When the | |||
| 269 | + | // job ends the server sends JobEnded, then closes the stream without error. | |||
| 270 | + | // On a job that has already ended the call fails like the other RPCs | |||
| 271 | + | // (failed_precondition with a JobEnded detail). | |||
| 272 | + | // | |||
| 273 | + | // On a CancelRequest the runner stops the job as the official runner does | |||
| 274 | + | // (interrupt, then terminate, then kill) and reports the conclusion given | |||
| 275 | + | // for the reason with CompleteJob. | |||
| 276 | + | // | |||
| 277 | + | // The server does not trust the runner to obey. If no conclusion arrives | |||
| 278 | + | // within 60 seconds after a cancellation request, whatever the reason (user, | |||
| 279 | + | // fail-fast, timeout), the server ends the job itself: CI_CONCLUSION_CANCELLED | |||
| 280 | + | // for user and fail-fast, CI_CONCLUSION_TIMED_OUT for timeout, with an error | |||
| 281 | + | // saying that the runner did not answer, and the steps not completed are | |||
| 282 | + | // completed as cancelled. The runner's later CompleteJob returns that result. | |||
| 283 | + | rpc WatchJob(WatchJobRequest) returns (stream WatchJobResponse); | |||
| 284 | + | // CompleteJob ends a job: it reports the final steps, the outputs and the | |||
| 285 | + | // conclusion. Needs the job token, which stays valid for this RPC for 24 | |||
| 286 | + | // hours after the end of the job, however it ended. The call is applied whole | |||
| 287 | + | // or not at all. Once the job has ended, the token works for no other RPC | |||
| 288 | + | // (they answer failed_precondition with a JobEnded detail). | |||
| 289 | + | // | |||
| 290 | + | // It is idempotent. The first call that finds the job running ends it and | |||
| 291 | + | // answers applied = true. Any later call, a retry after a lost response or a | |||
| 292 | + | // call after the server ended the job itself, is not validated and changes | |||
| 293 | + | // nothing: it answers OK with the stored conclusion and error and | |||
| 294 | + | // applied = false (a runner whose conclusion differs from the stored one | |||
| 295 | + | // simply logs it; the outputs it sent are dropped). | |||
| 296 | + | // | |||
| 297 | + | // Lines sent with AppendLog after this call are rejected: a runner flushes | |||
| 298 | + | // all its log lines first. Steps that were not reported COMPLETED when the job | |||
| 299 | + | // 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). | |||
| 301 | + | // | |||
| 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. | |||
| 304 | + | rpc CompleteJob(CompleteJobRequest) returns (CompleteJobResponse); | |||
| 305 | + | } | |||
| 306 | + | ||||
| 307 | + | // RegisterRunnerRequest describes the runner to register. | |||
| 308 | + | message RegisterRunnerRequest { | |||
| 309 | + | // Name identifies the runner for people (job page, logs, ListRunners) and is | |||
| 310 | + | // unique: 1 to 64 characters among letters, digits, ".", "_" and "-". It is | |||
| 311 | + | // not a label and never matches runs-on. | |||
| 312 | + | string name = 1; | |||
| 313 | + | // Labels are the labels the runner adds to its implicit ones: at most 64, | |||
| 314 | + | // each 1 to 64 characters among letters, digits, ".", "_" and "-", for | |||
| 315 | + | // example "ubuntu-latest". Comparison is case-insensitive and duplicates | |||
| 316 | + | // are removed. The server adds the implicit labels "self-hosted", the os | |||
| 317 | + | // and the arch ("linux", "x64" or "arm64"); the runner name is not a label. | |||
| 318 | + | repeated string labels = 2; | |||
| 319 | + | // Version is the release of the runner, for example "v0.1.0", or "devel". | |||
| 320 | + | // Informational, at most 64 characters. | |||
| 321 | + | string version = 3; | |||
| 322 | + | // Isolation is how the runner isolates jobs. Required. | |||
| 323 | + | RunnerIsolation isolation = 4; | |||
| 324 | + | // Arch is the CPU architecture, in GitHub's names: "x64" or "arm64". Other | |||
| 325 | + | // values are invalid_argument (the set may grow with compatible additions). | |||
| 326 | + | string arch = 5; | |||
| 327 | + | // Os is the operating system: "linux". Other values are invalid_argument | |||
| 328 | + | // (the set may grow with compatible additions). | |||
| 329 | + | string os = 6; | |||
| 330 | + | // Replace removes the runner that already has this name, if any, before | |||
| 331 | + | // registering this one: its runner token is revoked at once, as in | |||
| 332 | + | // RunService.RemoveRunner, and a job it is running is not affected. The new | |||
| 333 | + | // runner gets a new runner_id. Needs the same scope as registering. Without | |||
| 334 | + | // replace, a name in use is already_exists. It is how a runner re-registers | |||
| 335 | + | // after losing its state. | |||
| 336 | + | bool replace = 7; | |||
| 337 | + | } | |||
| 338 | + | ||||
| 339 | + | // RegisterRunnerResponse holds the new runner and its secret. | |||
| 340 | + | message RegisterRunnerResponse { | |||
| 341 | + | // RunnerId identifies the runner in ListRunners and RemoveRunner. It is not | |||
| 342 | + | // a secret. | |||
| 343 | + | string runner_id = 1; | |||
| 344 | + | // RunnerToken is the secret of the runner, to present as "Authorization: | |||
| 345 | + | // Bearer <runner_token>" to FetchJob: "ermiterun_" and 43 base64url | |||
| 346 | + | // characters (32 random bytes, no padding). It is returned once: the server | |||
| 347 | + | // keeps only its SHA-256, the runner must store it (mode 0600). It stops | |||
| 348 | + | // working when the runner is removed or replaced. | |||
| 349 | + | string runner_token = 2; | |||
| 350 | + | } | |||
| 351 | + | ||||
| 352 | + | // FetchJobRequest asks for a job. | |||
| 353 | + | message FetchJobRequest { | |||
| 354 | + | // Labels narrows the labels offered for this call. Empty means all the | |||
| 355 | + | // labels of the runner, which is the normal case. Otherwise each one must be | |||
| 356 | + | // a label of the runner (invalid_argument if not) and only jobs whose runs-on | |||
| 357 | + | // labels are all among them are offered. | |||
| 358 | + | repeated string labels = 1; | |||
| 359 | + | // PollId identifies this poll, chosen by the client (for example 16 random | |||
| 360 | + | // bytes in base64url), 1 to 64 characters among letters, digits, "_" and | |||
| 361 | + | // "-". A client that retries a call whose response it did not get sends the | |||
| 362 | + | // same poll_id, and gets the assignment the first call made, if it made one | |||
| 363 | + | // (see FetchJob). A new poll uses a new id. Empty means no retry protection: | |||
| 364 | + | // a lost assignment is then recovered by the acknowledgement timeout alone. | |||
| 365 | + | string poll_id = 2; | |||
| 366 | + | } | |||
| 367 | + | ||||
| 368 | + | // FetchJobResponse holds the job given to the runner, if any. | |||
| 369 | + | message FetchJobResponse { | |||
| 370 | + | // Job is the job to run. Unset when no job was available before the end of | |||
| 371 | + | // the poll. | |||
| 372 | + | Job job = 1; | |||
| 373 | + | // RetryAfter is how long the client waits before its next FetchJob. Unset or | |||
| 374 | + | // zero means call again at once. The server sets it when it has no job for | |||
| 375 | + | // the runner and wants to slow polling down (load, shutdown in progress); | |||
| 376 | + | // clients obey it and may wait longer, never less. Unset when job is set. | |||
| 377 | + | google.protobuf.Duration retry_after = 2; | |||
| 378 | + | } | |||
| 379 | + | ||||
| 380 | + | // Job is a job given to a runner: everything needed to run it, nothing more. | |||
| 381 | + | // The runner parses workflow_yaml itself; the protocol does not carry the | |||
| 382 | + | // workflow model, so it does not change when the model grows. | |||
| 383 | + | message Job { | |||
| 384 | + | // JobId identifies the job on the server. It is opaque, unique across | |||
| 385 | + | // repositories and runs, and the same for every attempt of the job. With | |||
| 386 | + | // attempt it names one execution. | |||
| 387 | + | string job_id = 1; | |||
| 388 | + | // RunId is the run the job belongs to, the "github.run_id". It is global to | |||
| 389 | + | // the server. | |||
| 390 | + | int64 run_id = 2; | |||
| 391 | + | // Attempt is the attempt of the run this job executes: 1 for the first, and | |||
| 392 | + | // one more after each RunService.RerunRun. It is the "github.run_attempt". | |||
| 393 | + | int32 attempt = 3; | |||
| 394 | + | // JobToken is the secret that authenticates this job's UpdateJob, | |||
| 395 | + | // AppendLog, Heartbeat, WatchJob and CompleteJob calls ("Authorization: | |||
| 396 | + | // Bearer <job_token>"): "ermitejob_" and 43 base64url characters (32 random | |||
| 397 | + | // bytes, no padding). It is random, valid from the assignment until the job | |||
| 398 | + | // ends (and for CompleteJob 24 hours more), survives a server restart and | |||
| 399 | + | // is shown only in this response, or in a repeat of the same poll_id. It | |||
| 400 | + | // must stay in the runner host process and never reach the job's code. A new | |||
| 401 | + | // assignment of the same job has a new token. | |||
| 402 | + | string job_token = 4; | |||
| 403 | + | // WorkflowPath is the path of the workflow file in the repository, for | |||
| 404 | + | // example ".github/workflows/ci.yml". | |||
| 405 | + | string workflow_path = 5; | |||
| 406 | + | // WorkflowYaml is the content of that file at sha, as is, at most 1 MiB | |||
| 407 | + | // (larger files fail at planning). | |||
| 408 | + | string workflow_yaml = 6; | |||
| 409 | + | // JobKey is the key of the job under "jobs:" in the workflow, the | |||
| 410 | + | // "github.job". | |||
| 411 | + | string job_key = 7; | |||
| 412 | + | // Matrix is the JSON object of the matrix values of this instance of the | |||
| 413 | + | // job, for example {"os":"linux","go":"1.27"}. "{}" when the job has no | |||
| 414 | + | // matrix. It is the "matrix" context. | |||
| 415 | + | string matrix = 8; | |||
| 416 | + | // Contexts is a JSON object with the contexts that the server computed for | |||
| 417 | + | // this job: "github", "needs", "strategy", "matrix", "inputs" and "vars", in | |||
| 418 | + | // the shape GitHub gives them. The contexts "job", "runner", "steps" and | |||
| 419 | + | // "env" belong to the runner and are not in it. Other keys may be added | |||
| 420 | + | // later; clients ignore those they do not know. | |||
| 421 | + | string contexts = 9; | |||
| 422 | + | // Labels are the runs-on labels of the job, evaluated. | |||
| 423 | + | repeated string labels = 10; | |||
| 424 | + | // TimeoutMinutes is the timeout of the job in minutes: the value of the | |||
| 425 | + | // workflow's timeout-minutes, or 360 when it has none. At least 1. The | |||
| 426 | + | // runner enforces it from the moment it receives the job. The server counts | |||
| 427 | + | // from the moment of the assignment and adds 60 seconds of slack, so the | |||
| 428 | + | // runner's own timeout always comes first: when the server's deadline | |||
| 429 | + | // passes it announces CANCEL_REASON_TIMEOUT, and ends the job itself after | |||
| 430 | + | // 60 more seconds without a conclusion (see WatchJob). | |||
| 431 | + | int32 timeout_minutes = 11; | |||
| 432 | + | // Repo is the name of the repository. | |||
| 433 | + | string repo = 12; | |||
| 434 | + | // Sha is the full id of the commit the run is for, 40 lowercase | |||
| 435 | + | // hexadecimal characters for SHA-1. The runner reads the code at it. | |||
| 436 | + | string sha = 13; | |||
| 437 | + | // Ref is the full ref name that triggered the run, for example | |||
| 438 | + | // "refs/heads/main" or "refs/tags/v1.0.0". | |||
| 439 | + | string ref = 14; | |||
| 440 | + | } | |||
| 441 | + | ||||
| 442 | + | // UpdateJobRequest reports the steps of a running job. | |||
| 443 | + | message UpdateJobRequest { | |||
| 444 | + | // JobId is the Job.job_id. It must be the job of the token. | |||
| 445 | + | string job_id = 1; | |||
| 446 | + | // Steps are the steps whose state changed or that are new. Each one is a | |||
| 447 | + | // full snapshot of its step, applied by number. At most 1,000 steps per job | |||
| 448 | + | // (invalid_argument beyond). Steps are numbered like GitHub's log: the | |||
| 449 | + | // implicit "Set up job" is 1, the steps of the workflow follow in order, and | |||
| 450 | + | // the implicit "Complete job" is last. | |||
| 451 | + | repeated StepUpdate steps = 2; | |||
| 452 | + | } | |||
| 453 | + | ||||
| 454 | + | // StepUpdate is the state of one step as the runner knows it. | |||
| 455 | + | message StepUpdate { | |||
| 456 | + | // Number is the 1-based number of the step in the job. It is the number | |||
| 457 | + | // used by AppendLog. | |||
| 458 | + | int32 number = 1; | |||
| 459 | + | // Name is the display name of the step, evaluated: the "name" of the | |||
| 460 | + | // workflow step, or "Run <first line of the script>", as GitHub shows it. | |||
| 461 | + | // At most 1,024 characters; the server cuts longer names. | |||
| 462 | + | string name = 2; | |||
| 463 | + | // State is where the step is. | |||
| 464 | + | StepState state = 3; | |||
| 465 | + | // Conclusion is how the step ended. Required (not UNSPECIFIED) when state is | |||
| 466 | + | // COMPLETED, must be UNSPECIFIED otherwise. Allowed values: success, | |||
| 467 | + | // failure, cancelled, skipped. | |||
| 468 | + | CiConclusion conclusion = 4; | |||
| 469 | + | // StartedAt is when the step started, by the clock of the runner. Required | |||
| 470 | + | // from IN_PROGRESS on. | |||
| 471 | + | google.protobuf.Timestamp started_at = 5; | |||
| 472 | + | // CompletedAt is when the step ended, by the clock of the runner. Required | |||
| 473 | + | // when state is COMPLETED. | |||
| 474 | + | google.protobuf.Timestamp completed_at = 6; | |||
| 475 | + | } | |||
| 476 | + | ||||
| 477 | + | // UpdateJobResponse is empty. | |||
| 478 | + | message UpdateJobResponse {} | |||
| 479 | + | ||||
| 480 | + | // CompleteJobRequest ends a job. | |||
| 481 | + | message CompleteJobRequest { | |||
| 482 | + | // JobId is the Job.job_id. It must be the job of the token. | |||
| 483 | + | string job_id = 1; | |||
| 484 | + | // Conclusion is how the job ended. Required: a runner reports | |||
| 485 | + | // CI_CONCLUSION_SUCCESS, CI_CONCLUSION_FAILURE, CI_CONCLUSION_CANCELLED or | |||
| 486 | + | // CI_CONCLUSION_TIMED_OUT; any other value is invalid_argument. Skipped is | |||
| 487 | + | // decided by the server and never reported by a runner. | |||
| 488 | + | CiConclusion conclusion = 2; | |||
| 489 | + | // Outputs are the outputs of the job, evaluated by the runner. Names are 1 | |||
| 490 | + | // to 255 characters among letters, digits, "_" and "-" and begin with a | |||
| 491 | + | // letter or "_" and are case-insensitive: two names that differ only by case | |||
| 492 | + | // are invalid_argument. Each value is at most 1 MiB, and all names and | |||
| 493 | + | // values together at most 1 MiB (1,048,576 bytes); beyond that the whole call | |||
| 494 | + | // is rejected with invalid_argument and the runner reports the failure of | |||
| 495 | + | // the job without outputs, with a line in the log. | |||
| 496 | + | map<string, string> outputs = 3; | |||
| 497 | + | // RunnerShutdown is true when the runner ends the job because it was told to | |||
| 498 | + | // stop (service stopped, signal). Only allowed with CI_CONCLUSION_CANCELLED. | |||
| 499 | + | // The server then annotates the job with GitHub's message "The runner has | |||
| 500 | + | // received a shutdown signal. This can happen when the runner service is | |||
| 501 | + | // stopped, or a manually started runner is canceled." The conclusion | |||
| 502 | + | // CANCELLED for this case is to be verified against a recording of GitHub. | |||
| 503 | + | bool runner_shutdown = 4; | |||
| 504 | + | // Steps are the final steps, with the same meaning as UpdateJobRequest.steps, | |||
| 505 | + | // applied in the same atomic step as the conclusion. | |||
| 506 | + | repeated StepUpdate steps = 5; | |||
| 507 | + | } | |||
| 508 | + | ||||
| 509 | + | // CompleteJobResponse is the stored result of a job. | |||
| 510 | + | message CompleteJobResponse { | |||
| 511 | + | // Conclusion is the conclusion of the job as the server stored it. It is the | |||
| 512 | + | // one of the request when applied is true. | |||
| 513 | + | CiConclusion conclusion = 1; | |||
| 514 | + | // Error is the error text stored with the job, as RunJob.error. Empty when | |||
| 515 | + | // there is none. | |||
| 516 | + | string error = 2; | |||
| 517 | + | // Applied is true when this call ended the job, false when the job had | |||
| 518 | + | // already ended (a retry, or the server ended it first) and nothing changed. | |||
| 519 | + | bool applied = 3; | |||
| 520 | + | } | |||
| 521 | + | ||||
| 522 | + | // JobEnded says that a job has ended. It is sent in WatchJob and attached, as | |||
| 523 | + | // an error detail, to the failed_precondition errors that RunnerService returns | |||
| 524 | + | // for a job that has ended. | |||
| 525 | + | message JobEnded { | |||
| 526 | + | // Conclusion is how the job ended. | |||
| 527 | + | CiConclusion conclusion = 1; | |||
| 528 | + | // Error is the error text stored with the job, as RunJob.error. Empty when | |||
| 529 | + | // there is none. | |||
| 530 | + | string error = 2; | |||
| 531 | + | } | |||
| 532 | + | ||||
| 533 | + | // AppendLogRequest carries log lines of one step. | |||
| 534 | + | message AppendLogRequest { | |||
| 535 | + | // JobId is the Job.job_id. It must be the job of the token. | |||
| 536 | + | string job_id = 1; | |||
| 537 | + | // Step is the number of the step the lines belong to (see | |||
| 538 | + | // UpdateJobRequest.steps), at least 1, and at least the step of the last | |||
| 539 | + | // stored line. A step that UpdateJob has not announced yet is created by the | |||
| 540 | + | // first lines. See the rules on AppendLog. | |||
| 541 | + | int32 step = 2; | |||
| 542 | + | // Offset is the number of the first line of the request in the log of the | |||
| 543 | + | // job, counted from 0 across all steps. See the rules on AppendLog. | |||
| 544 | + | int64 offset = 3; | |||
| 545 | + | // Lines are the lines to append, in order: at most 1,000, at most 64 KiB of | |||
| 546 | + | // text in total. | |||
| 547 | + | repeated LogLine lines = 4; | |||
| 548 | + | } | |||
| 549 | + | ||||
| 550 | + | // LogLine is one line of a job log. | |||
| 551 | + | message LogLine { | |||
| 552 | + | // Time is when the line was produced, by the clock of the runner. The server | |||
| 553 | + | // keeps it with nanosecond precision (RFC 3339) and does not reorder lines. | |||
| 554 | + | google.protobuf.Timestamp time = 1; | |||
| 555 | + | // Text is the line without its line terminator, valid UTF-8 (the client | |||
| 556 | + | // replaces invalid bytes with U+FFFD). It must not contain a line feed; a | |||
| 557 | + | // carriage return is kept. It is the line as GitHub's raw log has it, | |||
| 558 | + | // including "##[group]" and "##[endgroup]" markers and "##[error]" lines. | |||
| 559 | + | string text = 2; | |||
| 560 | + | // Continued is true when this line is the continuation of the line before | |||
| 561 | + | // it, which was too long to send whole. A runner cuts a line of more than | |||
| 562 | + | // 64 KiB into pieces at UTF-8 character boundaries, each piece of at most | |||
| 563 | + | // 64 KiB, and sets continued on every piece but the first. Each piece is one | |||
| 564 | + | // line for numbering and offsets; the server and the raw log join the | |||
| 565 | + | // pieces back, so the original line is rebuilt exactly. The first line of a | |||
| 566 | + | // job cannot be continued (invalid_argument) and a continued line is in | |||
| 567 | + | // the same step as the line before it (failed_precondition otherwise). | |||
| 568 | + | bool continued = 3; | |||
| 569 | + | } | |||
| 570 | + | ||||
| 571 | + | // AppendLogResponse tells how much of the log the server holds. | |||
| 572 | + | message AppendLogResponse { | |||
| 573 | + | // NextOffset is the number of lines of the job log after the call, which is | |||
| 574 | + | // the offset of the next request. | |||
| 575 | + | int64 next_offset = 1; | |||
| 576 | + | } | |||
| 577 | + | ||||
| 578 | + | // HeartbeatRequest names the job that is alive. | |||
| 579 | + | message HeartbeatRequest { | |||
| 580 | + | // JobId is the Job.job_id. It must be the job of the token. | |||
| 581 | + | string job_id = 1; | |||
| 582 | + | } | |||
| 583 | + | ||||
| 584 | + | // HeartbeatResponse is empty. | |||
| 585 | + | message HeartbeatResponse {} | |||
| 586 | + | ||||
| 587 | + | // WatchJobRequest names the job to watch. | |||
| 588 | + | message WatchJobRequest { | |||
| 589 | + | // JobId is the Job.job_id. It must be the job of the token. | |||
| 590 | + | string job_id = 1; | |||
| 591 | + | } | |||
| 592 | + | ||||
| 593 | + | // WatchJobResponse is one message of the stream. | |||
| 594 | + | message WatchJobResponse { | |||
| 595 | + | // Msg is what the message carries. New kinds may be added: a client ignores | |||
| 596 | + | // the ones it does not know. | |||
| 597 | + | oneof msg { | |||
| 598 | + | // Keepalive: nothing to do, the stream is alive. | |||
| 599 | + | Keepalive keepalive = 1; | |||
| 600 | + | // Cancel: the job must stop. | |||
| 601 | + | CancelRequest cancel = 2; | |||
| 602 | + | // Ended: the job has ended. The last message of a stream: a close without | |||
| 603 | + | // it means the client reconnects. | |||
| 604 | + | JobEnded ended = 3; | |||
| 605 | + | } | |||
| 606 | + | } | |||
| 607 | + | ||||
| 608 | + | // Keepalive has no fields. | |||
| 609 | + | message Keepalive {} | |||
| 610 | + | ||||
| 611 | + | // CancelRequest asks the runner to stop a job. | |||
| 612 | + | message CancelRequest { | |||
| 613 | + | // Reason is why the job must stop. New reasons may be added; a runner that | |||
| 614 | + | // does not know one cancels the job and reports CI_CONCLUSION_CANCELLED. | |||
| 615 | + | CancelReason reason = 1; | |||
| 616 | + | // Message is a short human-readable explanation, for the job log, for | |||
| 617 | + | // example "Canceled by the owner". At most 1,024 characters. | |||
| 618 | + | string message = 2; | |||
| 619 | + | // RequestedAt is when the cancellation was requested, by the clock of the | |||
| 620 | + | // server. | |||
| 621 | + | google.protobuf.Timestamp requested_at = 3; | |||
| 622 | + | } | |||
M proto/ermite/server.proto +5 -1
| @@ -31,7 +31,11 @@ message GetVersionResponse { | |||
| 31 | 31 | // that this server implements, for example "repo.update" or | |
| 32 | 32 | // "events.resume". A client that needs a capability missing from this list | |
| 33 | 33 | // should warn instead of failing on an unimplemented RPC or ignored field. | |
| 34 | - | // Identifiers are lowercase, dot-separated and never reused. | |
| 34 | + | // Identifiers are lowercase, dot-separated and never reused. The CI | |
| 35 | + | // identifiers are "ci.runs" (RunService runs, jobs, logs, checks, and the | |
| 36 | + | // run_updated and job_updated events), "ci.runs.filters" (ListRuns honours | |
| 37 | + | // its filters) and "ci.runners" (RunnerService, and RunService.ListRunners | |
| 38 | + | // and RemoveRunner). | |
| 35 | 39 | repeated string features = 2; | |
| 36 | 40 | } | |
| 37 | 41 | ||
| @@ -31,7 +31,11 @@ message GetVersionResponse { | |||||
| 31 | // that this server implements, for example "repo.update" or | 31 | // that this server implements, for example "repo.update" or | ||
| 32 | // "events.resume". A client that needs a capability missing from this list | 32 | // "events.resume". A client that needs a capability missing from this list | ||
| 33 | // should warn instead of failing on an unimplemented RPC or ignored field. | 33 | // should warn instead of failing on an unimplemented RPC or ignored field. | ||
| 34 | - | // Identifiers are lowercase, dot-separated and never reused. | 34 | + | // Identifiers are lowercase, dot-separated and never reused. The CI |
| 35 | + | // identifiers are "ci.runs" (RunService runs, jobs, logs, checks, and the | |||
| 36 | + | // run_updated and job_updated events), "ci.runs.filters" (ListRuns honours | |||
| 37 | + | // its filters) and "ci.runners" (RunnerService, and RunService.ListRunners | |||
| 38 | + | // and RemoveRunner). | |||
| 35 | repeated string features = 2; | 39 | repeated string features = 2; | ||
| 36 | } | 40 | } | ||
| 37 | 41 | ||||