feat(api): add the CI runner and run services
RunnerService (RegisterRunner, FetchJob, UpdateJob, AppendLog, Heartbeat, WatchJob) for runners, RunService (runs, jobs, logs, checks, runners) for owners, RunUpdated and JobUpdated events, and the ci.runs, ci.runs.filters and ci.runners feature ids.
- commit
- 8b008393b6b54aec6b901b0225d235bd366f80c5
- author
- brice · · 2026-10-12 14:08 UTC
- parent
- 86d856e
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
13 files · +6006 -32
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); 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); 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 +18 -2
| @@ -49,7 +49,15 @@ 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). A job update is published | |
| 60 | + | // before the update of its run. There is no event for a change of title or | |
| 53 | 61 | // description, nor for a resolved comment: after a gap, or to see such | |
| 54 | 62 | // changes, clients read the merge request again. A merge publishes | |
| 55 | 63 | // merge_request_merged, then pushed for the target branch. | |
| @@ -111,7 +119,15 @@ type EventServiceHandler interface { | |||
| 111 | 119 | // for merge requests, merge_request_opened, merge_request_merged, | |
| 112 | 120 | // merge_request_closed and comment_added. Each one is delivered on the | |
| 113 | 121 | // "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 | |
| 122 | + | // visibility of the repository. | |
| 123 | + | // | |
| 124 | + | // Servers with the feature "ci.runs" also publish run_updated and | |
| 125 | + | // job_updated, only on the "repo:<name>" topic of their repository (never on | |
| 126 | + | // "home"), subject to the same visibility. They carry the state after the | |
| 127 | + | // change and are published when a run or a job is created, changes status | |
| 128 | + | // or conclusion, or when one of its steps starts or ends (never per log | |
| 129 | + | // line: follow a log with RunService.GetJobLog). A job update is published | |
| 130 | + | // before the update of its run. There is no event for a change of title or | |
| 115 | 131 | // description, nor for a resolved comment: after a gap, or to see such | |
| 116 | 132 | // changes, clients read the merge request again. A merge publishes | |
| 117 | 133 | // merge_request_merged, then pushed for the target branch. | |
| @@ -49,7 +49,15 @@ 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). A job update is published | |||
| 60 | + | // before the update of its run. There is no event for a change of title or | |||
| 53 | // description, nor for a resolved comment: after a gap, or to see such | 61 | // description, nor for a resolved comment: after a gap, or to see such | ||
| 54 | // changes, clients read the merge request again. A merge publishes | 62 | // changes, clients read the merge request again. A merge publishes | ||
| 55 | // merge_request_merged, then pushed for the target branch. | 63 | // merge_request_merged, then pushed for the target branch. | ||
| @@ -111,7 +119,15 @@ type EventServiceHandler interface { | |||||
| 111 | // for merge requests, merge_request_opened, merge_request_merged, | 119 | // for merge requests, merge_request_opened, merge_request_merged, | ||
| 112 | // merge_request_closed and comment_added. Each one is delivered on the | 120 | // 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 | 121 | // "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 | 122 | + | // visibility of the repository. |
| 123 | + | // | |||
| 124 | + | // Servers with the feature "ci.runs" also publish run_updated and | |||
| 125 | + | // job_updated, only on the "repo:<name>" topic of their repository (never on | |||
| 126 | + | // "home"), subject to the same visibility. They carry the state after the | |||
| 127 | + | // change and are published when a run or a job is created, changes status | |||
| 128 | + | // or conclusion, or when one of its steps starts or ends (never per log | |||
| 129 | + | // line: follow a log with RunService.GetJobLog). A job update is published | |||
| 130 | + | // before the update of its run. There is no event for a change of title or | |||
| 115 | // description, nor for a resolved comment: after a gap, or to see such | 131 | // description, nor for a resolved comment: after a gap, or to see such | ||
| 116 | // changes, clients read the merge request again. A merge publishes | 132 | // changes, clients read the merge request again. A merge publishes | ||
| 117 | // merge_request_merged, then pushed for the target branch. | 133 | // 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 +415 -0
| @@ -0,0 +1,415 @@ | |||
| 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 | + | ) | |
| 52 | + | ||
| 53 | + | // RunnerServiceClient is a client for the ermite.RunnerService service. | |
| 54 | + | type RunnerServiceClient interface { | |
| 55 | + | // RegisterRunner registers a runner and returns its runner token, which is | |
| 56 | + | // shown only in this response; the runner keeps it. The caller needs the | |
| 57 | + | // runner:register scope on a token that is not restricted to repositories | |
| 58 | + | // (a runner serves every repository), or the root token. Fails with | |
| 59 | + | // unauthenticated without a valid token, permission_denied without the | |
| 60 | + | // scope or with a repository-restricted token, invalid_argument if a field | |
| 61 | + | // breaks a rule, and already_exists if name is taken by another runner. | |
| 62 | + | RegisterRunner(context.Context, *connect.Request[ermite.RegisterRunnerRequest]) (*connect.Response[ermite.RegisterRunnerResponse], error) | |
| 63 | + | // FetchJob asks for a job to run (long poll). The server holds the call | |
| 64 | + | // until a queued job matches the runner, or about 50 seconds (never more | |
| 65 | + | // than 50) pass, then answers; without a job the response has no job and the | |
| 66 | + | // runner calls again at once. Clients set a call timeout above 50 seconds | |
| 67 | + | // (60 is suggested) and must not poll when they have no free capacity. | |
| 68 | + | // | |
| 69 | + | // A job matches when every label of its runs-on is among the labels of the | |
| 70 | + | // runner (case-insensitive). A job is given to exactly one runner, even when | |
| 71 | + | // many runners and many polls compete; the assignment is recorded before | |
| 72 | + | // the response is sent, and the job is then in progress. If the response is | |
| 73 | + | // lost the job is not offered again: with no Heartbeat for 2 minutes the | |
| 74 | + | // server fails it as a lost runner. | |
| 75 | + | // | |
| 76 | + | // Needs the runner token. Fails with unauthenticated when the runner was | |
| 77 | + | // removed (also ending a call in progress) and invalid_argument if labels | |
| 78 | + | // breaks a rule. | |
| 79 | + | FetchJob(context.Context, *connect.Request[ermite.FetchJobRequest]) (*connect.Response[ermite.FetchJobResponse], error) | |
| 80 | + | // UpdateJob reports the state of the steps of a job and, once, how the job | |
| 81 | + | // ended. Needs the job token. Each call is applied whole or not at all, and | |
| 82 | + | // repeating an identical call changes nothing, so a runner may retry after a | |
| 83 | + | // network error. | |
| 84 | + | // | |
| 85 | + | // Fails with invalid_argument if a rule is broken (including the output | |
| 86 | + | // limits), and with failed_precondition if a step goes back (a COMPLETED | |
| 87 | + | // step cannot change, a step cannot go from IN_PROGRESS to PENDING). | |
| 88 | + | // The call that carries a conclusion ends the job: its job token stops | |
| 89 | + | // working, the server completes the job and no further call is accepted, | |
| 90 | + | // so a retry of that last call after a lost response gets unauthenticated, | |
| 91 | + | // which means it was applied. | |
| 92 | + | UpdateJob(context.Context, *connect.Request[ermite.UpdateJobRequest]) (*connect.Response[ermite.UpdateJobResponse], error) | |
| 93 | + | // AppendLog appends lines to the log of a job. Needs the job token. | |
| 94 | + | // | |
| 95 | + | // The log of a job is one sequence of lines, numbered from 0 across all | |
| 96 | + | // steps; offset is the number of lines the server holds when the request is | |
| 97 | + | // applied, that is the number of the first line of the request. Calls must | |
| 98 | + | // go in order, one at a time, and are idempotent: | |
| 99 | + | // | |
| 100 | + | // - offset equal to the server's line count: the lines are appended; | |
| 101 | + | // - offset below it: the first (count - offset) lines were already stored | |
| 102 | + | // (a retry after a lost response) and are skipped, and the rest is | |
| 103 | + | // appended; a request entirely stored changes nothing; | |
| 104 | + | // - offset above it: out_of_range (a gap is a bug of the client; the | |
| 105 | + | // message gives the line count the server holds). | |
| 106 | + | // | |
| 107 | + | // The response gives the line count after the call, so a client can check | |
| 108 | + | // itself. A request has at most 1,000 lines whose text adds up to at most | |
| 109 | + | // 64 KiB (65,536 bytes), all in the same step; more is invalid_argument. A | |
| 110 | + | // line of more than 64 KiB is cut by the client into several lines. | |
| 111 | + | // | |
| 112 | + | // The log of a job is limited to 512 MiB (536,870,912 bytes) as stored by | |
| 113 | + | // the server (text, timestamps and separators). The call that would exceed | |
| 114 | + | // it appends nothing; the server adds a last line saying that the limit was | |
| 115 | + | // reached, ends the job with CONCLUSION_FAILURE and RunJob.error set, and | |
| 116 | + | // answers resource_exhausted. The runner then stops the job and sends | |
| 117 | + | // nothing more: its calls get unauthenticated. | |
| 118 | + | // | |
| 119 | + | // Fails with invalid_argument if a rule is broken (step number, text with a | |
| 120 | + | // line feed or invalid UTF-8, missing time). | |
| 121 | + | AppendLog(context.Context, *connect.Request[ermite.AppendLogRequest]) (*connect.Response[ermite.AppendLogResponse], error) | |
| 122 | + | // Heartbeat tells the server that the runner is alive and still working on | |
| 123 | + | // the job. Needs the job token. The runner calls it every 30 seconds for as | |
| 124 | + | // long as the job runs, from assignment to the end. Without a heartbeat for | |
| 125 | + | // 2 minutes the server ends the job with CONCLUSION_FAILURE and the message | |
| 126 | + | // "The self-hosted runner: <name> lost communication with the server...". | |
| 127 | + | // Only Heartbeat counts: other calls do not keep a job alive. | |
| 128 | + | Heartbeat(context.Context, *connect.Request[ermite.HeartbeatRequest]) (*connect.Response[ermite.HeartbeatResponse], error) | |
| 129 | + | // WatchJob streams the requests to stop a job. Needs the job token. The | |
| 130 | + | // runner opens it when it starts the job and keeps it open; it reopens it | |
| 131 | + | // after any error other than unauthenticated. | |
| 132 | + | // | |
| 133 | + | // When the stream opens and a cancellation has already been requested, the | |
| 134 | + | // first message is that request, so a reconnect never misses one. Only the | |
| 135 | + | // first reason is sent: a job already cancelled by the owner does not get a | |
| 136 | + | // second message when its timeout expires. The server also sends a | |
| 137 | + | // keep-alive message (reason CANCEL_REASON_UNSPECIFIED) at least every 30 | |
| 138 | + | // seconds, which the client ignores. The stream ends without error when | |
| 139 | + | // the job ends. The runner stops the job as the official runner does | |
| 140 | + | // (interrupt, then terminate, then kill) and reports the conclusion given | |
| 141 | + | // for the reason with UpdateJob. | |
| 142 | + | WatchJob(context.Context, *connect.Request[ermite.WatchJobRequest]) (*connect.ServerStreamForClient[ermite.WatchJobResponse], error) | |
| 143 | + | } | |
| 144 | + | ||
| 145 | + | // NewRunnerServiceClient constructs a client for the ermite.RunnerService service. By default, it | |
| 146 | + | // uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and sends | |
| 147 | + | // uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or | |
| 148 | + | // connect.WithGRPCWeb() options. | |
| 149 | + | // | |
| 150 | + | // The URL supplied here should be the base URL for the Connect or gRPC server (for example, | |
| 151 | + | // http://api.acme.com or https://acme.com/grpc). | |
| 152 | + | func NewRunnerServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) RunnerServiceClient { | |
| 153 | + | baseURL = strings.TrimRight(baseURL, "/") | |
| 154 | + | runnerServiceMethods := ermite.File_ermite_runner_proto.Services().ByName("RunnerService").Methods() | |
| 155 | + | return &runnerServiceClient{ | |
| 156 | + | registerRunner: connect.NewClient[ermite.RegisterRunnerRequest, ermite.RegisterRunnerResponse]( | |
| 157 | + | httpClient, | |
| 158 | + | baseURL+RunnerServiceRegisterRunnerProcedure, | |
| 159 | + | connect.WithSchema(runnerServiceMethods.ByName("RegisterRunner")), | |
| 160 | + | connect.WithClientOptions(opts...), | |
| 161 | + | ), | |
| 162 | + | fetchJob: connect.NewClient[ermite.FetchJobRequest, ermite.FetchJobResponse]( | |
| 163 | + | httpClient, | |
| 164 | + | baseURL+RunnerServiceFetchJobProcedure, | |
| 165 | + | connect.WithSchema(runnerServiceMethods.ByName("FetchJob")), | |
| 166 | + | connect.WithClientOptions(opts...), | |
| 167 | + | ), | |
| 168 | + | updateJob: connect.NewClient[ermite.UpdateJobRequest, ermite.UpdateJobResponse]( | |
| 169 | + | httpClient, | |
| 170 | + | baseURL+RunnerServiceUpdateJobProcedure, | |
| 171 | + | connect.WithSchema(runnerServiceMethods.ByName("UpdateJob")), | |
| 172 | + | connect.WithClientOptions(opts...), | |
| 173 | + | ), | |
| 174 | + | appendLog: connect.NewClient[ermite.AppendLogRequest, ermite.AppendLogResponse]( | |
| 175 | + | httpClient, | |
| 176 | + | baseURL+RunnerServiceAppendLogProcedure, | |
| 177 | + | connect.WithSchema(runnerServiceMethods.ByName("AppendLog")), | |
| 178 | + | connect.WithClientOptions(opts...), | |
| 179 | + | ), | |
| 180 | + | heartbeat: connect.NewClient[ermite.HeartbeatRequest, ermite.HeartbeatResponse]( | |
| 181 | + | httpClient, | |
| 182 | + | baseURL+RunnerServiceHeartbeatProcedure, | |
| 183 | + | connect.WithSchema(runnerServiceMethods.ByName("Heartbeat")), | |
| 184 | + | connect.WithClientOptions(opts...), | |
| 185 | + | ), | |
| 186 | + | watchJob: connect.NewClient[ermite.WatchJobRequest, ermite.WatchJobResponse]( | |
| 187 | + | httpClient, | |
| 188 | + | baseURL+RunnerServiceWatchJobProcedure, | |
| 189 | + | connect.WithSchema(runnerServiceMethods.ByName("WatchJob")), | |
| 190 | + | connect.WithClientOptions(opts...), | |
| 191 | + | ), | |
| 192 | + | } | |
| 193 | + | } | |
| 194 | + | ||
| 195 | + | // runnerServiceClient implements RunnerServiceClient. | |
| 196 | + | type runnerServiceClient struct { | |
| 197 | + | registerRunner *connect.Client[ermite.RegisterRunnerRequest, ermite.RegisterRunnerResponse] | |
| 198 | + | fetchJob *connect.Client[ermite.FetchJobRequest, ermite.FetchJobResponse] | |
| 199 | + | updateJob *connect.Client[ermite.UpdateJobRequest, ermite.UpdateJobResponse] | |
| 200 | + | appendLog *connect.Client[ermite.AppendLogRequest, ermite.AppendLogResponse] | |
| 201 | + | heartbeat *connect.Client[ermite.HeartbeatRequest, ermite.HeartbeatResponse] | |
| 202 | + | watchJob *connect.Client[ermite.WatchJobRequest, ermite.WatchJobResponse] | |
| 203 | + | } | |
| 204 | + | ||
| 205 | + | // RegisterRunner calls ermite.RunnerService.RegisterRunner. | |
| 206 | + | func (c *runnerServiceClient) RegisterRunner(ctx context.Context, req *connect.Request[ermite.RegisterRunnerRequest]) (*connect.Response[ermite.RegisterRunnerResponse], error) { | |
| 207 | + | return c.registerRunner.CallUnary(ctx, req) | |
| 208 | + | } | |
| 209 | + | ||
| 210 | + | // FetchJob calls ermite.RunnerService.FetchJob. | |
| 211 | + | func (c *runnerServiceClient) FetchJob(ctx context.Context, req *connect.Request[ermite.FetchJobRequest]) (*connect.Response[ermite.FetchJobResponse], error) { | |
| 212 | + | return c.fetchJob.CallUnary(ctx, req) | |
| 213 | + | } | |
| 214 | + | ||
| 215 | + | // UpdateJob calls ermite.RunnerService.UpdateJob. | |
| 216 | + | func (c *runnerServiceClient) UpdateJob(ctx context.Context, req *connect.Request[ermite.UpdateJobRequest]) (*connect.Response[ermite.UpdateJobResponse], error) { | |
| 217 | + | return c.updateJob.CallUnary(ctx, req) | |
| 218 | + | } | |
| 219 | + | ||
| 220 | + | // AppendLog calls ermite.RunnerService.AppendLog. | |
| 221 | + | func (c *runnerServiceClient) AppendLog(ctx context.Context, req *connect.Request[ermite.AppendLogRequest]) (*connect.Response[ermite.AppendLogResponse], error) { | |
| 222 | + | return c.appendLog.CallUnary(ctx, req) | |
| 223 | + | } | |
| 224 | + | ||
| 225 | + | // Heartbeat calls ermite.RunnerService.Heartbeat. | |
| 226 | + | func (c *runnerServiceClient) Heartbeat(ctx context.Context, req *connect.Request[ermite.HeartbeatRequest]) (*connect.Response[ermite.HeartbeatResponse], error) { | |
| 227 | + | return c.heartbeat.CallUnary(ctx, req) | |
| 228 | + | } | |
| 229 | + | ||
| 230 | + | // WatchJob calls ermite.RunnerService.WatchJob. | |
| 231 | + | func (c *runnerServiceClient) WatchJob(ctx context.Context, req *connect.Request[ermite.WatchJobRequest]) (*connect.ServerStreamForClient[ermite.WatchJobResponse], error) { | |
| 232 | + | return c.watchJob.CallServerStream(ctx, req) | |
| 233 | + | } | |
| 234 | + | ||
| 235 | + | // RunnerServiceHandler is an implementation of the ermite.RunnerService service. | |
| 236 | + | type RunnerServiceHandler interface { | |
| 237 | + | // RegisterRunner registers a runner and returns its runner token, which is | |
| 238 | + | // shown only in this response; the runner keeps it. The caller needs the | |
| 239 | + | // runner:register scope on a token that is not restricted to repositories | |
| 240 | + | // (a runner serves every repository), or the root token. Fails with | |
| 241 | + | // unauthenticated without a valid token, permission_denied without the | |
| 242 | + | // scope or with a repository-restricted token, invalid_argument if a field | |
| 243 | + | // breaks a rule, and already_exists if name is taken by another runner. | |
| 244 | + | RegisterRunner(context.Context, *connect.Request[ermite.RegisterRunnerRequest]) (*connect.Response[ermite.RegisterRunnerResponse], error) | |
| 245 | + | // FetchJob asks for a job to run (long poll). The server holds the call | |
| 246 | + | // until a queued job matches the runner, or about 50 seconds (never more | |
| 247 | + | // than 50) pass, then answers; without a job the response has no job and the | |
| 248 | + | // runner calls again at once. Clients set a call timeout above 50 seconds | |
| 249 | + | // (60 is suggested) and must not poll when they have no free capacity. | |
| 250 | + | // | |
| 251 | + | // A job matches when every label of its runs-on is among the labels of the | |
| 252 | + | // runner (case-insensitive). A job is given to exactly one runner, even when | |
| 253 | + | // many runners and many polls compete; the assignment is recorded before | |
| 254 | + | // the response is sent, and the job is then in progress. If the response is | |
| 255 | + | // lost the job is not offered again: with no Heartbeat for 2 minutes the | |
| 256 | + | // server fails it as a lost runner. | |
| 257 | + | // | |
| 258 | + | // Needs the runner token. Fails with unauthenticated when the runner was | |
| 259 | + | // removed (also ending a call in progress) and invalid_argument if labels | |
| 260 | + | // breaks a rule. | |
| 261 | + | FetchJob(context.Context, *connect.Request[ermite.FetchJobRequest]) (*connect.Response[ermite.FetchJobResponse], error) | |
| 262 | + | // UpdateJob reports the state of the steps of a job and, once, how the job | |
| 263 | + | // ended. Needs the job token. Each call is applied whole or not at all, and | |
| 264 | + | // repeating an identical call changes nothing, so a runner may retry after a | |
| 265 | + | // network error. | |
| 266 | + | // | |
| 267 | + | // Fails with invalid_argument if a rule is broken (including the output | |
| 268 | + | // limits), and with failed_precondition if a step goes back (a COMPLETED | |
| 269 | + | // step cannot change, a step cannot go from IN_PROGRESS to PENDING). | |
| 270 | + | // The call that carries a conclusion ends the job: its job token stops | |
| 271 | + | // working, the server completes the job and no further call is accepted, | |
| 272 | + | // so a retry of that last call after a lost response gets unauthenticated, | |
| 273 | + | // which means it was applied. | |
| 274 | + | UpdateJob(context.Context, *connect.Request[ermite.UpdateJobRequest]) (*connect.Response[ermite.UpdateJobResponse], error) | |
| 275 | + | // AppendLog appends lines to the log of a job. Needs the job token. | |
| 276 | + | // | |
| 277 | + | // The log of a job is one sequence of lines, numbered from 0 across all | |
| 278 | + | // steps; offset is the number of lines the server holds when the request is | |
| 279 | + | // applied, that is the number of the first line of the request. Calls must | |
| 280 | + | // go in order, one at a time, and are idempotent: | |
| 281 | + | // | |
| 282 | + | // - offset equal to the server's line count: the lines are appended; | |
| 283 | + | // - offset below it: the first (count - offset) lines were already stored | |
| 284 | + | // (a retry after a lost response) and are skipped, and the rest is | |
| 285 | + | // appended; a request entirely stored changes nothing; | |
| 286 | + | // - offset above it: out_of_range (a gap is a bug of the client; the | |
| 287 | + | // message gives the line count the server holds). | |
| 288 | + | // | |
| 289 | + | // The response gives the line count after the call, so a client can check | |
| 290 | + | // itself. A request has at most 1,000 lines whose text adds up to at most | |
| 291 | + | // 64 KiB (65,536 bytes), all in the same step; more is invalid_argument. A | |
| 292 | + | // line of more than 64 KiB is cut by the client into several lines. | |
| 293 | + | // | |
| 294 | + | // The log of a job is limited to 512 MiB (536,870,912 bytes) as stored by | |
| 295 | + | // the server (text, timestamps and separators). The call that would exceed | |
| 296 | + | // it appends nothing; the server adds a last line saying that the limit was | |
| 297 | + | // reached, ends the job with CONCLUSION_FAILURE and RunJob.error set, and | |
| 298 | + | // answers resource_exhausted. The runner then stops the job and sends | |
| 299 | + | // nothing more: its calls get unauthenticated. | |
| 300 | + | // | |
| 301 | + | // Fails with invalid_argument if a rule is broken (step number, text with a | |
| 302 | + | // line feed or invalid UTF-8, missing time). | |
| 303 | + | AppendLog(context.Context, *connect.Request[ermite.AppendLogRequest]) (*connect.Response[ermite.AppendLogResponse], error) | |
| 304 | + | // Heartbeat tells the server that the runner is alive and still working on | |
| 305 | + | // the job. Needs the job token. The runner calls it every 30 seconds for as | |
| 306 | + | // long as the job runs, from assignment to the end. Without a heartbeat for | |
| 307 | + | // 2 minutes the server ends the job with CONCLUSION_FAILURE and the message | |
| 308 | + | // "The self-hosted runner: <name> lost communication with the server...". | |
| 309 | + | // Only Heartbeat counts: other calls do not keep a job alive. | |
| 310 | + | Heartbeat(context.Context, *connect.Request[ermite.HeartbeatRequest]) (*connect.Response[ermite.HeartbeatResponse], error) | |
| 311 | + | // WatchJob streams the requests to stop a job. Needs the job token. The | |
| 312 | + | // runner opens it when it starts the job and keeps it open; it reopens it | |
| 313 | + | // after any error other than unauthenticated. | |
| 314 | + | // | |
| 315 | + | // When the stream opens and a cancellation has already been requested, the | |
| 316 | + | // first message is that request, so a reconnect never misses one. Only the | |
| 317 | + | // first reason is sent: a job already cancelled by the owner does not get a | |
| 318 | + | // second message when its timeout expires. The server also sends a | |
| 319 | + | // keep-alive message (reason CANCEL_REASON_UNSPECIFIED) at least every 30 | |
| 320 | + | // seconds, which the client ignores. The stream ends without error when | |
| 321 | + | // the job ends. The runner stops the job as the official runner does | |
| 322 | + | // (interrupt, then terminate, then kill) and reports the conclusion given | |
| 323 | + | // for the reason with UpdateJob. | |
| 324 | + | WatchJob(context.Context, *connect.Request[ermite.WatchJobRequest], *connect.ServerStream[ermite.WatchJobResponse]) error | |
| 325 | + | } | |
| 326 | + | ||
| 327 | + | // NewRunnerServiceHandler builds an HTTP handler from the service implementation. It returns the | |
| 328 | + | // path on which to mount the handler and the handler itself. | |
| 329 | + | // | |
| 330 | + | // By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf | |
| 331 | + | // and JSON codecs. They also support gzip compression. | |
| 332 | + | func NewRunnerServiceHandler(svc RunnerServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { | |
| 333 | + | runnerServiceMethods := ermite.File_ermite_runner_proto.Services().ByName("RunnerService").Methods() | |
| 334 | + | runnerServiceRegisterRunnerHandler := connect.NewUnaryHandler( | |
| 335 | + | RunnerServiceRegisterRunnerProcedure, | |
| 336 | + | svc.RegisterRunner, | |
| 337 | + | connect.WithSchema(runnerServiceMethods.ByName("RegisterRunner")), | |
| 338 | + | connect.WithHandlerOptions(opts...), | |
| 339 | + | ) | |
| 340 | + | runnerServiceFetchJobHandler := connect.NewUnaryHandler( | |
| 341 | + | RunnerServiceFetchJobProcedure, | |
| 342 | + | svc.FetchJob, | |
| 343 | + | connect.WithSchema(runnerServiceMethods.ByName("FetchJob")), | |
| 344 | + | connect.WithHandlerOptions(opts...), | |
| 345 | + | ) | |
| 346 | + | runnerServiceUpdateJobHandler := connect.NewUnaryHandler( | |
| 347 | + | RunnerServiceUpdateJobProcedure, | |
| 348 | + | svc.UpdateJob, | |
| 349 | + | connect.WithSchema(runnerServiceMethods.ByName("UpdateJob")), | |
| 350 | + | connect.WithHandlerOptions(opts...), | |
| 351 | + | ) | |
| 352 | + | runnerServiceAppendLogHandler := connect.NewUnaryHandler( | |
| 353 | + | RunnerServiceAppendLogProcedure, | |
| 354 | + | svc.AppendLog, | |
| 355 | + | connect.WithSchema(runnerServiceMethods.ByName("AppendLog")), | |
| 356 | + | connect.WithHandlerOptions(opts...), | |
| 357 | + | ) | |
| 358 | + | runnerServiceHeartbeatHandler := connect.NewUnaryHandler( | |
| 359 | + | RunnerServiceHeartbeatProcedure, | |
| 360 | + | svc.Heartbeat, | |
| 361 | + | connect.WithSchema(runnerServiceMethods.ByName("Heartbeat")), | |
| 362 | + | connect.WithHandlerOptions(opts...), | |
| 363 | + | ) | |
| 364 | + | runnerServiceWatchJobHandler := connect.NewServerStreamHandler( | |
| 365 | + | RunnerServiceWatchJobProcedure, | |
| 366 | + | svc.WatchJob, | |
| 367 | + | connect.WithSchema(runnerServiceMethods.ByName("WatchJob")), | |
| 368 | + | connect.WithHandlerOptions(opts...), | |
| 369 | + | ) | |
| 370 | + | return "/ermite.RunnerService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| 371 | + | switch r.URL.Path { | |
| 372 | + | case RunnerServiceRegisterRunnerProcedure: | |
| 373 | + | runnerServiceRegisterRunnerHandler.ServeHTTP(w, r) | |
| 374 | + | case RunnerServiceFetchJobProcedure: | |
| 375 | + | runnerServiceFetchJobHandler.ServeHTTP(w, r) | |
| 376 | + | case RunnerServiceUpdateJobProcedure: | |
| 377 | + | runnerServiceUpdateJobHandler.ServeHTTP(w, r) | |
| 378 | + | case RunnerServiceAppendLogProcedure: | |
| 379 | + | runnerServiceAppendLogHandler.ServeHTTP(w, r) | |
| 380 | + | case RunnerServiceHeartbeatProcedure: | |
| 381 | + | runnerServiceHeartbeatHandler.ServeHTTP(w, r) | |
| 382 | + | case RunnerServiceWatchJobProcedure: | |
| 383 | + | runnerServiceWatchJobHandler.ServeHTTP(w, r) | |
| 384 | + | default: | |
| 385 | + | http.NotFound(w, r) | |
| 386 | + | } | |
| 387 | + | }) | |
| 388 | + | } | |
| 389 | + | ||
| 390 | + | // UnimplementedRunnerServiceHandler returns CodeUnimplemented from all methods. | |
| 391 | + | type UnimplementedRunnerServiceHandler struct{} | |
| 392 | + | ||
| 393 | + | func (UnimplementedRunnerServiceHandler) RegisterRunner(context.Context, *connect.Request[ermite.RegisterRunnerRequest]) (*connect.Response[ermite.RegisterRunnerResponse], error) { | |
| 394 | + | return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunnerService.RegisterRunner is not implemented")) | |
| 395 | + | } | |
| 396 | + | ||
| 397 | + | func (UnimplementedRunnerServiceHandler) FetchJob(context.Context, *connect.Request[ermite.FetchJobRequest]) (*connect.Response[ermite.FetchJobResponse], error) { | |
| 398 | + | return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunnerService.FetchJob is not implemented")) | |
| 399 | + | } | |
| 400 | + | ||
| 401 | + | func (UnimplementedRunnerServiceHandler) UpdateJob(context.Context, *connect.Request[ermite.UpdateJobRequest]) (*connect.Response[ermite.UpdateJobResponse], error) { | |
| 402 | + | return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunnerService.UpdateJob is not implemented")) | |
| 403 | + | } | |
| 404 | + | ||
| 405 | + | func (UnimplementedRunnerServiceHandler) AppendLog(context.Context, *connect.Request[ermite.AppendLogRequest]) (*connect.Response[ermite.AppendLogResponse], error) { | |
| 406 | + | return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunnerService.AppendLog is not implemented")) | |
| 407 | + | } | |
| 408 | + | ||
| 409 | + | func (UnimplementedRunnerServiceHandler) Heartbeat(context.Context, *connect.Request[ermite.HeartbeatRequest]) (*connect.Response[ermite.HeartbeatResponse], error) { | |
| 410 | + | return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunnerService.Heartbeat is not implemented")) | |
| 411 | + | } | |
| 412 | + | ||
| 413 | + | func (UnimplementedRunnerServiceHandler) WatchJob(context.Context, *connect.Request[ermite.WatchJobRequest], *connect.ServerStream[ermite.WatchJobResponse]) error { | |
| 414 | + | return connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunnerService.WatchJob is not implemented")) | |
| 415 | + | } | |
| @@ -0,0 +1,415 @@ | |||||
| 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 | + | ) | |||
| 52 | + | ||||
| 53 | + | // RunnerServiceClient is a client for the ermite.RunnerService service. | |||
| 54 | + | type RunnerServiceClient interface { | |||
| 55 | + | // RegisterRunner registers a runner and returns its runner token, which is | |||
| 56 | + | // shown only in this response; the runner keeps it. The caller needs the | |||
| 57 | + | // runner:register scope on a token that is not restricted to repositories | |||
| 58 | + | // (a runner serves every repository), or the root token. Fails with | |||
| 59 | + | // unauthenticated without a valid token, permission_denied without the | |||
| 60 | + | // scope or with a repository-restricted token, invalid_argument if a field | |||
| 61 | + | // breaks a rule, and already_exists if name is taken by another runner. | |||
| 62 | + | RegisterRunner(context.Context, *connect.Request[ermite.RegisterRunnerRequest]) (*connect.Response[ermite.RegisterRunnerResponse], error) | |||
| 63 | + | // FetchJob asks for a job to run (long poll). The server holds the call | |||
| 64 | + | // until a queued job matches the runner, or about 50 seconds (never more | |||
| 65 | + | // than 50) pass, then answers; without a job the response has no job and the | |||
| 66 | + | // runner calls again at once. Clients set a call timeout above 50 seconds | |||
| 67 | + | // (60 is suggested) and must not poll when they have no free capacity. | |||
| 68 | + | // | |||
| 69 | + | // A job matches when every label of its runs-on is among the labels of the | |||
| 70 | + | // runner (case-insensitive). A job is given to exactly one runner, even when | |||
| 71 | + | // many runners and many polls compete; the assignment is recorded before | |||
| 72 | + | // the response is sent, and the job is then in progress. If the response is | |||
| 73 | + | // lost the job is not offered again: with no Heartbeat for 2 minutes the | |||
| 74 | + | // server fails it as a lost runner. | |||
| 75 | + | // | |||
| 76 | + | // Needs the runner token. Fails with unauthenticated when the runner was | |||
| 77 | + | // removed (also ending a call in progress) and invalid_argument if labels | |||
| 78 | + | // breaks a rule. | |||
| 79 | + | FetchJob(context.Context, *connect.Request[ermite.FetchJobRequest]) (*connect.Response[ermite.FetchJobResponse], error) | |||
| 80 | + | // UpdateJob reports the state of the steps of a job and, once, how the job | |||
| 81 | + | // ended. Needs the job token. Each call is applied whole or not at all, and | |||
| 82 | + | // repeating an identical call changes nothing, so a runner may retry after a | |||
| 83 | + | // network error. | |||
| 84 | + | // | |||
| 85 | + | // Fails with invalid_argument if a rule is broken (including the output | |||
| 86 | + | // limits), and with failed_precondition if a step goes back (a COMPLETED | |||
| 87 | + | // step cannot change, a step cannot go from IN_PROGRESS to PENDING). | |||
| 88 | + | // The call that carries a conclusion ends the job: its job token stops | |||
| 89 | + | // working, the server completes the job and no further call is accepted, | |||
| 90 | + | // so a retry of that last call after a lost response gets unauthenticated, | |||
| 91 | + | // which means it was applied. | |||
| 92 | + | UpdateJob(context.Context, *connect.Request[ermite.UpdateJobRequest]) (*connect.Response[ermite.UpdateJobResponse], error) | |||
| 93 | + | // AppendLog appends lines to the log of a job. Needs the job token. | |||
| 94 | + | // | |||
| 95 | + | // The log of a job is one sequence of lines, numbered from 0 across all | |||
| 96 | + | // steps; offset is the number of lines the server holds when the request is | |||
| 97 | + | // applied, that is the number of the first line of the request. Calls must | |||
| 98 | + | // go in order, one at a time, and are idempotent: | |||
| 99 | + | // | |||
| 100 | + | // - offset equal to the server's line count: the lines are appended; | |||
| 101 | + | // - offset below it: the first (count - offset) lines were already stored | |||
| 102 | + | // (a retry after a lost response) and are skipped, and the rest is | |||
| 103 | + | // appended; a request entirely stored changes nothing; | |||
| 104 | + | // - offset above it: out_of_range (a gap is a bug of the client; the | |||
| 105 | + | // message gives the line count the server holds). | |||
| 106 | + | // | |||
| 107 | + | // The response gives the line count after the call, so a client can check | |||
| 108 | + | // itself. A request has at most 1,000 lines whose text adds up to at most | |||
| 109 | + | // 64 KiB (65,536 bytes), all in the same step; more is invalid_argument. A | |||
| 110 | + | // line of more than 64 KiB is cut by the client into several lines. | |||
| 111 | + | // | |||
| 112 | + | // The log of a job is limited to 512 MiB (536,870,912 bytes) as stored by | |||
| 113 | + | // the server (text, timestamps and separators). The call that would exceed | |||
| 114 | + | // it appends nothing; the server adds a last line saying that the limit was | |||
| 115 | + | // reached, ends the job with CONCLUSION_FAILURE and RunJob.error set, and | |||
| 116 | + | // answers resource_exhausted. The runner then stops the job and sends | |||
| 117 | + | // nothing more: its calls get unauthenticated. | |||
| 118 | + | // | |||
| 119 | + | // Fails with invalid_argument if a rule is broken (step number, text with a | |||
| 120 | + | // line feed or invalid UTF-8, missing time). | |||
| 121 | + | AppendLog(context.Context, *connect.Request[ermite.AppendLogRequest]) (*connect.Response[ermite.AppendLogResponse], error) | |||
| 122 | + | // Heartbeat tells the server that the runner is alive and still working on | |||
| 123 | + | // the job. Needs the job token. The runner calls it every 30 seconds for as | |||
| 124 | + | // long as the job runs, from assignment to the end. Without a heartbeat for | |||
| 125 | + | // 2 minutes the server ends the job with CONCLUSION_FAILURE and the message | |||
| 126 | + | // "The self-hosted runner: <name> lost communication with the server...". | |||
| 127 | + | // Only Heartbeat counts: other calls do not keep a job alive. | |||
| 128 | + | Heartbeat(context.Context, *connect.Request[ermite.HeartbeatRequest]) (*connect.Response[ermite.HeartbeatResponse], error) | |||
| 129 | + | // WatchJob streams the requests to stop a job. Needs the job token. The | |||
| 130 | + | // runner opens it when it starts the job and keeps it open; it reopens it | |||
| 131 | + | // after any error other than unauthenticated. | |||
| 132 | + | // | |||
| 133 | + | // When the stream opens and a cancellation has already been requested, the | |||
| 134 | + | // first message is that request, so a reconnect never misses one. Only the | |||
| 135 | + | // first reason is sent: a job already cancelled by the owner does not get a | |||
| 136 | + | // second message when its timeout expires. The server also sends a | |||
| 137 | + | // keep-alive message (reason CANCEL_REASON_UNSPECIFIED) at least every 30 | |||
| 138 | + | // seconds, which the client ignores. The stream ends without error when | |||
| 139 | + | // the job ends. The runner stops the job as the official runner does | |||
| 140 | + | // (interrupt, then terminate, then kill) and reports the conclusion given | |||
| 141 | + | // for the reason with UpdateJob. | |||
| 142 | + | WatchJob(context.Context, *connect.Request[ermite.WatchJobRequest]) (*connect.ServerStreamForClient[ermite.WatchJobResponse], error) | |||
| 143 | + | } | |||
| 144 | + | ||||
| 145 | + | // NewRunnerServiceClient constructs a client for the ermite.RunnerService service. By default, it | |||
| 146 | + | // uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and sends | |||
| 147 | + | // uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or | |||
| 148 | + | // connect.WithGRPCWeb() options. | |||
| 149 | + | // | |||
| 150 | + | // The URL supplied here should be the base URL for the Connect or gRPC server (for example, | |||
| 151 | + | // http://api.acme.com or https://acme.com/grpc). | |||
| 152 | + | func NewRunnerServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) RunnerServiceClient { | |||
| 153 | + | baseURL = strings.TrimRight(baseURL, "/") | |||
| 154 | + | runnerServiceMethods := ermite.File_ermite_runner_proto.Services().ByName("RunnerService").Methods() | |||
| 155 | + | return &runnerServiceClient{ | |||
| 156 | + | registerRunner: connect.NewClient[ermite.RegisterRunnerRequest, ermite.RegisterRunnerResponse]( | |||
| 157 | + | httpClient, | |||
| 158 | + | baseURL+RunnerServiceRegisterRunnerProcedure, | |||
| 159 | + | connect.WithSchema(runnerServiceMethods.ByName("RegisterRunner")), | |||
| 160 | + | connect.WithClientOptions(opts...), | |||
| 161 | + | ), | |||
| 162 | + | fetchJob: connect.NewClient[ermite.FetchJobRequest, ermite.FetchJobResponse]( | |||
| 163 | + | httpClient, | |||
| 164 | + | baseURL+RunnerServiceFetchJobProcedure, | |||
| 165 | + | connect.WithSchema(runnerServiceMethods.ByName("FetchJob")), | |||
| 166 | + | connect.WithClientOptions(opts...), | |||
| 167 | + | ), | |||
| 168 | + | updateJob: connect.NewClient[ermite.UpdateJobRequest, ermite.UpdateJobResponse]( | |||
| 169 | + | httpClient, | |||
| 170 | + | baseURL+RunnerServiceUpdateJobProcedure, | |||
| 171 | + | connect.WithSchema(runnerServiceMethods.ByName("UpdateJob")), | |||
| 172 | + | connect.WithClientOptions(opts...), | |||
| 173 | + | ), | |||
| 174 | + | appendLog: connect.NewClient[ermite.AppendLogRequest, ermite.AppendLogResponse]( | |||
| 175 | + | httpClient, | |||
| 176 | + | baseURL+RunnerServiceAppendLogProcedure, | |||
| 177 | + | connect.WithSchema(runnerServiceMethods.ByName("AppendLog")), | |||
| 178 | + | connect.WithClientOptions(opts...), | |||
| 179 | + | ), | |||
| 180 | + | heartbeat: connect.NewClient[ermite.HeartbeatRequest, ermite.HeartbeatResponse]( | |||
| 181 | + | httpClient, | |||
| 182 | + | baseURL+RunnerServiceHeartbeatProcedure, | |||
| 183 | + | connect.WithSchema(runnerServiceMethods.ByName("Heartbeat")), | |||
| 184 | + | connect.WithClientOptions(opts...), | |||
| 185 | + | ), | |||
| 186 | + | watchJob: connect.NewClient[ermite.WatchJobRequest, ermite.WatchJobResponse]( | |||
| 187 | + | httpClient, | |||
| 188 | + | baseURL+RunnerServiceWatchJobProcedure, | |||
| 189 | + | connect.WithSchema(runnerServiceMethods.ByName("WatchJob")), | |||
| 190 | + | connect.WithClientOptions(opts...), | |||
| 191 | + | ), | |||
| 192 | + | } | |||
| 193 | + | } | |||
| 194 | + | ||||
| 195 | + | // runnerServiceClient implements RunnerServiceClient. | |||
| 196 | + | type runnerServiceClient struct { | |||
| 197 | + | registerRunner *connect.Client[ermite.RegisterRunnerRequest, ermite.RegisterRunnerResponse] | |||
| 198 | + | fetchJob *connect.Client[ermite.FetchJobRequest, ermite.FetchJobResponse] | |||
| 199 | + | updateJob *connect.Client[ermite.UpdateJobRequest, ermite.UpdateJobResponse] | |||
| 200 | + | appendLog *connect.Client[ermite.AppendLogRequest, ermite.AppendLogResponse] | |||
| 201 | + | heartbeat *connect.Client[ermite.HeartbeatRequest, ermite.HeartbeatResponse] | |||
| 202 | + | watchJob *connect.Client[ermite.WatchJobRequest, ermite.WatchJobResponse] | |||
| 203 | + | } | |||
| 204 | + | ||||
| 205 | + | // RegisterRunner calls ermite.RunnerService.RegisterRunner. | |||
| 206 | + | func (c *runnerServiceClient) RegisterRunner(ctx context.Context, req *connect.Request[ermite.RegisterRunnerRequest]) (*connect.Response[ermite.RegisterRunnerResponse], error) { | |||
| 207 | + | return c.registerRunner.CallUnary(ctx, req) | |||
| 208 | + | } | |||
| 209 | + | ||||
| 210 | + | // FetchJob calls ermite.RunnerService.FetchJob. | |||
| 211 | + | func (c *runnerServiceClient) FetchJob(ctx context.Context, req *connect.Request[ermite.FetchJobRequest]) (*connect.Response[ermite.FetchJobResponse], error) { | |||
| 212 | + | return c.fetchJob.CallUnary(ctx, req) | |||
| 213 | + | } | |||
| 214 | + | ||||
| 215 | + | // UpdateJob calls ermite.RunnerService.UpdateJob. | |||
| 216 | + | func (c *runnerServiceClient) UpdateJob(ctx context.Context, req *connect.Request[ermite.UpdateJobRequest]) (*connect.Response[ermite.UpdateJobResponse], error) { | |||
| 217 | + | return c.updateJob.CallUnary(ctx, req) | |||
| 218 | + | } | |||
| 219 | + | ||||
| 220 | + | // AppendLog calls ermite.RunnerService.AppendLog. | |||
| 221 | + | func (c *runnerServiceClient) AppendLog(ctx context.Context, req *connect.Request[ermite.AppendLogRequest]) (*connect.Response[ermite.AppendLogResponse], error) { | |||
| 222 | + | return c.appendLog.CallUnary(ctx, req) | |||
| 223 | + | } | |||
| 224 | + | ||||
| 225 | + | // Heartbeat calls ermite.RunnerService.Heartbeat. | |||
| 226 | + | func (c *runnerServiceClient) Heartbeat(ctx context.Context, req *connect.Request[ermite.HeartbeatRequest]) (*connect.Response[ermite.HeartbeatResponse], error) { | |||
| 227 | + | return c.heartbeat.CallUnary(ctx, req) | |||
| 228 | + | } | |||
| 229 | + | ||||
| 230 | + | // WatchJob calls ermite.RunnerService.WatchJob. | |||
| 231 | + | func (c *runnerServiceClient) WatchJob(ctx context.Context, req *connect.Request[ermite.WatchJobRequest]) (*connect.ServerStreamForClient[ermite.WatchJobResponse], error) { | |||
| 232 | + | return c.watchJob.CallServerStream(ctx, req) | |||
| 233 | + | } | |||
| 234 | + | ||||
| 235 | + | // RunnerServiceHandler is an implementation of the ermite.RunnerService service. | |||
| 236 | + | type RunnerServiceHandler interface { | |||
| 237 | + | // RegisterRunner registers a runner and returns its runner token, which is | |||
| 238 | + | // shown only in this response; the runner keeps it. The caller needs the | |||
| 239 | + | // runner:register scope on a token that is not restricted to repositories | |||
| 240 | + | // (a runner serves every repository), or the root token. Fails with | |||
| 241 | + | // unauthenticated without a valid token, permission_denied without the | |||
| 242 | + | // scope or with a repository-restricted token, invalid_argument if a field | |||
| 243 | + | // breaks a rule, and already_exists if name is taken by another runner. | |||
| 244 | + | RegisterRunner(context.Context, *connect.Request[ermite.RegisterRunnerRequest]) (*connect.Response[ermite.RegisterRunnerResponse], error) | |||
| 245 | + | // FetchJob asks for a job to run (long poll). The server holds the call | |||
| 246 | + | // until a queued job matches the runner, or about 50 seconds (never more | |||
| 247 | + | // than 50) pass, then answers; without a job the response has no job and the | |||
| 248 | + | // runner calls again at once. Clients set a call timeout above 50 seconds | |||
| 249 | + | // (60 is suggested) and must not poll when they have no free capacity. | |||
| 250 | + | // | |||
| 251 | + | // A job matches when every label of its runs-on is among the labels of the | |||
| 252 | + | // runner (case-insensitive). A job is given to exactly one runner, even when | |||
| 253 | + | // many runners and many polls compete; the assignment is recorded before | |||
| 254 | + | // the response is sent, and the job is then in progress. If the response is | |||
| 255 | + | // lost the job is not offered again: with no Heartbeat for 2 minutes the | |||
| 256 | + | // server fails it as a lost runner. | |||
| 257 | + | // | |||
| 258 | + | // Needs the runner token. Fails with unauthenticated when the runner was | |||
| 259 | + | // removed (also ending a call in progress) and invalid_argument if labels | |||
| 260 | + | // breaks a rule. | |||
| 261 | + | FetchJob(context.Context, *connect.Request[ermite.FetchJobRequest]) (*connect.Response[ermite.FetchJobResponse], error) | |||
| 262 | + | // UpdateJob reports the state of the steps of a job and, once, how the job | |||
| 263 | + | // ended. Needs the job token. Each call is applied whole or not at all, and | |||
| 264 | + | // repeating an identical call changes nothing, so a runner may retry after a | |||
| 265 | + | // network error. | |||
| 266 | + | // | |||
| 267 | + | // Fails with invalid_argument if a rule is broken (including the output | |||
| 268 | + | // limits), and with failed_precondition if a step goes back (a COMPLETED | |||
| 269 | + | // step cannot change, a step cannot go from IN_PROGRESS to PENDING). | |||
| 270 | + | // The call that carries a conclusion ends the job: its job token stops | |||
| 271 | + | // working, the server completes the job and no further call is accepted, | |||
| 272 | + | // so a retry of that last call after a lost response gets unauthenticated, | |||
| 273 | + | // which means it was applied. | |||
| 274 | + | UpdateJob(context.Context, *connect.Request[ermite.UpdateJobRequest]) (*connect.Response[ermite.UpdateJobResponse], error) | |||
| 275 | + | // AppendLog appends lines to the log of a job. Needs the job token. | |||
| 276 | + | // | |||
| 277 | + | // The log of a job is one sequence of lines, numbered from 0 across all | |||
| 278 | + | // steps; offset is the number of lines the server holds when the request is | |||
| 279 | + | // applied, that is the number of the first line of the request. Calls must | |||
| 280 | + | // go in order, one at a time, and are idempotent: | |||
| 281 | + | // | |||
| 282 | + | // - offset equal to the server's line count: the lines are appended; | |||
| 283 | + | // - offset below it: the first (count - offset) lines were already stored | |||
| 284 | + | // (a retry after a lost response) and are skipped, and the rest is | |||
| 285 | + | // appended; a request entirely stored changes nothing; | |||
| 286 | + | // - offset above it: out_of_range (a gap is a bug of the client; the | |||
| 287 | + | // message gives the line count the server holds). | |||
| 288 | + | // | |||
| 289 | + | // The response gives the line count after the call, so a client can check | |||
| 290 | + | // itself. A request has at most 1,000 lines whose text adds up to at most | |||
| 291 | + | // 64 KiB (65,536 bytes), all in the same step; more is invalid_argument. A | |||
| 292 | + | // line of more than 64 KiB is cut by the client into several lines. | |||
| 293 | + | // | |||
| 294 | + | // The log of a job is limited to 512 MiB (536,870,912 bytes) as stored by | |||
| 295 | + | // the server (text, timestamps and separators). The call that would exceed | |||
| 296 | + | // it appends nothing; the server adds a last line saying that the limit was | |||
| 297 | + | // reached, ends the job with CONCLUSION_FAILURE and RunJob.error set, and | |||
| 298 | + | // answers resource_exhausted. The runner then stops the job and sends | |||
| 299 | + | // nothing more: its calls get unauthenticated. | |||
| 300 | + | // | |||
| 301 | + | // Fails with invalid_argument if a rule is broken (step number, text with a | |||
| 302 | + | // line feed or invalid UTF-8, missing time). | |||
| 303 | + | AppendLog(context.Context, *connect.Request[ermite.AppendLogRequest]) (*connect.Response[ermite.AppendLogResponse], error) | |||
| 304 | + | // Heartbeat tells the server that the runner is alive and still working on | |||
| 305 | + | // the job. Needs the job token. The runner calls it every 30 seconds for as | |||
| 306 | + | // long as the job runs, from assignment to the end. Without a heartbeat for | |||
| 307 | + | // 2 minutes the server ends the job with CONCLUSION_FAILURE and the message | |||
| 308 | + | // "The self-hosted runner: <name> lost communication with the server...". | |||
| 309 | + | // Only Heartbeat counts: other calls do not keep a job alive. | |||
| 310 | + | Heartbeat(context.Context, *connect.Request[ermite.HeartbeatRequest]) (*connect.Response[ermite.HeartbeatResponse], error) | |||
| 311 | + | // WatchJob streams the requests to stop a job. Needs the job token. The | |||
| 312 | + | // runner opens it when it starts the job and keeps it open; it reopens it | |||
| 313 | + | // after any error other than unauthenticated. | |||
| 314 | + | // | |||
| 315 | + | // When the stream opens and a cancellation has already been requested, the | |||
| 316 | + | // first message is that request, so a reconnect never misses one. Only the | |||
| 317 | + | // first reason is sent: a job already cancelled by the owner does not get a | |||
| 318 | + | // second message when its timeout expires. The server also sends a | |||
| 319 | + | // keep-alive message (reason CANCEL_REASON_UNSPECIFIED) at least every 30 | |||
| 320 | + | // seconds, which the client ignores. The stream ends without error when | |||
| 321 | + | // the job ends. The runner stops the job as the official runner does | |||
| 322 | + | // (interrupt, then terminate, then kill) and reports the conclusion given | |||
| 323 | + | // for the reason with UpdateJob. | |||
| 324 | + | WatchJob(context.Context, *connect.Request[ermite.WatchJobRequest], *connect.ServerStream[ermite.WatchJobResponse]) error | |||
| 325 | + | } | |||
| 326 | + | ||||
| 327 | + | // NewRunnerServiceHandler builds an HTTP handler from the service implementation. It returns the | |||
| 328 | + | // path on which to mount the handler and the handler itself. | |||
| 329 | + | // | |||
| 330 | + | // By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf | |||
| 331 | + | // and JSON codecs. They also support gzip compression. | |||
| 332 | + | func NewRunnerServiceHandler(svc RunnerServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { | |||
| 333 | + | runnerServiceMethods := ermite.File_ermite_runner_proto.Services().ByName("RunnerService").Methods() | |||
| 334 | + | runnerServiceRegisterRunnerHandler := connect.NewUnaryHandler( | |||
| 335 | + | RunnerServiceRegisterRunnerProcedure, | |||
| 336 | + | svc.RegisterRunner, | |||
| 337 | + | connect.WithSchema(runnerServiceMethods.ByName("RegisterRunner")), | |||
| 338 | + | connect.WithHandlerOptions(opts...), | |||
| 339 | + | ) | |||
| 340 | + | runnerServiceFetchJobHandler := connect.NewUnaryHandler( | |||
| 341 | + | RunnerServiceFetchJobProcedure, | |||
| 342 | + | svc.FetchJob, | |||
| 343 | + | connect.WithSchema(runnerServiceMethods.ByName("FetchJob")), | |||
| 344 | + | connect.WithHandlerOptions(opts...), | |||
| 345 | + | ) | |||
| 346 | + | runnerServiceUpdateJobHandler := connect.NewUnaryHandler( | |||
| 347 | + | RunnerServiceUpdateJobProcedure, | |||
| 348 | + | svc.UpdateJob, | |||
| 349 | + | connect.WithSchema(runnerServiceMethods.ByName("UpdateJob")), | |||
| 350 | + | connect.WithHandlerOptions(opts...), | |||
| 351 | + | ) | |||
| 352 | + | runnerServiceAppendLogHandler := connect.NewUnaryHandler( | |||
| 353 | + | RunnerServiceAppendLogProcedure, | |||
| 354 | + | svc.AppendLog, | |||
| 355 | + | connect.WithSchema(runnerServiceMethods.ByName("AppendLog")), | |||
| 356 | + | connect.WithHandlerOptions(opts...), | |||
| 357 | + | ) | |||
| 358 | + | runnerServiceHeartbeatHandler := connect.NewUnaryHandler( | |||
| 359 | + | RunnerServiceHeartbeatProcedure, | |||
| 360 | + | svc.Heartbeat, | |||
| 361 | + | connect.WithSchema(runnerServiceMethods.ByName("Heartbeat")), | |||
| 362 | + | connect.WithHandlerOptions(opts...), | |||
| 363 | + | ) | |||
| 364 | + | runnerServiceWatchJobHandler := connect.NewServerStreamHandler( | |||
| 365 | + | RunnerServiceWatchJobProcedure, | |||
| 366 | + | svc.WatchJob, | |||
| 367 | + | connect.WithSchema(runnerServiceMethods.ByName("WatchJob")), | |||
| 368 | + | connect.WithHandlerOptions(opts...), | |||
| 369 | + | ) | |||
| 370 | + | return "/ermite.RunnerService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |||
| 371 | + | switch r.URL.Path { | |||
| 372 | + | case RunnerServiceRegisterRunnerProcedure: | |||
| 373 | + | runnerServiceRegisterRunnerHandler.ServeHTTP(w, r) | |||
| 374 | + | case RunnerServiceFetchJobProcedure: | |||
| 375 | + | runnerServiceFetchJobHandler.ServeHTTP(w, r) | |||
| 376 | + | case RunnerServiceUpdateJobProcedure: | |||
| 377 | + | runnerServiceUpdateJobHandler.ServeHTTP(w, r) | |||
| 378 | + | case RunnerServiceAppendLogProcedure: | |||
| 379 | + | runnerServiceAppendLogHandler.ServeHTTP(w, r) | |||
| 380 | + | case RunnerServiceHeartbeatProcedure: | |||
| 381 | + | runnerServiceHeartbeatHandler.ServeHTTP(w, r) | |||
| 382 | + | case RunnerServiceWatchJobProcedure: | |||
| 383 | + | runnerServiceWatchJobHandler.ServeHTTP(w, r) | |||
| 384 | + | default: | |||
| 385 | + | http.NotFound(w, r) | |||
| 386 | + | } | |||
| 387 | + | }) | |||
| 388 | + | } | |||
| 389 | + | ||||
| 390 | + | // UnimplementedRunnerServiceHandler returns CodeUnimplemented from all methods. | |||
| 391 | + | type UnimplementedRunnerServiceHandler struct{} | |||
| 392 | + | ||||
| 393 | + | func (UnimplementedRunnerServiceHandler) RegisterRunner(context.Context, *connect.Request[ermite.RegisterRunnerRequest]) (*connect.Response[ermite.RegisterRunnerResponse], error) { | |||
| 394 | + | return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunnerService.RegisterRunner is not implemented")) | |||
| 395 | + | } | |||
| 396 | + | ||||
| 397 | + | func (UnimplementedRunnerServiceHandler) FetchJob(context.Context, *connect.Request[ermite.FetchJobRequest]) (*connect.Response[ermite.FetchJobResponse], error) { | |||
| 398 | + | return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunnerService.FetchJob is not implemented")) | |||
| 399 | + | } | |||
| 400 | + | ||||
| 401 | + | func (UnimplementedRunnerServiceHandler) UpdateJob(context.Context, *connect.Request[ermite.UpdateJobRequest]) (*connect.Response[ermite.UpdateJobResponse], error) { | |||
| 402 | + | return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunnerService.UpdateJob is not implemented")) | |||
| 403 | + | } | |||
| 404 | + | ||||
| 405 | + | func (UnimplementedRunnerServiceHandler) AppendLog(context.Context, *connect.Request[ermite.AppendLogRequest]) (*connect.Response[ermite.AppendLogResponse], error) { | |||
| 406 | + | return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunnerService.AppendLog is not implemented")) | |||
| 407 | + | } | |||
| 408 | + | ||||
| 409 | + | func (UnimplementedRunnerServiceHandler) Heartbeat(context.Context, *connect.Request[ermite.HeartbeatRequest]) (*connect.Response[ermite.HeartbeatResponse], error) { | |||
| 410 | + | return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunnerService.Heartbeat is not implemented")) | |||
| 411 | + | } | |||
| 412 | + | ||||
| 413 | + | func (UnimplementedRunnerServiceHandler) WatchJob(context.Context, *connect.Request[ermite.WatchJobRequest], *connect.ServerStream[ermite.WatchJobResponse]) error { | |||
| 414 | + | return connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunnerService.WatchJob is not implemented")) | |||
| 415 | + | } | |||
M gen/ermite/event.pb.go +174 -25 generated file: collapsed
A gen/ermite/run.pb.go +2278 -0 generated file: collapsed
A gen/ermite/runner.pb.go +1620 -0 generated file: collapsed
M gen/ermite/server.pb.go +5 -1 generated file: collapsed
M gen_test.go +86 -0
| @@ -6,6 +6,7 @@ import ( | |||
| 6 | 6 | "testing" | |
| 7 | 7 | ||
| 8 | 8 | "google.golang.org/protobuf/proto" | |
| 9 | + | "google.golang.org/protobuf/reflect/protoreflect" | |
| 9 | 10 | "google.golang.org/protobuf/types/known/fieldmaskpb" | |
| 10 | 11 | ||
| 11 | 12 | "bnema.dev/ermite-api/gen/ermite" | |
| @@ -43,6 +44,21 @@ func TestProcedures(t *testing.T) { | |||
| 43 | 44 | {ermiteconnect.SessionServiceSignOutEverywhereProcedure, "/ermite.SessionService/SignOutEverywhere"}, | |
| 44 | 45 | {ermiteconnect.ServerServiceGetVersionProcedure, "/ermite.ServerService/GetVersion"}, | |
| 45 | 46 | {ermiteconnect.ServerServiceGetSigningKeyProcedure, "/ermite.ServerService/GetSigningKey"}, | |
| 47 | + | {ermiteconnect.RunnerServiceRegisterRunnerProcedure, "/ermite.RunnerService/RegisterRunner"}, | |
| 48 | + | {ermiteconnect.RunnerServiceFetchJobProcedure, "/ermite.RunnerService/FetchJob"}, | |
| 49 | + | {ermiteconnect.RunnerServiceUpdateJobProcedure, "/ermite.RunnerService/UpdateJob"}, | |
| 50 | + | {ermiteconnect.RunnerServiceAppendLogProcedure, "/ermite.RunnerService/AppendLog"}, | |
| 51 | + | {ermiteconnect.RunnerServiceHeartbeatProcedure, "/ermite.RunnerService/Heartbeat"}, | |
| 52 | + | {ermiteconnect.RunnerServiceWatchJobProcedure, "/ermite.RunnerService/WatchJob"}, | |
| 53 | + | {ermiteconnect.RunServiceListRunsProcedure, "/ermite.RunService/ListRuns"}, | |
| 54 | + | {ermiteconnect.RunServiceGetRunProcedure, "/ermite.RunService/GetRun"}, | |
| 55 | + | {ermiteconnect.RunServiceGetJobProcedure, "/ermite.RunService/GetJob"}, | |
| 56 | + | {ermiteconnect.RunServiceGetJobLogProcedure, "/ermite.RunService/GetJobLog"}, | |
| 57 | + | {ermiteconnect.RunServiceCancelRunProcedure, "/ermite.RunService/CancelRun"}, | |
| 58 | + | {ermiteconnect.RunServiceRerunRunProcedure, "/ermite.RunService/RerunRun"}, | |
| 59 | + | {ermiteconnect.RunServiceListChecksProcedure, "/ermite.RunService/ListChecks"}, | |
| 60 | + | {ermiteconnect.RunServiceListRunnersProcedure, "/ermite.RunService/ListRunners"}, | |
| 61 | + | {ermiteconnect.RunServiceRemoveRunnerProcedure, "/ermite.RunService/RemoveRunner"}, | |
| 46 | 62 | } | |
| 47 | 63 | for _, tt := range tests { | |
| 48 | 64 | if tt.got != tt.want { | |
| @@ -71,6 +87,21 @@ func TestEnumNumbers(t *testing.T) { | |||
| 71 | 87 | if got := ermite.DiffSide_DIFF_SIDE_NEW; got != 2 { | |
| 72 | 88 | t.Errorf("DIFF_SIDE_NEW = %d, want 2", got) | |
| 73 | 89 | } | |
| 90 | + | if got := ermite.RunStatus_RUN_STATUS_COMPLETED; got != 3 { | |
| 91 | + | t.Errorf("RUN_STATUS_COMPLETED = %d, want 3", got) | |
| 92 | + | } | |
| 93 | + | if got := ermite.Conclusion_CONCLUSION_ACTION_REQUIRED; got != 7 { | |
| 94 | + | t.Errorf("CONCLUSION_ACTION_REQUIRED = %d, want 7", got) | |
| 95 | + | } | |
| 96 | + | if got := ermite.StepState_STEP_STATE_COMPLETED; got != 3 { | |
| 97 | + | t.Errorf("STEP_STATE_COMPLETED = %d, want 3", got) | |
| 98 | + | } | |
| 99 | + | if got := ermite.RunnerIsolation_RUNNER_ISOLATION_CONTAINER_PRIVILEGED; got != 3 { | |
| 100 | + | t.Errorf("RUNNER_ISOLATION_CONTAINER_PRIVILEGED = %d, want 3", got) | |
| 101 | + | } | |
| 102 | + | if got := ermite.CancelReason_CANCEL_REASON_TIMEOUT; got != 3 { | |
| 103 | + | t.Errorf("CANCEL_REASON_TIMEOUT = %d, want 3", got) | |
| 104 | + | } | |
| 74 | 105 | } | |
| 75 | 106 | ||
| 76 | 107 | // TestEventRoundTrip checks that an event with a oneof kind survives the wire. | |
| @@ -152,3 +183,58 @@ func TestMergeRequestEventRoundTrip(t *testing.T) { | |||
| 152 | 183 | t.Errorf("modes = %v", got) | |
| 153 | 184 | } | |
| 154 | 185 | } | |
| 186 | + | ||
| 187 | + | // TestCIEventRoundTrip checks that the CI event kinds keep their field numbers | |
| 188 | + | // and survive the wire. | |
| 189 | + | func TestCIEventRoundTrip(t *testing.T) { | |
| 190 | + | in := &ermite.WatchResponse{Event: &ermite.Event{ | |
| 191 | + | Id: "44", | |
| 192 | + | Kind: &ermite.Event_JobUpdated{JobUpdated: &ermite.JobUpdated{Job: &ermite.RunJob{ | |
| 193 | + | Id: "j1", | |
| 194 | + | RunId: 9, | |
| 195 | + | Attempt: 2, | |
| 196 | + | Status: ermite.RunStatus_RUN_STATUS_COMPLETED, | |
| 197 | + | Conclusion: ermite.Conclusion_CONCLUSION_TIMED_OUT, | |
| 198 | + | Isolation: ermite.RunnerIsolation_RUNNER_ISOLATION_VM, | |
| 199 | + | Steps: []*ermite.Step{{Number: 1, Name: "Set up job", LogStart: 0, LogEnd: 3}}, | |
| 200 | + | }}}, | |
| 201 | + | }} | |
| 202 | + | b, err := proto.Marshal(in) | |
| 203 | + | if err != nil { | |
| 204 | + | t.Fatal(err) | |
| 205 | + | } | |
| 206 | + | var out ermite.WatchResponse | |
| 207 | + | if err := proto.Unmarshal(b, &out); err != nil { | |
| 208 | + | t.Fatal(err) | |
| 209 | + | } | |
| 210 | + | if !proto.Equal(in, &out) { | |
| 211 | + | t.Errorf("round trip mismatch: %v != %v", &out, in) | |
| 212 | + | } | |
| 213 | + | f := (&ermite.Event{}).ProtoReflect().Descriptor().Fields() | |
| 214 | + | for name, num := range map[string]int{"run_updated": 12, "job_updated": 13} { | |
| 215 | + | if fd := f.ByName(protoreflect.Name(name)); fd == nil || int(fd.Number()) != num { | |
| 216 | + | t.Errorf("event field %s: %v, want number %d", name, fd, num) | |
| 217 | + | } | |
| 218 | + | } | |
| 219 | + | } | |
| 220 | + | ||
| 221 | + | // TestJobRoundTrip checks the job handed to a runner survives the wire. | |
| 222 | + | func TestJobRoundTrip(t *testing.T) { | |
| 223 | + | in := &ermite.FetchJobResponse{Job: &ermite.Job{ | |
| 224 | + | JobId: "j1", RunId: 9, Attempt: 1, JobToken: "tok", WorkflowPath: ".github/workflows/ci.yml", | |
| 225 | + | WorkflowYaml: "on: push", JobKey: "build", Matrix: "{}", Contexts: "{}", | |
| 226 | + | Labels: []string{"self-hosted"}, TimeoutMinutes: 360, Repo: "ermite", | |
| 227 | + | Sha: "0123456789012345678901234567890123456789", Ref: "refs/heads/main", | |
| 228 | + | }} | |
| 229 | + | b, err := proto.Marshal(in) | |
| 230 | + | if err != nil { | |
| 231 | + | t.Fatal(err) | |
| 232 | + | } | |
| 233 | + | var out ermite.FetchJobResponse | |
| 234 | + | if err := proto.Unmarshal(b, &out); err != nil { | |
| 235 | + | t.Fatal(err) | |
| 236 | + | } | |
| 237 | + | if !proto.Equal(in, &out) { | |
| 238 | + | t.Errorf("round trip mismatch: %v != %v", &out, in) | |
| 239 | + | } | |
| 240 | + | } | |
| @@ -6,6 +6,7 @@ import ( | |||||
| 6 | "testing" | 6 | "testing" | ||
| 7 | 7 | ||||
| 8 | "google.golang.org/protobuf/proto" | 8 | "google.golang.org/protobuf/proto" | ||
| 9 | + | "google.golang.org/protobuf/reflect/protoreflect" | |||
| 9 | "google.golang.org/protobuf/types/known/fieldmaskpb" | 10 | "google.golang.org/protobuf/types/known/fieldmaskpb" | ||
| 10 | 11 | ||||
| 11 | "bnema.dev/ermite-api/gen/ermite" | 12 | "bnema.dev/ermite-api/gen/ermite" | ||
| @@ -43,6 +44,21 @@ func TestProcedures(t *testing.T) { | |||||
| 43 | {ermiteconnect.SessionServiceSignOutEverywhereProcedure, "/ermite.SessionService/SignOutEverywhere"}, | 44 | {ermiteconnect.SessionServiceSignOutEverywhereProcedure, "/ermite.SessionService/SignOutEverywhere"}, | ||
| 44 | {ermiteconnect.ServerServiceGetVersionProcedure, "/ermite.ServerService/GetVersion"}, | 45 | {ermiteconnect.ServerServiceGetVersionProcedure, "/ermite.ServerService/GetVersion"}, | ||
| 45 | {ermiteconnect.ServerServiceGetSigningKeyProcedure, "/ermite.ServerService/GetSigningKey"}, | 46 | {ermiteconnect.ServerServiceGetSigningKeyProcedure, "/ermite.ServerService/GetSigningKey"}, | ||
| 47 | + | {ermiteconnect.RunnerServiceRegisterRunnerProcedure, "/ermite.RunnerService/RegisterRunner"}, | |||
| 48 | + | {ermiteconnect.RunnerServiceFetchJobProcedure, "/ermite.RunnerService/FetchJob"}, | |||
| 49 | + | {ermiteconnect.RunnerServiceUpdateJobProcedure, "/ermite.RunnerService/UpdateJob"}, | |||
| 50 | + | {ermiteconnect.RunnerServiceAppendLogProcedure, "/ermite.RunnerService/AppendLog"}, | |||
| 51 | + | {ermiteconnect.RunnerServiceHeartbeatProcedure, "/ermite.RunnerService/Heartbeat"}, | |||
| 52 | + | {ermiteconnect.RunnerServiceWatchJobProcedure, "/ermite.RunnerService/WatchJob"}, | |||
| 53 | + | {ermiteconnect.RunServiceListRunsProcedure, "/ermite.RunService/ListRuns"}, | |||
| 54 | + | {ermiteconnect.RunServiceGetRunProcedure, "/ermite.RunService/GetRun"}, | |||
| 55 | + | {ermiteconnect.RunServiceGetJobProcedure, "/ermite.RunService/GetJob"}, | |||
| 56 | + | {ermiteconnect.RunServiceGetJobLogProcedure, "/ermite.RunService/GetJobLog"}, | |||
| 57 | + | {ermiteconnect.RunServiceCancelRunProcedure, "/ermite.RunService/CancelRun"}, | |||
| 58 | + | {ermiteconnect.RunServiceRerunRunProcedure, "/ermite.RunService/RerunRun"}, | |||
| 59 | + | {ermiteconnect.RunServiceListChecksProcedure, "/ermite.RunService/ListChecks"}, | |||
| 60 | + | {ermiteconnect.RunServiceListRunnersProcedure, "/ermite.RunService/ListRunners"}, | |||
| 61 | + | {ermiteconnect.RunServiceRemoveRunnerProcedure, "/ermite.RunService/RemoveRunner"}, | |||
| 46 | } | 62 | } | ||
| 47 | for _, tt := range tests { | 63 | for _, tt := range tests { | ||
| 48 | if tt.got != tt.want { | 64 | if tt.got != tt.want { | ||
| @@ -71,6 +87,21 @@ func TestEnumNumbers(t *testing.T) { | |||||
| 71 | if got := ermite.DiffSide_DIFF_SIDE_NEW; got != 2 { | 87 | if got := ermite.DiffSide_DIFF_SIDE_NEW; got != 2 { | ||
| 72 | t.Errorf("DIFF_SIDE_NEW = %d, want 2", got) | 88 | t.Errorf("DIFF_SIDE_NEW = %d, want 2", got) | ||
| 73 | } | 89 | } | ||
| 90 | + | if got := ermite.RunStatus_RUN_STATUS_COMPLETED; got != 3 { | |||
| 91 | + | t.Errorf("RUN_STATUS_COMPLETED = %d, want 3", got) | |||
| 92 | + | } | |||
| 93 | + | if got := ermite.Conclusion_CONCLUSION_ACTION_REQUIRED; got != 7 { | |||
| 94 | + | t.Errorf("CONCLUSION_ACTION_REQUIRED = %d, want 7", got) | |||
| 95 | + | } | |||
| 96 | + | if got := ermite.StepState_STEP_STATE_COMPLETED; got != 3 { | |||
| 97 | + | t.Errorf("STEP_STATE_COMPLETED = %d, want 3", got) | |||
| 98 | + | } | |||
| 99 | + | if got := ermite.RunnerIsolation_RUNNER_ISOLATION_CONTAINER_PRIVILEGED; got != 3 { | |||
| 100 | + | t.Errorf("RUNNER_ISOLATION_CONTAINER_PRIVILEGED = %d, want 3", got) | |||
| 101 | + | } | |||
| 102 | + | if got := ermite.CancelReason_CANCEL_REASON_TIMEOUT; got != 3 { | |||
| 103 | + | t.Errorf("CANCEL_REASON_TIMEOUT = %d, want 3", got) | |||
| 104 | + | } | |||
| 74 | } | 105 | } | ||
| 75 | 106 | ||||
| 76 | // TestEventRoundTrip checks that an event with a oneof kind survives the wire. | 107 | // TestEventRoundTrip checks that an event with a oneof kind survives the wire. | ||
| @@ -152,3 +183,58 @@ func TestMergeRequestEventRoundTrip(t *testing.T) { | |||||
| 152 | t.Errorf("modes = %v", got) | 183 | t.Errorf("modes = %v", got) | ||
| 153 | } | 184 | } | ||
| 154 | } | 185 | } | ||
| 186 | + | ||||
| 187 | + | // TestCIEventRoundTrip checks that the CI event kinds keep their field numbers | |||
| 188 | + | // and survive the wire. | |||
| 189 | + | func TestCIEventRoundTrip(t *testing.T) { | |||
| 190 | + | in := &ermite.WatchResponse{Event: &ermite.Event{ | |||
| 191 | + | Id: "44", | |||
| 192 | + | Kind: &ermite.Event_JobUpdated{JobUpdated: &ermite.JobUpdated{Job: &ermite.RunJob{ | |||
| 193 | + | Id: "j1", | |||
| 194 | + | RunId: 9, | |||
| 195 | + | Attempt: 2, | |||
| 196 | + | Status: ermite.RunStatus_RUN_STATUS_COMPLETED, | |||
| 197 | + | Conclusion: ermite.Conclusion_CONCLUSION_TIMED_OUT, | |||
| 198 | + | Isolation: ermite.RunnerIsolation_RUNNER_ISOLATION_VM, | |||
| 199 | + | Steps: []*ermite.Step{{Number: 1, Name: "Set up job", LogStart: 0, LogEnd: 3}}, | |||
| 200 | + | }}}, | |||
| 201 | + | }} | |||
| 202 | + | b, err := proto.Marshal(in) | |||
| 203 | + | if err != nil { | |||
| 204 | + | t.Fatal(err) | |||
| 205 | + | } | |||
| 206 | + | var out ermite.WatchResponse | |||
| 207 | + | if err := proto.Unmarshal(b, &out); err != nil { | |||
| 208 | + | t.Fatal(err) | |||
| 209 | + | } | |||
| 210 | + | if !proto.Equal(in, &out) { | |||
| 211 | + | t.Errorf("round trip mismatch: %v != %v", &out, in) | |||
| 212 | + | } | |||
| 213 | + | f := (&ermite.Event{}).ProtoReflect().Descriptor().Fields() | |||
| 214 | + | for name, num := range map[string]int{"run_updated": 12, "job_updated": 13} { | |||
| 215 | + | if fd := f.ByName(protoreflect.Name(name)); fd == nil || int(fd.Number()) != num { | |||
| 216 | + | t.Errorf("event field %s: %v, want number %d", name, fd, num) | |||
| 217 | + | } | |||
| 218 | + | } | |||
| 219 | + | } | |||
| 220 | + | ||||
| 221 | + | // TestJobRoundTrip checks the job handed to a runner survives the wire. | |||
| 222 | + | func TestJobRoundTrip(t *testing.T) { | |||
| 223 | + | in := &ermite.FetchJobResponse{Job: &ermite.Job{ | |||
| 224 | + | JobId: "j1", RunId: 9, Attempt: 1, JobToken: "tok", WorkflowPath: ".github/workflows/ci.yml", | |||
| 225 | + | WorkflowYaml: "on: push", JobKey: "build", Matrix: "{}", Contexts: "{}", | |||
| 226 | + | Labels: []string{"self-hosted"}, TimeoutMinutes: 360, Repo: "ermite", | |||
| 227 | + | Sha: "0123456789012345678901234567890123456789", Ref: "refs/heads/main", | |||
| 228 | + | }} | |||
| 229 | + | b, err := proto.Marshal(in) | |||
| 230 | + | if err != nil { | |||
| 231 | + | t.Fatal(err) | |||
| 232 | + | } | |||
| 233 | + | var out ermite.FetchJobResponse | |||
| 234 | + | if err := proto.Unmarshal(b, &out); err != nil { | |||
| 235 | + | t.Fatal(err) | |||
| 236 | + | } | |||
| 237 | + | if !proto.Equal(in, &out) { | |||
| 238 | + | t.Errorf("round trip mismatch: %v != %v", &out, in) | |||
| 239 | + | } | |||
| 240 | + | } | |||
M proto/ermite/event.proto +28 -1
| @@ -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,15 @@ 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). A job update is published | |
| 30 | + | // before the update of its run. There is no event for a change of title or | |
| 22 | 31 | // description, nor for a resolved comment: after a gap, or to see such | |
| 23 | 32 | // changes, clients read the merge request again. A merge publishes | |
| 24 | 33 | // merge_request_merged, then pushed for the target branch. | |
| @@ -87,6 +96,10 @@ message Event { | |||
| 87 | 96 | MergeRequestClosed merge_request_closed = 10; | |
| 88 | 97 | // CommentAdded: a comment was added to a merge request. | |
| 89 | 98 | CommentAdded comment_added = 11; | |
| 99 | + | // RunUpdated: a CI run was created or changed. | |
| 100 | + | RunUpdated run_updated = 12; | |
| 101 | + | // JobUpdated: a CI job was created or changed. | |
| 102 | + | JobUpdated job_updated = 13; | |
| 90 | 103 | } | |
| 91 | 104 | } | |
| 92 | 105 | ||
| @@ -160,3 +173,17 @@ message CommentAdded { | |||
| 160 | 173 | // the state it displays (repositories, refs, merge requests and their | |
| 161 | 174 | // comments) and carry on with the next events. | |
| 162 | 175 | message Gap {} | |
| 176 | + | ||
| 177 | + | // RunUpdated reports a CI run that was created or whose status, conclusion or | |
| 178 | + | // timestamps changed. It is delivered on the "repo:<name>" topic only. | |
| 179 | + | message RunUpdated { | |
| 180 | + | // Run is the run after the change, as of its latest attempt, without jobs. | |
| 181 | + | Run run = 1; | |
| 182 | + | } | |
| 183 | + | ||
| 184 | + | // JobUpdated reports a CI job that was created or whose status, conclusion, | |
| 185 | + | // runner or steps changed. It is delivered on the "repo:<name>" topic only. | |
| 186 | + | message JobUpdated { | |
| 187 | + | // Job is the job after the change, with its steps. | |
| 188 | + | RunJob job = 1; | |
| 189 | + | } | |
| @@ -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,15 @@ 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). A job update is published | |||
| 30 | + | // before the update of its run. There is no event for a change of title or | |||
| 22 | // description, nor for a resolved comment: after a gap, or to see such | 31 | // description, nor for a resolved comment: after a gap, or to see such | ||
| 23 | // changes, clients read the merge request again. A merge publishes | 32 | // changes, clients read the merge request again. A merge publishes | ||
| 24 | // merge_request_merged, then pushed for the target branch. | 33 | // merge_request_merged, then pushed for the target branch. | ||
| @@ -87,6 +96,10 @@ message Event { | |||||
| 87 | MergeRequestClosed merge_request_closed = 10; | 96 | MergeRequestClosed merge_request_closed = 10; | ||
| 88 | // CommentAdded: a comment was added to a merge request. | 97 | // CommentAdded: a comment was added to a merge request. | ||
| 89 | CommentAdded comment_added = 11; | 98 | CommentAdded comment_added = 11; | ||
| 99 | + | // RunUpdated: a CI run was created or changed. | |||
| 100 | + | RunUpdated run_updated = 12; | |||
| 101 | + | // JobUpdated: a CI job was created or changed. | |||
| 102 | + | JobUpdated job_updated = 13; | |||
| 90 | } | 103 | } | ||
| 91 | } | 104 | } | ||
| 92 | 105 | ||||
| @@ -160,3 +173,17 @@ message CommentAdded { | |||||
| 160 | // the state it displays (repositories, refs, merge requests and their | 173 | // the state it displays (repositories, refs, merge requests and their | ||
| 161 | // comments) and carry on with the next events. | 174 | // comments) and carry on with the next events. | ||
| 162 | message Gap {} | 175 | message Gap {} | ||
| 176 | + | ||||
| 177 | + | // RunUpdated reports a CI run that was created or whose status, conclusion or | |||
| 178 | + | // timestamps changed. It is delivered on the "repo:<name>" topic only. | |||
| 179 | + | message RunUpdated { | |||
| 180 | + | // Run is the run after the change, as of its latest attempt, without jobs. | |||
| 181 | + | Run run = 1; | |||
| 182 | + | } | |||
| 183 | + | ||||
| 184 | + | // JobUpdated reports a CI job that was created or whose status, conclusion, | |||
| 185 | + | // runner or steps changed. It is delivered on the "repo:<name>" topic only. | |||
| 186 | + | message JobUpdated { | |||
| 187 | + | // Job is the job after the change, with its steps. | |||
| 188 | + | RunJob job = 1; | |||
| 189 | + | } | |||
A proto/ermite/run.proto +487 -0
| @@ -0,0 +1,487 @@ | |||
| 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 | + | RunStatus 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 | + | Conclusion 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 | + | } | |
| 72 | + | ||
| 73 | + | // RunJob is a job of a run as people see it: the state of one job instance | |
| 74 | + | // (one matrix combination is one job) in one attempt. It is not Job, which is | |
| 75 | + | // what a runner receives. | |
| 76 | + | message RunJob { | |
| 77 | + | // Id is the Job.job_id. It is the same in every attempt. | |
| 78 | + | string id = 1; | |
| 79 | + | // RunId is the run the job belongs to. | |
| 80 | + | int64 run_id = 2; | |
| 81 | + | // Attempt is the attempt of the run in which this job executed. It is lower | |
| 82 | + | // than the attempt of the run when the job was kept from an earlier attempt | |
| 83 | + | // by a re-run of the failed jobs only; the log and steps are then those of | |
| 84 | + | // that earlier attempt. Pass it to GetJob and GetJobLog. | |
| 85 | + | int32 attempt = 3; | |
| 86 | + | // Repo is the name of the repository. | |
| 87 | + | string repo = 4; | |
| 88 | + | // Key is the key of the job under "jobs:" in the workflow (the | |
| 89 | + | // "github.job"). Matrix instances of one job share it. | |
| 90 | + | string key = 5; | |
| 91 | + | // Name is the display name of the job, evaluated, with the matrix suffix as | |
| 92 | + | // GitHub names them, for example "build (linux, 1.27)". It is also the | |
| 93 | + | // name of the job's check. | |
| 94 | + | string name = 6; | |
| 95 | + | // Status is where the job is. A job waits QUEUED for a runner and for the | |
| 96 | + | // jobs it needs. | |
| 97 | + | RunStatus status = 7; | |
| 98 | + | // Conclusion is how the job ended. Unset until status is COMPLETED. | |
| 99 | + | Conclusion conclusion = 8; | |
| 100 | + | // Error explains a FAILURE, CANCELLED or TIMED_OUT that no step explains, in | |
| 101 | + | // GitHub's words where it has them: the runner was lost ("The self-hosted | |
| 102 | + | // runner: <name> lost communication with the server..."), it received a | |
| 103 | + | // shutdown signal, no runner took the job within 24 hours, the log limit | |
| 104 | + | // was reached, the workflow uses a feature that is not supported yet | |
| 105 | + | // ("not supported yet: <feature>"). At most 4 KiB. Empty otherwise. | |
| 106 | + | string error = 9; | |
| 107 | + | // Labels are the runs-on labels of the job, evaluated. Empty while the job | |
| 108 | + | // is not planned yet. | |
| 109 | + | repeated string labels = 10; | |
| 110 | + | // Needs are the keys of the jobs this job needs, for drawing the graph. | |
| 111 | + | repeated string needs = 11; | |
| 112 | + | // RunnerId is the id of the runner that executed or is executing the job. | |
| 113 | + | // Empty while queued, and when the runner has since been removed (runner_name | |
| 114 | + | // and isolation are kept). | |
| 115 | + | string runner_id = 12; | |
| 116 | + | // RunnerName is the name of that runner. Empty while queued. | |
| 117 | + | string runner_name = 13; | |
| 118 | + | // Isolation is how that runner isolated the job. UNSPECIFIED while queued. | |
| 119 | + | RunnerIsolation isolation = 14; | |
| 120 | + | // Steps are the steps reported so far, ordered by number, including the | |
| 121 | + | // implicit "Set up job" and "Complete job". Empty while queued. Always | |
| 122 | + | // set, including in JobUpdated events. | |
| 123 | + | repeated Step steps = 15; | |
| 124 | + | // CreatedAt is when the job was created (planned). | |
| 125 | + | google.protobuf.Timestamp created_at = 16; | |
| 126 | + | // StartedAt is when the job was given to a runner. Unset until then. | |
| 127 | + | google.protobuf.Timestamp started_at = 17; | |
| 128 | + | // CompletedAt is when the job completed. Unset until then. | |
| 129 | + | google.protobuf.Timestamp completed_at = 18; | |
| 130 | + | } | |
| 131 | + | ||
| 132 | + | // Step is one step of a job, with where its lines are in the job log. | |
| 133 | + | message Step { | |
| 134 | + | // Number is the 1-based number of the step in the job, the one the runner | |
| 135 | + | // uses in UpdateJob and AppendLog. | |
| 136 | + | int32 number = 1; | |
| 137 | + | // Name is the display name of the step. | |
| 138 | + | string name = 2; | |
| 139 | + | // State is where the step is. | |
| 140 | + | StepState state = 3; | |
| 141 | + | // Conclusion is how the step ended. Unset until state is COMPLETED. | |
| 142 | + | Conclusion conclusion = 4; | |
| 143 | + | // StartedAt is when the step started. Unset while PENDING. | |
| 144 | + | google.protobuf.Timestamp started_at = 5; | |
| 145 | + | // CompletedAt is when the step ended. Unset until COMPLETED. | |
| 146 | + | google.protobuf.Timestamp completed_at = 6; | |
| 147 | + | // LogStart is the number of the first line of the step in the job log (the | |
| 148 | + | // offset of GetJobLogRequest and AppendLogRequest), counted from 0. | |
| 149 | + | int64 log_start = 7; | |
| 150 | + | // LogEnd is the number of the line after the last one of the step. For an | |
| 151 | + | // IN_PROGRESS step it is the number of lines held at the time of the | |
| 152 | + | // call and grows; for a step without a line it equals log_start. | |
| 153 | + | int64 log_end = 8; | |
| 154 | + | } | |
| 155 | + | ||
| 156 | + | // Check is the result of one job on one commit, the unit shown next to a | |
| 157 | + | // commit or a merge request and counted by the merge rules. There is one per | |
| 158 | + | // job instance, named like GitHub's checks. | |
| 159 | + | message Check { | |
| 160 | + | // Id identifies the check. It is opaque and stable across attempts. | |
| 161 | + | string id = 1; | |
| 162 | + | // Repo is the name of the repository. | |
| 163 | + | string repo = 2; | |
| 164 | + | // Sha is the full id of the commit. | |
| 165 | + | string sha = 3; | |
| 166 | + | // Name is the name of the check: the RunJob.name of the job. | |
| 167 | + | string name = 4; | |
| 168 | + | // Status is where the check is. | |
| 169 | + | RunStatus status = 5; | |
| 170 | + | // Conclusion is how the check ended. Unset until status is COMPLETED. The | |
| 171 | + | // value "stale" of GitHub is never used. | |
| 172 | + | Conclusion conclusion = 6; | |
| 173 | + | // RunId is the run of the job. | |
| 174 | + | int64 run_id = 7; | |
| 175 | + | // JobId is the RunJob.id of the job, to open its page and log. | |
| 176 | + | string job_id = 8; | |
| 177 | + | // Attempt is the attempt the check describes: the latest attempt of the run. | |
| 178 | + | int32 attempt = 9; | |
| 179 | + | // WorkflowName is the Run.workflow_name of the run. | |
| 180 | + | string workflow_name = 10; | |
| 181 | + | // StartedAt is when the job was given to a runner. Unset while queued. | |
| 182 | + | google.protobuf.Timestamp started_at = 11; | |
| 183 | + | // CompletedAt is when the job completed. Unset until then. | |
| 184 | + | google.protobuf.Timestamp completed_at = 12; | |
| 185 | + | } | |
| 186 | + | ||
| 187 | + | // Runner is a registered CI runner. It never contains the runner token nor its | |
| 188 | + | // hash. | |
| 189 | + | message Runner { | |
| 190 | + | // Id identifies the runner in RemoveRunner. It is not a secret. | |
| 191 | + | string id = 1; | |
| 192 | + | // Name is the unique name chosen at registration. | |
| 193 | + | string name = 2; | |
| 194 | + | // Labels are all the labels the runner can match: the ones chosen at | |
| 195 | + | // registration plus the implicit ones ("self-hosted", the os and the arch), | |
| 196 | + | // lowercase, sorted. | |
| 197 | + | repeated string labels = 3; | |
| 198 | + | // Isolation is how the runner isolates jobs. | |
| 199 | + | RunnerIsolation isolation = 4; | |
| 200 | + | // Version is the version of the runner software. | |
| 201 | + | string version = 5; | |
| 202 | + | // Arch is "x64" or "arm64". | |
| 203 | + | string arch = 6; | |
| 204 | + | // Os is "linux". | |
| 205 | + | string os = 7; | |
| 206 | + | // RegisteredAt is when the runner was registered. | |
| 207 | + | google.protobuf.Timestamp registered_at = 8; | |
| 208 | + | // LastSeen is when the server last heard from the runner: a FetchJob poll or | |
| 209 | + | // a heartbeat of one of its jobs. Unset if never. It may lag behind the real | |
| 210 | + | // time by a short delay. A runner not seen for more than about 2 minutes is | |
| 211 | + | // offline. | |
| 212 | + | google.protobuf.Timestamp last_seen = 9; | |
| 213 | + | // Busy is true while the runner has a job in progress. | |
| 214 | + | bool busy = 10; | |
| 215 | + | } | |
| 216 | + | ||
| 217 | + | // RunService reads and controls CI runs, like `gh run` does on GitHub. It is for | |
| 218 | + | // owners and their tools; runners use RunnerService. | |
| 219 | + | // | |
| 220 | + | // Runs, jobs, checks and logs belong to a repository, and reading them needs | |
| 221 | + | // the same access as reading it: nothing for a public repository, the | |
| 222 | + | // repo:read scope for a private one. A repository that does not exist or that | |
| 223 | + | // the caller cannot read gives not_found; so does a run or a job that belongs | |
| 224 | + | // to another repository than the one named (ids are global but are always | |
| 225 | + | // checked against the repository). Cancelling and re-running need the ci:write | |
| 226 | + | // scope on the repository (a token restricted to other repositories gets | |
| 227 | + | // permission_denied). The runner RPCs (ListRunners, RemoveRunner) are not about | |
| 228 | + | // a repository and need the runner:register scope on a token that is not | |
| 229 | + | // restricted to repositories, or the root token. A caller without the needed | |
| 230 | + | // scope gets permission_denied. | |
| 231 | + | // | |
| 232 | + | // Page sizes and page tokens work as in the other services: page_size 0 is the | |
| 233 | + | // server default, larger values are lowered to the server maximum, and a | |
| 234 | + | // page_token is the next_page_token of a previous response for the same | |
| 235 | + | // request (other fields must be unchanged). | |
| 236 | + | // | |
| 237 | + | // Servers that implement the runs side (every RPC but ListRunners and | |
| 238 | + | // RemoveRunner) list "ci.runs" in GetVersionResponse.features; those that | |
| 239 | + | // implement the runners side list "ci.runners", as RunnerService does. A server | |
| 240 | + | // that honours the filters of ListRunsRequest also lists "ci.runs.filters". | |
| 241 | + | service RunService { | |
| 242 | + | // ListRuns lists the runs of a repository, highest id (newest) first. Each | |
| 243 | + | // run is described by its latest attempt. The filters narrow the result; a | |
| 244 | + | // server without the feature "ci.runs.filters" ignores them and returns | |
| 245 | + | // every run, so a client that needs them checks the feature. Fails with | |
| 246 | + | // invalid_argument if a filter is malformed. | |
| 247 | + | rpc ListRuns(ListRunsRequest) returns (ListRunsResponse); | |
| 248 | + | // GetRun returns a run with its jobs and their steps, for the latest attempt | |
| 249 | + | // or for the one asked. Fails with not_found if the run or the attempt does | |
| 250 | + | // not exist. | |
| 251 | + | rpc GetRun(GetRunRequest) returns (GetRunResponse); | |
| 252 | + | // GetJob returns one job with its steps. Fails with not_found if the job | |
| 253 | + | // or the attempt does not exist. | |
| 254 | + | rpc GetJob(GetJobRequest) returns (GetJobResponse); | |
| 255 | + | // GetJobLog streams the lines of the log of a job, from a line number. The | |
| 256 | + | // server sends them in order in messages of at most 1,000 lines and 64 KiB of | |
| 257 | + | // text each. | |
| 258 | + | // | |
| 259 | + | // Without follow, the stream ends after the last line stored when the call | |
| 260 | + | // was made. With follow, it goes on as lines arrive, with at most 1 second of | |
| 261 | + | // delay after the runner's AppendLog, and ends without error once the job has | |
| 262 | + | // completed and all its lines were sent; on a completed job follow changes | |
| 263 | + | // nothing. A client that loses the stream calls again with the offset after | |
| 264 | + | // the last line it received. The client may cancel at any time. | |
| 265 | + | // | |
| 266 | + | // Fails with not_found if the job, the attempt or its log does not exist (the | |
| 267 | + | // log is removed by the retention sweep, and a queued job has none: with | |
| 268 | + | // follow, the call waits for the first line instead), with out_of_range if | |
| 269 | + | // offset is beyond the end of the log of a job without follow, and with | |
| 270 | + | // resource_exhausted when there are too many concurrent log streams, in | |
| 271 | + | // total or from one client. Like Watch, a stream ends with unauthenticated | |
| 272 | + | // when the token was revoked or expired. | |
| 273 | + | rpc GetJobLog(GetJobLogRequest) returns (stream GetJobLogResponse); | |
| 274 | + | // CancelRun asks to cancel the latest attempt of a run. Needs the ci:write | |
| 275 | + | // scope. Jobs that have not started are cancelled at once; jobs in progress | |
| 276 | + | // get a CANCEL_REASON_USER request through WatchJob, and the run completes | |
| 277 | + | // when they have ended, so the returned run may still be IN_PROGRESS. | |
| 278 | + | // Cancelling a run that already has a cancellation requested succeeds and | |
| 279 | + | // changes nothing. Fails with failed_precondition if the run has completed. | |
| 280 | + | rpc CancelRun(CancelRunRequest) returns (CancelRunResponse); | |
| 281 | + | // RerunRun starts a new attempt of a completed run: same run id and number, | |
| 282 | + | // attempt + 1, same commit and same workflow file as at the original run (the | |
| 283 | + | // file of the repository is not read again). Needs the ci:write scope. Fails | |
| 284 | + | // with failed_precondition if the latest attempt has not completed, or if | |
| 285 | + | // failed_only is set and no job failed. The new attempt is returned QUEUED. | |
| 286 | + | rpc RerunRun(RerunRunRequest) returns (RerunRunResponse); | |
| 287 | + | // ListChecks lists the checks of a commit: for each job of each run on that | |
| 288 | + | // sha, the one of the latest attempt, ordered by creation time then name. | |
| 289 | + | // A commit without a run has no check: the list is empty, which is not an | |
| 290 | + | // error, and the server does not verify that the commit exists. Returns at | |
| 291 | + | // most 1,000 checks (truncated tells). | |
| 292 | + | rpc ListChecks(ListChecksRequest) returns (ListChecksResponse); | |
| 293 | + | // ListRunners lists the registered runners, ordered by name. Needs the | |
| 294 | + | // runner:register scope. | |
| 295 | + | rpc ListRunners(ListRunnersRequest) returns (ListRunnersResponse); | |
| 296 | + | // RemoveRunner removes a runner: its runner token stops working at once (a | |
| 297 | + | // FetchJob in progress ends with unauthenticated) and it no longer gets | |
| 298 | + | // jobs. A job it is running is not affected: it ends normally, or fails as | |
| 299 | + | // a lost runner without heartbeats. Needs the runner:register scope. Fails | |
| 300 | + | // with not_found if the id is unknown. | |
| 301 | + | rpc RemoveRunner(RemoveRunnerRequest) returns (RemoveRunnerResponse); | |
| 302 | + | } | |
| 303 | + | ||
| 304 | + | // ListRunsRequest selects a page of runs. | |
| 305 | + | message ListRunsRequest { | |
| 306 | + | // Repo is the repository name. | |
| 307 | + | string repo = 1; | |
| 308 | + | // PageSize is the maximum number of runs to return. | |
| 309 | + | int32 page_size = 2; | |
| 310 | + | // PageToken is the next_page_token of a previous response. Empty for the | |
| 311 | + | // first page. | |
| 312 | + | string page_token = 3; | |
| 313 | + | // Workflow restricts the result to one workflow file: its path | |
| 314 | + | // (".github/workflows/ci.yml") or its file name ("ci.yml"). Empty means | |
| 315 | + | // every workflow. Ignored by servers without "ci.runs.filters". | |
| 316 | + | string workflow = 4; | |
| 317 | + | // Branch restricts the result to the runs whose ref is this branch, by | |
| 318 | + | // short name ("main"). Empty means every ref. Ignored by servers without | |
| 319 | + | // "ci.runs.filters". | |
| 320 | + | string branch = 5; | |
| 321 | + | // Status restricts the result to the runs whose latest attempt has this | |
| 322 | + | // status. RUN_STATUS_UNSPECIFIED means every status. Ignored by servers | |
| 323 | + | // without "ci.runs.filters". | |
| 324 | + | RunStatus status = 6; | |
| 325 | + | } | |
| 326 | + | ||
| 327 | + | // ListRunsResponse is one page of runs. | |
| 328 | + | message ListRunsResponse { | |
| 329 | + | // Runs are the runs of this page, highest id first, without jobs. | |
| 330 | + | repeated Run runs = 1; | |
| 331 | + | // NextPageToken is passed as page_token to get the next page. Empty when | |
| 332 | + | // there is no next page. | |
| 333 | + | string next_page_token = 2; | |
| 334 | + | } | |
| 335 | + | ||
| 336 | + | // GetRunRequest selects a run. | |
| 337 | + | message GetRunRequest { | |
| 338 | + | // Repo is the repository name. | |
| 339 | + | string repo = 1; | |
| 340 | + | // RunId is the Run.id. | |
| 341 | + | int64 run_id = 2; | |
| 342 | + | // Attempt is the attempt to read. 0 means the latest. | |
| 343 | + | int32 attempt = 3; | |
| 344 | + | } | |
| 345 | + | ||
| 346 | + | // GetRunResponse holds a run with its jobs. | |
| 347 | + | message GetRunResponse { | |
| 348 | + | // Run is the run, as of the attempt asked. | |
| 349 | + | Run run = 1; | |
| 350 | + | // Jobs are the jobs of that attempt, with their steps, in the order of the | |
| 351 | + | // workflow file (matrix instances in the order of the matrix). Jobs kept from | |
| 352 | + | // an earlier attempt by a re-run of failed jobs are included, with their own | |
| 353 | + | // RunJob.attempt. Empty for a run that failed on an invalid workflow file. | |
| 354 | + | repeated RunJob jobs = 2; | |
| 355 | + | // LatestAttempt is the latest attempt of the run, to offer the other ones. | |
| 356 | + | int32 latest_attempt = 3; | |
| 357 | + | } | |
| 358 | + | ||
| 359 | + | // GetJobRequest selects a job. | |
| 360 | + | message GetJobRequest { | |
| 361 | + | // Repo is the repository name. | |
| 362 | + | string repo = 1; | |
| 363 | + | // JobId is the RunJob.id. | |
| 364 | + | string job_id = 2; | |
| 365 | + | // Attempt is the RunJob.attempt to read. 0 means the latest. | |
| 366 | + | int32 attempt = 3; | |
| 367 | + | } | |
| 368 | + | ||
| 369 | + | // GetJobResponse holds a job. | |
| 370 | + | message GetJobResponse { | |
| 371 | + | // Job is the job with its steps. | |
| 372 | + | RunJob job = 1; | |
| 373 | + | } | |
| 374 | + | ||
| 375 | + | // GetJobLogRequest selects the lines to stream. | |
| 376 | + | message GetJobLogRequest { | |
| 377 | + | // Repo is the repository name. | |
| 378 | + | string repo = 1; | |
| 379 | + | // JobId is the RunJob.id. | |
| 380 | + | string job_id = 2; | |
| 381 | + | // Attempt is the RunJob.attempt whose log to read. 0 means the latest. | |
| 382 | + | int32 attempt = 3; | |
| 383 | + | // Offset is the number of the first line to send, counted from 0 across all | |
| 384 | + | // steps (see Step.log_start). Negative is invalid_argument. | |
| 385 | + | int64 offset = 4; | |
| 386 | + | // Follow keeps the stream open while the job runs. See GetJobLog. | |
| 387 | + | bool follow = 5; | |
| 388 | + | } | |
| 389 | + | ||
| 390 | + | // GetJobLogResponse is one message of the log stream. | |
| 391 | + | message GetJobLogResponse { | |
| 392 | + | // Offset is the number of the first line of lines. | |
| 393 | + | int64 offset = 1; | |
| 394 | + | // Lines are consecutive lines of the log, at most 1,000, at most 64 KiB of | |
| 395 | + | // text in total. Never empty. | |
| 396 | + | repeated JobLogLine lines = 2; | |
| 397 | + | } | |
| 398 | + | ||
| 399 | + | // JobLogLine is one line of a job log, as the runner sent it. | |
| 400 | + | message JobLogLine { | |
| 401 | + | // Step is the number of the step the line belongs to. | |
| 402 | + | int32 step = 1; | |
| 403 | + | // Time is when the runner produced the line, with nanosecond precision. | |
| 404 | + | google.protobuf.Timestamp time = 2; | |
| 405 | + | // Text is the line without its terminator, as in LogLine.text, including the | |
| 406 | + | // "##[group]" and "##[endgroup]" markers. Clients show it escaped. | |
| 407 | + | string text = 3; | |
| 408 | + | } | |
| 409 | + | ||
| 410 | + | // CancelRunRequest names the run to cancel. | |
| 411 | + | message CancelRunRequest { | |
| 412 | + | // Repo is the repository name. | |
| 413 | + | string repo = 1; | |
| 414 | + | // RunId is the Run.id. | |
| 415 | + | int64 run_id = 2; | |
| 416 | + | } | |
| 417 | + | ||
| 418 | + | // CancelRunResponse holds the run after the request. | |
| 419 | + | message CancelRunResponse { | |
| 420 | + | // Run is the latest attempt of the run. | |
| 421 | + | Run run = 1; | |
| 422 | + | } | |
| 423 | + | ||
| 424 | + | // RerunRunRequest names the run to run again. | |
| 425 | + | message RerunRunRequest { | |
| 426 | + | // Repo is the repository name. | |
| 427 | + | string repo = 1; | |
| 428 | + | // RunId is the Run.id. | |
| 429 | + | int64 run_id = 2; | |
| 430 | + | // FailedOnly runs again only the jobs of the latest attempt that did not | |
| 431 | + | // succeed (conclusion FAILURE, CANCELLED, TIMED_OUT or ACTION_REQUIRED) and | |
| 432 | + | // the jobs that need them, directly or not, even if they were skipped. The | |
| 433 | + | // other jobs keep their result, steps and log from the earlier attempt. When | |
| 434 | + | // false, every job runs again. | |
| 435 | + | bool failed_only = 3; | |
| 436 | + | } | |
| 437 | + | ||
| 438 | + | // RerunRunResponse holds the new attempt. | |
| 439 | + | message RerunRunResponse { | |
| 440 | + | // Run is the run as of the new attempt, QUEUED. | |
| 441 | + | Run run = 1; | |
| 442 | + | } | |
| 443 | + | ||
| 444 | + | // ListChecksRequest names the commit. | |
| 445 | + | message ListChecksRequest { | |
| 446 | + | // Repo is the repository name. | |
| 447 | + | string repo = 1; | |
| 448 | + | // Sha is the full commit id (40 lowercase hexadecimal characters for SHA-1). | |
| 449 | + | // Abbreviations and names are invalid_argument. | |
| 450 | + | string sha = 2; | |
| 451 | + | } | |
| 452 | + | ||
| 453 | + | // ListChecksResponse holds the checks of a commit. | |
| 454 | + | message ListChecksResponse { | |
| 455 | + | // Checks are the checks of the commit, one per job of the latest attempt of | |
| 456 | + | // each run, ordered by creation time then name. | |
| 457 | + | repeated Check checks = 1; | |
| 458 | + | // Truncated is true when the commit has more checks than checks holds. | |
| 459 | + | bool truncated = 2; | |
| 460 | + | } | |
| 461 | + | ||
| 462 | + | // ListRunnersRequest selects a page of runners. | |
| 463 | + | message ListRunnersRequest { | |
| 464 | + | // PageSize is the maximum number of runners to return. | |
| 465 | + | int32 page_size = 1; | |
| 466 | + | // PageToken is the next_page_token of a previous response. Empty for the | |
| 467 | + | // first page. | |
| 468 | + | string page_token = 2; | |
| 469 | + | } | |
| 470 | + | ||
| 471 | + | // ListRunnersResponse is one page of runners. | |
| 472 | + | message ListRunnersResponse { | |
| 473 | + | // Runners are the runners of this page, ordered by name. | |
| 474 | + | repeated Runner runners = 1; | |
| 475 | + | // NextPageToken is passed as page_token to get the next page. Empty when | |
| 476 | + | // there is no next page. | |
| 477 | + | string next_page_token = 2; | |
| 478 | + | } | |
| 479 | + | ||
| 480 | + | // RemoveRunnerRequest names the runner to remove. | |
| 481 | + | message RemoveRunnerRequest { | |
| 482 | + | // RunnerId is the Runner.id. | |
| 483 | + | string runner_id = 1; | |
| 484 | + | } | |
| 485 | + | ||
| 486 | + | // RemoveRunnerResponse is empty. | |
| 487 | + | message RemoveRunnerResponse {} | |
| @@ -0,0 +1,487 @@ | |||||
| 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 | + | RunStatus 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 | + | Conclusion 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 | + | } | |||
| 72 | + | ||||
| 73 | + | // RunJob is a job of a run as people see it: the state of one job instance | |||
| 74 | + | // (one matrix combination is one job) in one attempt. It is not Job, which is | |||
| 75 | + | // what a runner receives. | |||
| 76 | + | message RunJob { | |||
| 77 | + | // Id is the Job.job_id. It is the same in every attempt. | |||
| 78 | + | string id = 1; | |||
| 79 | + | // RunId is the run the job belongs to. | |||
| 80 | + | int64 run_id = 2; | |||
| 81 | + | // Attempt is the attempt of the run in which this job executed. It is lower | |||
| 82 | + | // than the attempt of the run when the job was kept from an earlier attempt | |||
| 83 | + | // by a re-run of the failed jobs only; the log and steps are then those of | |||
| 84 | + | // that earlier attempt. Pass it to GetJob and GetJobLog. | |||
| 85 | + | int32 attempt = 3; | |||
| 86 | + | // Repo is the name of the repository. | |||
| 87 | + | string repo = 4; | |||
| 88 | + | // Key is the key of the job under "jobs:" in the workflow (the | |||
| 89 | + | // "github.job"). Matrix instances of one job share it. | |||
| 90 | + | string key = 5; | |||
| 91 | + | // Name is the display name of the job, evaluated, with the matrix suffix as | |||
| 92 | + | // GitHub names them, for example "build (linux, 1.27)". It is also the | |||
| 93 | + | // name of the job's check. | |||
| 94 | + | string name = 6; | |||
| 95 | + | // Status is where the job is. A job waits QUEUED for a runner and for the | |||
| 96 | + | // jobs it needs. | |||
| 97 | + | RunStatus status = 7; | |||
| 98 | + | // Conclusion is how the job ended. Unset until status is COMPLETED. | |||
| 99 | + | Conclusion conclusion = 8; | |||
| 100 | + | // Error explains a FAILURE, CANCELLED or TIMED_OUT that no step explains, in | |||
| 101 | + | // GitHub's words where it has them: the runner was lost ("The self-hosted | |||
| 102 | + | // runner: <name> lost communication with the server..."), it received a | |||
| 103 | + | // shutdown signal, no runner took the job within 24 hours, the log limit | |||
| 104 | + | // was reached, the workflow uses a feature that is not supported yet | |||
| 105 | + | // ("not supported yet: <feature>"). At most 4 KiB. Empty otherwise. | |||
| 106 | + | string error = 9; | |||
| 107 | + | // Labels are the runs-on labels of the job, evaluated. Empty while the job | |||
| 108 | + | // is not planned yet. | |||
| 109 | + | repeated string labels = 10; | |||
| 110 | + | // Needs are the keys of the jobs this job needs, for drawing the graph. | |||
| 111 | + | repeated string needs = 11; | |||
| 112 | + | // RunnerId is the id of the runner that executed or is executing the job. | |||
| 113 | + | // Empty while queued, and when the runner has since been removed (runner_name | |||
| 114 | + | // and isolation are kept). | |||
| 115 | + | string runner_id = 12; | |||
| 116 | + | // RunnerName is the name of that runner. Empty while queued. | |||
| 117 | + | string runner_name = 13; | |||
| 118 | + | // Isolation is how that runner isolated the job. UNSPECIFIED while queued. | |||
| 119 | + | RunnerIsolation isolation = 14; | |||
| 120 | + | // Steps are the steps reported so far, ordered by number, including the | |||
| 121 | + | // implicit "Set up job" and "Complete job". Empty while queued. Always | |||
| 122 | + | // set, including in JobUpdated events. | |||
| 123 | + | repeated Step steps = 15; | |||
| 124 | + | // CreatedAt is when the job was created (planned). | |||
| 125 | + | google.protobuf.Timestamp created_at = 16; | |||
| 126 | + | // StartedAt is when the job was given to a runner. Unset until then. | |||
| 127 | + | google.protobuf.Timestamp started_at = 17; | |||
| 128 | + | // CompletedAt is when the job completed. Unset until then. | |||
| 129 | + | google.protobuf.Timestamp completed_at = 18; | |||
| 130 | + | } | |||
| 131 | + | ||||
| 132 | + | // Step is one step of a job, with where its lines are in the job log. | |||
| 133 | + | message Step { | |||
| 134 | + | // Number is the 1-based number of the step in the job, the one the runner | |||
| 135 | + | // uses in UpdateJob and AppendLog. | |||
| 136 | + | int32 number = 1; | |||
| 137 | + | // Name is the display name of the step. | |||
| 138 | + | string name = 2; | |||
| 139 | + | // State is where the step is. | |||
| 140 | + | StepState state = 3; | |||
| 141 | + | // Conclusion is how the step ended. Unset until state is COMPLETED. | |||
| 142 | + | Conclusion conclusion = 4; | |||
| 143 | + | // StartedAt is when the step started. Unset while PENDING. | |||
| 144 | + | google.protobuf.Timestamp started_at = 5; | |||
| 145 | + | // CompletedAt is when the step ended. Unset until COMPLETED. | |||
| 146 | + | google.protobuf.Timestamp completed_at = 6; | |||
| 147 | + | // LogStart is the number of the first line of the step in the job log (the | |||
| 148 | + | // offset of GetJobLogRequest and AppendLogRequest), counted from 0. | |||
| 149 | + | int64 log_start = 7; | |||
| 150 | + | // LogEnd is the number of the line after the last one of the step. For an | |||
| 151 | + | // IN_PROGRESS step it is the number of lines held at the time of the | |||
| 152 | + | // call and grows; for a step without a line it equals log_start. | |||
| 153 | + | int64 log_end = 8; | |||
| 154 | + | } | |||
| 155 | + | ||||
| 156 | + | // Check is the result of one job on one commit, the unit shown next to a | |||
| 157 | + | // commit or a merge request and counted by the merge rules. There is one per | |||
| 158 | + | // job instance, named like GitHub's checks. | |||
| 159 | + | message Check { | |||
| 160 | + | // Id identifies the check. It is opaque and stable across attempts. | |||
| 161 | + | string id = 1; | |||
| 162 | + | // Repo is the name of the repository. | |||
| 163 | + | string repo = 2; | |||
| 164 | + | // Sha is the full id of the commit. | |||
| 165 | + | string sha = 3; | |||
| 166 | + | // Name is the name of the check: the RunJob.name of the job. | |||
| 167 | + | string name = 4; | |||
| 168 | + | // Status is where the check is. | |||
| 169 | + | RunStatus status = 5; | |||
| 170 | + | // Conclusion is how the check ended. Unset until status is COMPLETED. The | |||
| 171 | + | // value "stale" of GitHub is never used. | |||
| 172 | + | Conclusion conclusion = 6; | |||
| 173 | + | // RunId is the run of the job. | |||
| 174 | + | int64 run_id = 7; | |||
| 175 | + | // JobId is the RunJob.id of the job, to open its page and log. | |||
| 176 | + | string job_id = 8; | |||
| 177 | + | // Attempt is the attempt the check describes: the latest attempt of the run. | |||
| 178 | + | int32 attempt = 9; | |||
| 179 | + | // WorkflowName is the Run.workflow_name of the run. | |||
| 180 | + | string workflow_name = 10; | |||
| 181 | + | // StartedAt is when the job was given to a runner. Unset while queued. | |||
| 182 | + | google.protobuf.Timestamp started_at = 11; | |||
| 183 | + | // CompletedAt is when the job completed. Unset until then. | |||
| 184 | + | google.protobuf.Timestamp completed_at = 12; | |||
| 185 | + | } | |||
| 186 | + | ||||
| 187 | + | // Runner is a registered CI runner. It never contains the runner token nor its | |||
| 188 | + | // hash. | |||
| 189 | + | message Runner { | |||
| 190 | + | // Id identifies the runner in RemoveRunner. It is not a secret. | |||
| 191 | + | string id = 1; | |||
| 192 | + | // Name is the unique name chosen at registration. | |||
| 193 | + | string name = 2; | |||
| 194 | + | // Labels are all the labels the runner can match: the ones chosen at | |||
| 195 | + | // registration plus the implicit ones ("self-hosted", the os and the arch), | |||
| 196 | + | // lowercase, sorted. | |||
| 197 | + | repeated string labels = 3; | |||
| 198 | + | // Isolation is how the runner isolates jobs. | |||
| 199 | + | RunnerIsolation isolation = 4; | |||
| 200 | + | // Version is the version of the runner software. | |||
| 201 | + | string version = 5; | |||
| 202 | + | // Arch is "x64" or "arm64". | |||
| 203 | + | string arch = 6; | |||
| 204 | + | // Os is "linux". | |||
| 205 | + | string os = 7; | |||
| 206 | + | // RegisteredAt is when the runner was registered. | |||
| 207 | + | google.protobuf.Timestamp registered_at = 8; | |||
| 208 | + | // LastSeen is when the server last heard from the runner: a FetchJob poll or | |||
| 209 | + | // a heartbeat of one of its jobs. Unset if never. It may lag behind the real | |||
| 210 | + | // time by a short delay. A runner not seen for more than about 2 minutes is | |||
| 211 | + | // offline. | |||
| 212 | + | google.protobuf.Timestamp last_seen = 9; | |||
| 213 | + | // Busy is true while the runner has a job in progress. | |||
| 214 | + | bool busy = 10; | |||
| 215 | + | } | |||
| 216 | + | ||||
| 217 | + | // RunService reads and controls CI runs, like `gh run` does on GitHub. It is for | |||
| 218 | + | // owners and their tools; runners use RunnerService. | |||
| 219 | + | // | |||
| 220 | + | // Runs, jobs, checks and logs belong to a repository, and reading them needs | |||
| 221 | + | // the same access as reading it: nothing for a public repository, the | |||
| 222 | + | // repo:read scope for a private one. A repository that does not exist or that | |||
| 223 | + | // the caller cannot read gives not_found; so does a run or a job that belongs | |||
| 224 | + | // to another repository than the one named (ids are global but are always | |||
| 225 | + | // checked against the repository). Cancelling and re-running need the ci:write | |||
| 226 | + | // scope on the repository (a token restricted to other repositories gets | |||
| 227 | + | // permission_denied). The runner RPCs (ListRunners, RemoveRunner) are not about | |||
| 228 | + | // a repository and need the runner:register scope on a token that is not | |||
| 229 | + | // restricted to repositories, or the root token. A caller without the needed | |||
| 230 | + | // scope gets permission_denied. | |||
| 231 | + | // | |||
| 232 | + | // Page sizes and page tokens work as in the other services: page_size 0 is the | |||
| 233 | + | // server default, larger values are lowered to the server maximum, and a | |||
| 234 | + | // page_token is the next_page_token of a previous response for the same | |||
| 235 | + | // request (other fields must be unchanged). | |||
| 236 | + | // | |||
| 237 | + | // Servers that implement the runs side (every RPC but ListRunners and | |||
| 238 | + | // RemoveRunner) list "ci.runs" in GetVersionResponse.features; those that | |||
| 239 | + | // implement the runners side list "ci.runners", as RunnerService does. A server | |||
| 240 | + | // that honours the filters of ListRunsRequest also lists "ci.runs.filters". | |||
| 241 | + | service RunService { | |||
| 242 | + | // ListRuns lists the runs of a repository, highest id (newest) first. Each | |||
| 243 | + | // run is described by its latest attempt. The filters narrow the result; a | |||
| 244 | + | // server without the feature "ci.runs.filters" ignores them and returns | |||
| 245 | + | // every run, so a client that needs them checks the feature. Fails with | |||
| 246 | + | // invalid_argument if a filter is malformed. | |||
| 247 | + | rpc ListRuns(ListRunsRequest) returns (ListRunsResponse); | |||
| 248 | + | // GetRun returns a run with its jobs and their steps, for the latest attempt | |||
| 249 | + | // or for the one asked. Fails with not_found if the run or the attempt does | |||
| 250 | + | // not exist. | |||
| 251 | + | rpc GetRun(GetRunRequest) returns (GetRunResponse); | |||
| 252 | + | // GetJob returns one job with its steps. Fails with not_found if the job | |||
| 253 | + | // or the attempt does not exist. | |||
| 254 | + | rpc GetJob(GetJobRequest) returns (GetJobResponse); | |||
| 255 | + | // GetJobLog streams the lines of the log of a job, from a line number. The | |||
| 256 | + | // server sends them in order in messages of at most 1,000 lines and 64 KiB of | |||
| 257 | + | // text each. | |||
| 258 | + | // | |||
| 259 | + | // Without follow, the stream ends after the last line stored when the call | |||
| 260 | + | // was made. With follow, it goes on as lines arrive, with at most 1 second of | |||
| 261 | + | // delay after the runner's AppendLog, and ends without error once the job has | |||
| 262 | + | // completed and all its lines were sent; on a completed job follow changes | |||
| 263 | + | // nothing. A client that loses the stream calls again with the offset after | |||
| 264 | + | // the last line it received. The client may cancel at any time. | |||
| 265 | + | // | |||
| 266 | + | // Fails with not_found if the job, the attempt or its log does not exist (the | |||
| 267 | + | // log is removed by the retention sweep, and a queued job has none: with | |||
| 268 | + | // follow, the call waits for the first line instead), with out_of_range if | |||
| 269 | + | // offset is beyond the end of the log of a job without follow, and with | |||
| 270 | + | // resource_exhausted when there are too many concurrent log streams, in | |||
| 271 | + | // total or from one client. Like Watch, a stream ends with unauthenticated | |||
| 272 | + | // when the token was revoked or expired. | |||
| 273 | + | rpc GetJobLog(GetJobLogRequest) returns (stream GetJobLogResponse); | |||
| 274 | + | // CancelRun asks to cancel the latest attempt of a run. Needs the ci:write | |||
| 275 | + | // scope. Jobs that have not started are cancelled at once; jobs in progress | |||
| 276 | + | // get a CANCEL_REASON_USER request through WatchJob, and the run completes | |||
| 277 | + | // when they have ended, so the returned run may still be IN_PROGRESS. | |||
| 278 | + | // Cancelling a run that already has a cancellation requested succeeds and | |||
| 279 | + | // changes nothing. Fails with failed_precondition if the run has completed. | |||
| 280 | + | rpc CancelRun(CancelRunRequest) returns (CancelRunResponse); | |||
| 281 | + | // RerunRun starts a new attempt of a completed run: same run id and number, | |||
| 282 | + | // attempt + 1, same commit and same workflow file as at the original run (the | |||
| 283 | + | // file of the repository is not read again). Needs the ci:write scope. Fails | |||
| 284 | + | // with failed_precondition if the latest attempt has not completed, or if | |||
| 285 | + | // failed_only is set and no job failed. The new attempt is returned QUEUED. | |||
| 286 | + | rpc RerunRun(RerunRunRequest) returns (RerunRunResponse); | |||
| 287 | + | // ListChecks lists the checks of a commit: for each job of each run on that | |||
| 288 | + | // sha, the one of the latest attempt, ordered by creation time then name. | |||
| 289 | + | // A commit without a run has no check: the list is empty, which is not an | |||
| 290 | + | // error, and the server does not verify that the commit exists. Returns at | |||
| 291 | + | // most 1,000 checks (truncated tells). | |||
| 292 | + | rpc ListChecks(ListChecksRequest) returns (ListChecksResponse); | |||
| 293 | + | // ListRunners lists the registered runners, ordered by name. Needs the | |||
| 294 | + | // runner:register scope. | |||
| 295 | + | rpc ListRunners(ListRunnersRequest) returns (ListRunnersResponse); | |||
| 296 | + | // RemoveRunner removes a runner: its runner token stops working at once (a | |||
| 297 | + | // FetchJob in progress ends with unauthenticated) and it no longer gets | |||
| 298 | + | // jobs. A job it is running is not affected: it ends normally, or fails as | |||
| 299 | + | // a lost runner without heartbeats. Needs the runner:register scope. Fails | |||
| 300 | + | // with not_found if the id is unknown. | |||
| 301 | + | rpc RemoveRunner(RemoveRunnerRequest) returns (RemoveRunnerResponse); | |||
| 302 | + | } | |||
| 303 | + | ||||
| 304 | + | // ListRunsRequest selects a page of runs. | |||
| 305 | + | message ListRunsRequest { | |||
| 306 | + | // Repo is the repository name. | |||
| 307 | + | string repo = 1; | |||
| 308 | + | // PageSize is the maximum number of runs to return. | |||
| 309 | + | int32 page_size = 2; | |||
| 310 | + | // PageToken is the next_page_token of a previous response. Empty for the | |||
| 311 | + | // first page. | |||
| 312 | + | string page_token = 3; | |||
| 313 | + | // Workflow restricts the result to one workflow file: its path | |||
| 314 | + | // (".github/workflows/ci.yml") or its file name ("ci.yml"). Empty means | |||
| 315 | + | // every workflow. Ignored by servers without "ci.runs.filters". | |||
| 316 | + | string workflow = 4; | |||
| 317 | + | // Branch restricts the result to the runs whose ref is this branch, by | |||
| 318 | + | // short name ("main"). Empty means every ref. Ignored by servers without | |||
| 319 | + | // "ci.runs.filters". | |||
| 320 | + | string branch = 5; | |||
| 321 | + | // Status restricts the result to the runs whose latest attempt has this | |||
| 322 | + | // status. RUN_STATUS_UNSPECIFIED means every status. Ignored by servers | |||
| 323 | + | // without "ci.runs.filters". | |||
| 324 | + | RunStatus status = 6; | |||
| 325 | + | } | |||
| 326 | + | ||||
| 327 | + | // ListRunsResponse is one page of runs. | |||
| 328 | + | message ListRunsResponse { | |||
| 329 | + | // Runs are the runs of this page, highest id first, without jobs. | |||
| 330 | + | repeated Run runs = 1; | |||
| 331 | + | // NextPageToken is passed as page_token to get the next page. Empty when | |||
| 332 | + | // there is no next page. | |||
| 333 | + | string next_page_token = 2; | |||
| 334 | + | } | |||
| 335 | + | ||||
| 336 | + | // GetRunRequest selects a run. | |||
| 337 | + | message GetRunRequest { | |||
| 338 | + | // Repo is the repository name. | |||
| 339 | + | string repo = 1; | |||
| 340 | + | // RunId is the Run.id. | |||
| 341 | + | int64 run_id = 2; | |||
| 342 | + | // Attempt is the attempt to read. 0 means the latest. | |||
| 343 | + | int32 attempt = 3; | |||
| 344 | + | } | |||
| 345 | + | ||||
| 346 | + | // GetRunResponse holds a run with its jobs. | |||
| 347 | + | message GetRunResponse { | |||
| 348 | + | // Run is the run, as of the attempt asked. | |||
| 349 | + | Run run = 1; | |||
| 350 | + | // Jobs are the jobs of that attempt, with their steps, in the order of the | |||
| 351 | + | // workflow file (matrix instances in the order of the matrix). Jobs kept from | |||
| 352 | + | // an earlier attempt by a re-run of failed jobs are included, with their own | |||
| 353 | + | // RunJob.attempt. Empty for a run that failed on an invalid workflow file. | |||
| 354 | + | repeated RunJob jobs = 2; | |||
| 355 | + | // LatestAttempt is the latest attempt of the run, to offer the other ones. | |||
| 356 | + | int32 latest_attempt = 3; | |||
| 357 | + | } | |||
| 358 | + | ||||
| 359 | + | // GetJobRequest selects a job. | |||
| 360 | + | message GetJobRequest { | |||
| 361 | + | // Repo is the repository name. | |||
| 362 | + | string repo = 1; | |||
| 363 | + | // JobId is the RunJob.id. | |||
| 364 | + | string job_id = 2; | |||
| 365 | + | // Attempt is the RunJob.attempt to read. 0 means the latest. | |||
| 366 | + | int32 attempt = 3; | |||
| 367 | + | } | |||
| 368 | + | ||||
| 369 | + | // GetJobResponse holds a job. | |||
| 370 | + | message GetJobResponse { | |||
| 371 | + | // Job is the job with its steps. | |||
| 372 | + | RunJob job = 1; | |||
| 373 | + | } | |||
| 374 | + | ||||
| 375 | + | // GetJobLogRequest selects the lines to stream. | |||
| 376 | + | message GetJobLogRequest { | |||
| 377 | + | // Repo is the repository name. | |||
| 378 | + | string repo = 1; | |||
| 379 | + | // JobId is the RunJob.id. | |||
| 380 | + | string job_id = 2; | |||
| 381 | + | // Attempt is the RunJob.attempt whose log to read. 0 means the latest. | |||
| 382 | + | int32 attempt = 3; | |||
| 383 | + | // Offset is the number of the first line to send, counted from 0 across all | |||
| 384 | + | // steps (see Step.log_start). Negative is invalid_argument. | |||
| 385 | + | int64 offset = 4; | |||
| 386 | + | // Follow keeps the stream open while the job runs. See GetJobLog. | |||
| 387 | + | bool follow = 5; | |||
| 388 | + | } | |||
| 389 | + | ||||
| 390 | + | // GetJobLogResponse is one message of the log stream. | |||
| 391 | + | message GetJobLogResponse { | |||
| 392 | + | // Offset is the number of the first line of lines. | |||
| 393 | + | int64 offset = 1; | |||
| 394 | + | // Lines are consecutive lines of the log, at most 1,000, at most 64 KiB of | |||
| 395 | + | // text in total. Never empty. | |||
| 396 | + | repeated JobLogLine lines = 2; | |||
| 397 | + | } | |||
| 398 | + | ||||
| 399 | + | // JobLogLine is one line of a job log, as the runner sent it. | |||
| 400 | + | message JobLogLine { | |||
| 401 | + | // Step is the number of the step the line belongs to. | |||
| 402 | + | int32 step = 1; | |||
| 403 | + | // Time is when the runner produced the line, with nanosecond precision. | |||
| 404 | + | google.protobuf.Timestamp time = 2; | |||
| 405 | + | // Text is the line without its terminator, as in LogLine.text, including the | |||
| 406 | + | // "##[group]" and "##[endgroup]" markers. Clients show it escaped. | |||
| 407 | + | string text = 3; | |||
| 408 | + | } | |||
| 409 | + | ||||
| 410 | + | // CancelRunRequest names the run to cancel. | |||
| 411 | + | message CancelRunRequest { | |||
| 412 | + | // Repo is the repository name. | |||
| 413 | + | string repo = 1; | |||
| 414 | + | // RunId is the Run.id. | |||
| 415 | + | int64 run_id = 2; | |||
| 416 | + | } | |||
| 417 | + | ||||
| 418 | + | // CancelRunResponse holds the run after the request. | |||
| 419 | + | message CancelRunResponse { | |||
| 420 | + | // Run is the latest attempt of the run. | |||
| 421 | + | Run run = 1; | |||
| 422 | + | } | |||
| 423 | + | ||||
| 424 | + | // RerunRunRequest names the run to run again. | |||
| 425 | + | message RerunRunRequest { | |||
| 426 | + | // Repo is the repository name. | |||
| 427 | + | string repo = 1; | |||
| 428 | + | // RunId is the Run.id. | |||
| 429 | + | int64 run_id = 2; | |||
| 430 | + | // FailedOnly runs again only the jobs of the latest attempt that did not | |||
| 431 | + | // succeed (conclusion FAILURE, CANCELLED, TIMED_OUT or ACTION_REQUIRED) and | |||
| 432 | + | // the jobs that need them, directly or not, even if they were skipped. The | |||
| 433 | + | // other jobs keep their result, steps and log from the earlier attempt. When | |||
| 434 | + | // false, every job runs again. | |||
| 435 | + | bool failed_only = 3; | |||
| 436 | + | } | |||
| 437 | + | ||||
| 438 | + | // RerunRunResponse holds the new attempt. | |||
| 439 | + | message RerunRunResponse { | |||
| 440 | + | // Run is the run as of the new attempt, QUEUED. | |||
| 441 | + | Run run = 1; | |||
| 442 | + | } | |||
| 443 | + | ||||
| 444 | + | // ListChecksRequest names the commit. | |||
| 445 | + | message ListChecksRequest { | |||
| 446 | + | // Repo is the repository name. | |||
| 447 | + | string repo = 1; | |||
| 448 | + | // Sha is the full commit id (40 lowercase hexadecimal characters for SHA-1). | |||
| 449 | + | // Abbreviations and names are invalid_argument. | |||
| 450 | + | string sha = 2; | |||
| 451 | + | } | |||
| 452 | + | ||||
| 453 | + | // ListChecksResponse holds the checks of a commit. | |||
| 454 | + | message ListChecksResponse { | |||
| 455 | + | // Checks are the checks of the commit, one per job of the latest attempt of | |||
| 456 | + | // each run, ordered by creation time then name. | |||
| 457 | + | repeated Check checks = 1; | |||
| 458 | + | // Truncated is true when the commit has more checks than checks holds. | |||
| 459 | + | bool truncated = 2; | |||
| 460 | + | } | |||
| 461 | + | ||||
| 462 | + | // ListRunnersRequest selects a page of runners. | |||
| 463 | + | message ListRunnersRequest { | |||
| 464 | + | // PageSize is the maximum number of runners to return. | |||
| 465 | + | int32 page_size = 1; | |||
| 466 | + | // PageToken is the next_page_token of a previous response. Empty for the | |||
| 467 | + | // first page. | |||
| 468 | + | string page_token = 2; | |||
| 469 | + | } | |||
| 470 | + | ||||
| 471 | + | // ListRunnersResponse is one page of runners. | |||
| 472 | + | message ListRunnersResponse { | |||
| 473 | + | // Runners are the runners of this page, ordered by name. | |||
| 474 | + | repeated Runner runners = 1; | |||
| 475 | + | // NextPageToken is passed as page_token to get the next page. Empty when | |||
| 476 | + | // there is no next page. | |||
| 477 | + | string next_page_token = 2; | |||
| 478 | + | } | |||
| 479 | + | ||||
| 480 | + | // RemoveRunnerRequest names the runner to remove. | |||
| 481 | + | message RemoveRunnerRequest { | |||
| 482 | + | // RunnerId is the Runner.id. | |||
| 483 | + | string runner_id = 1; | |||
| 484 | + | } | |||
| 485 | + | ||||
| 486 | + | // RemoveRunnerResponse is empty. | |||
| 487 | + | message RemoveRunnerResponse {} | |||
A proto/ermite/runner.proto +450 -0
| @@ -0,0 +1,450 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | syntax = "proto3"; | |
| 4 | + | ||
| 5 | + | package ermite; | |
| 6 | + | ||
| 7 | + | import "google/protobuf/timestamp.proto"; | |
| 8 | + | ||
| 9 | + | // RunStatus is where a run, a job or a check is in its life. It is the same | |
| 10 | + | // as GitHub's status, without the rarely used values. | |
| 11 | + | enum RunStatus { | |
| 12 | + | // Not set. In a filter it means every status. Never returned by the server. | |
| 13 | + | RUN_STATUS_UNSPECIFIED = 0; | |
| 14 | + | // Waiting: for a runner, for the jobs it needs, or for its turn | |
| 15 | + | // (max-parallel). | |
| 16 | + | RUN_STATUS_QUEUED = 1; | |
| 17 | + | // At least one job has been given to a runner and has not ended. | |
| 18 | + | RUN_STATUS_IN_PROGRESS = 2; | |
| 19 | + | // Over. The conclusion says how it ended. | |
| 20 | + | RUN_STATUS_COMPLETED = 3; | |
| 21 | + | } | |
| 22 | + | ||
| 23 | + | // Conclusion is how a run, a job, a step or a check ended, with GitHub's | |
| 24 | + | // values and meaning. It is unset (CONCLUSION_UNSPECIFIED) until the status is | |
| 25 | + | // COMPLETED. | |
| 26 | + | enum Conclusion { | |
| 27 | + | // Not completed yet. Never returned by the server for a completed item. | |
| 28 | + | CONCLUSION_UNSPECIFIED = 0; | |
| 29 | + | // Everything it ran succeeded. | |
| 30 | + | CONCLUSION_SUCCESS = 1; | |
| 31 | + | // A step failed, or the job failed for another reason described in | |
| 32 | + | // RunJob.error (runner lost, queue timeout, log limit, feature not | |
| 33 | + | // supported yet, invalid workflow file). | |
| 34 | + | CONCLUSION_FAILURE = 2; | |
| 35 | + | // Cancelled by the owner, or by fail-fast, or because the runner shut down. | |
| 36 | + | CONCLUSION_CANCELLED = 3; | |
| 37 | + | // Never ran: its "if" was false, or a job it needs did not succeed, or it was | |
| 38 | + | // not selected. Never reported by a runner. | |
| 39 | + | CONCLUSION_SKIPPED = 4; | |
| 40 | + | // Exceeded its timeout. | |
| 41 | + | CONCLUSION_TIMED_OUT = 5; | |
| 42 | + | // Neither a success nor a failure. Defined for parity with GitHub; no | |
| 43 | + | // workflow job ends this way yet. For merging it counts as a success. | |
| 44 | + | CONCLUSION_NEUTRAL = 6; | |
| 45 | + | // Waits for a human action (for example the approval of an environment). | |
| 46 | + | // Defined for parity with GitHub; the server does not produce it before | |
| 47 | + | // environments exist. For merging it counts as a failure. | |
| 48 | + | CONCLUSION_ACTION_REQUIRED = 7; | |
| 49 | + | } | |
| 50 | + | ||
| 51 | + | // StepState is where a step is in its life. | |
| 52 | + | enum StepState { | |
| 53 | + | // Not set. Rejected in requests, never returned by the server. | |
| 54 | + | STEP_STATE_UNSPECIFIED = 0; | |
| 55 | + | // Known but not started. | |
| 56 | + | STEP_STATE_PENDING = 1; | |
| 57 | + | // Running. | |
| 58 | + | STEP_STATE_IN_PROGRESS = 2; | |
| 59 | + | // Over. The conclusion says how it ended. A step that did not run because | |
| 60 | + | // of its "if" or an earlier failure is COMPLETED with CONCLUSION_SKIPPED. | |
| 61 | + | STEP_STATE_COMPLETED = 3; | |
| 62 | + | } | |
| 63 | + | ||
| 64 | + | // RunnerIsolation is how a runner isolates each job from the host and from | |
| 65 | + | // other jobs. The runner chooses it (auto-detection included) and reports it | |
| 66 | + | // at registration; the job page shows it. | |
| 67 | + | enum RunnerIsolation { | |
| 68 | + | // Not set. Rejected at registration, never returned by the server. | |
| 69 | + | RUNNER_ISOLATION_UNSPECIFIED = 0; | |
| 70 | + | // One micro-VM per job, destroyed at the end. | |
| 71 | + | RUNNER_ISOLATION_VM = 1; | |
| 72 | + | // One container per job with its own Docker daemon, run by the sysbox | |
| 73 | + | // runtime. Shares the host kernel. | |
| 74 | + | RUNNER_ISOLATION_CONTAINER_SYSBOX = 2; | |
| 75 | + | // One privileged container per job with its own Docker daemon. Shares the | |
| 76 | + | // host kernel and is equivalent to root on the host: the weakest mode. | |
| 77 | + | RUNNER_ISOLATION_CONTAINER_PRIVILEGED = 3; | |
| 78 | + | } | |
| 79 | + | ||
| 80 | + | // CancelReason says why the server asks a runner to stop a job. | |
| 81 | + | enum CancelReason { | |
| 82 | + | // Not set. A WatchJobResponse with this reason is a keep-alive and carries | |
| 83 | + | // no request. | |
| 84 | + | CANCEL_REASON_UNSPECIFIED = 0; | |
| 85 | + | // The owner cancelled the run (RunService.CancelRun, web or CLI). The job | |
| 86 | + | // ends 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 CONCLUSION_CANCELLED. | |
| 90 | + | CANCEL_REASON_FAIL_FAST = 2; | |
| 91 | + | // The job exceeded its timeout_minutes without the runner ending it. The | |
| 92 | + | // job ends 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: | |
| 100 | + | // | |
| 101 | + | // - RegisterRunner: a stored token with the runner:register scope, or the | |
| 102 | + | // root token, as "Authorization: Bearer <secret>". | |
| 103 | + | // - FetchJob: the runner token returned by RegisterRunner, as | |
| 104 | + | // "Authorization: Bearer <runner_token>". It identifies the runner. It | |
| 105 | + | // only works for FetchJob. | |
| 106 | + | // - UpdateJob, AppendLog, Heartbeat, WatchJob: the job token of the Job that | |
| 107 | + | // FetchJob returned, as "Authorization: Bearer <job_token>". It is valid | |
| 108 | + | // for that one job and attempt, from its assignment until it ends. The | |
| 109 | + | // job_id of the request must be the one of the token. | |
| 110 | + | // | |
| 111 | + | // A request without a credential, with an unknown, expired or revoked one, or | |
| 112 | + | // with a credential of the wrong kind for the RPC, gets unauthenticated. A | |
| 113 | + | // job token that is valid but belongs to another job than the job_id of the | |
| 114 | + | // request gets permission_denied. A job that has ended (concluded, cancelled, | |
| 115 | + | // failed by the server, or whose runner was lost) no longer has a valid job | |
| 116 | + | // token: every later call gets unauthenticated, which a runner treats as "the | |
| 117 | + | // job is over" and stops sending anything for it. Repeated failed | |
| 118 | + | // authentications are rate limited per client address. | |
| 119 | + | // | |
| 120 | + | // Runner tokens and job tokens are secrets: the server never logs them and | |
| 121 | + | // clients must not log them. The job token stays in the runner host process; it | |
| 122 | + | // is never given to the sandbox that runs the job's code. | |
| 123 | + | // | |
| 124 | + | // Servers that implement this service list "ci.runners" in | |
| 125 | + | // GetVersionResponse.features. | |
| 126 | + | service RunnerService { | |
| 127 | + | // RegisterRunner registers a runner and returns its runner token, which is | |
| 128 | + | // shown only in this response; the runner keeps it. The caller needs the | |
| 129 | + | // runner:register scope on a token that is not restricted to repositories | |
| 130 | + | // (a runner serves every repository), or the root token. Fails with | |
| 131 | + | // unauthenticated without a valid token, permission_denied without the | |
| 132 | + | // scope or with a repository-restricted token, invalid_argument if a field | |
| 133 | + | // breaks a rule, and already_exists if name is taken by another runner. | |
| 134 | + | rpc RegisterRunner(RegisterRunnerRequest) returns (RegisterRunnerResponse); | |
| 135 | + | // FetchJob asks for a job to run (long poll). The server holds the call | |
| 136 | + | // until a queued job matches the runner, or about 50 seconds (never more | |
| 137 | + | // than 50) pass, then answers; without a job the response has no job and the | |
| 138 | + | // runner calls again at once. Clients set a call timeout above 50 seconds | |
| 139 | + | // (60 is suggested) and must not poll when they have no free capacity. | |
| 140 | + | // | |
| 141 | + | // A job matches when every label of its runs-on is among the labels of the | |
| 142 | + | // runner (case-insensitive). A job is given to exactly one runner, even when | |
| 143 | + | // many runners and many polls compete; the assignment is recorded before | |
| 144 | + | // the response is sent, and the job is then in progress. If the response is | |
| 145 | + | // lost the job is not offered again: with no Heartbeat for 2 minutes the | |
| 146 | + | // server fails it as a lost runner. | |
| 147 | + | // | |
| 148 | + | // Needs the runner token. Fails with unauthenticated when the runner was | |
| 149 | + | // removed (also ending a call in progress) and invalid_argument if labels | |
| 150 | + | // breaks a rule. | |
| 151 | + | rpc FetchJob(FetchJobRequest) returns (FetchJobResponse); | |
| 152 | + | // UpdateJob reports the state of the steps of a job and, once, how the job | |
| 153 | + | // ended. Needs the job token. Each call is applied whole or not at all, and | |
| 154 | + | // repeating an identical call changes nothing, so a runner may retry after a | |
| 155 | + | // network error. | |
| 156 | + | // | |
| 157 | + | // Fails with invalid_argument if a rule is broken (including the output | |
| 158 | + | // limits), and with failed_precondition if a step goes back (a COMPLETED | |
| 159 | + | // step cannot change, a step cannot go from IN_PROGRESS to PENDING). | |
| 160 | + | // The call that carries a conclusion ends the job: its job token stops | |
| 161 | + | // working, the server completes the job and no further call is accepted, | |
| 162 | + | // so a retry of that last call after a lost response gets unauthenticated, | |
| 163 | + | // which means it was applied. | |
| 164 | + | rpc UpdateJob(UpdateJobRequest) returns (UpdateJobResponse); | |
| 165 | + | // AppendLog appends lines to the log of a job. Needs the job token. | |
| 166 | + | // | |
| 167 | + | // The log of a job is one sequence of lines, numbered from 0 across all | |
| 168 | + | // steps; offset is the number of lines the server holds when the request is | |
| 169 | + | // applied, that is the number of the first line of the request. Calls must | |
| 170 | + | // go in order, one at a time, and are idempotent: | |
| 171 | + | // | |
| 172 | + | // - offset equal to the server's line count: the lines are appended; | |
| 173 | + | // - offset below it: the first (count - offset) lines were already stored | |
| 174 | + | // (a retry after a lost response) and are skipped, and the rest is | |
| 175 | + | // appended; a request entirely stored changes nothing; | |
| 176 | + | // - offset above it: out_of_range (a gap is a bug of the client; the | |
| 177 | + | // message gives the line count the server holds). | |
| 178 | + | // | |
| 179 | + | // The response gives the line count after the call, so a client can check | |
| 180 | + | // itself. A request has at most 1,000 lines whose text adds up to at most | |
| 181 | + | // 64 KiB (65,536 bytes), all in the same step; more is invalid_argument. A | |
| 182 | + | // line of more than 64 KiB is cut by the client into several lines. | |
| 183 | + | // | |
| 184 | + | // The log of a job is limited to 512 MiB (536,870,912 bytes) as stored by | |
| 185 | + | // the server (text, timestamps and separators). The call that would exceed | |
| 186 | + | // it appends nothing; the server adds a last line saying that the limit was | |
| 187 | + | // reached, ends the job with CONCLUSION_FAILURE and RunJob.error set, and | |
| 188 | + | // answers resource_exhausted. The runner then stops the job and sends | |
| 189 | + | // nothing more: its calls get unauthenticated. | |
| 190 | + | // | |
| 191 | + | // Fails with invalid_argument if a rule is broken (step number, text with a | |
| 192 | + | // line feed or invalid UTF-8, missing time). | |
| 193 | + | rpc AppendLog(AppendLogRequest) returns (AppendLogResponse); | |
| 194 | + | // Heartbeat tells the server that the runner is alive and still working on | |
| 195 | + | // the job. Needs the job token. The runner calls it every 30 seconds for as | |
| 196 | + | // long as the job runs, from assignment to the end. Without a heartbeat for | |
| 197 | + | // 2 minutes the server ends the job with CONCLUSION_FAILURE and the message | |
| 198 | + | // "The self-hosted runner: <name> lost communication with the server...". | |
| 199 | + | // Only Heartbeat counts: other calls do not keep a job alive. | |
| 200 | + | rpc Heartbeat(HeartbeatRequest) returns (HeartbeatResponse); | |
| 201 | + | // WatchJob streams the requests to stop a job. Needs the job token. The | |
| 202 | + | // runner opens it when it starts the job and keeps it open; it reopens it | |
| 203 | + | // after any error other than unauthenticated. | |
| 204 | + | // | |
| 205 | + | // When the stream opens and a cancellation has already been requested, the | |
| 206 | + | // first message is that request, so a reconnect never misses one. Only the | |
| 207 | + | // first reason is sent: a job already cancelled by the owner does not get a | |
| 208 | + | // second message when its timeout expires. The server also sends a | |
| 209 | + | // keep-alive message (reason CANCEL_REASON_UNSPECIFIED) at least every 30 | |
| 210 | + | // seconds, which the client ignores. The stream ends without error when | |
| 211 | + | // the job ends. The runner stops the job as the official runner does | |
| 212 | + | // (interrupt, then terminate, then kill) and reports the conclusion given | |
| 213 | + | // for the reason with UpdateJob. | |
| 214 | + | rpc WatchJob(WatchJobRequest) returns (stream WatchJobResponse); | |
| 215 | + | } | |
| 216 | + | ||
| 217 | + | // RegisterRunnerRequest describes the runner to register. | |
| 218 | + | message RegisterRunnerRequest { | |
| 219 | + | // Name identifies the runner for people (job page, logs, ListRunners) and is | |
| 220 | + | // unique: 1 to 64 characters among letters, digits, ".", "_" and "-". It is | |
| 221 | + | // not a label and never matches runs-on. | |
| 222 | + | string name = 1; | |
| 223 | + | // Labels are the labels the runner adds to its implicit ones: at most 64, | |
| 224 | + | // each 1 to 64 characters among letters, digits, ".", "_" and "-", for | |
| 225 | + | // example "ubuntu-latest". Comparison is case-insensitive and duplicates | |
| 226 | + | // are removed. The server adds the implicit labels "self-hosted", the os | |
| 227 | + | // and the arch ("linux", "x64" or "arm64"); the runner name is not a label. | |
| 228 | + | repeated string labels = 2; | |
| 229 | + | // Version is the release of the runner, for example "v0.1.0", or "devel". | |
| 230 | + | // Informational, at most 64 characters. | |
| 231 | + | string version = 3; | |
| 232 | + | // Isolation is how the runner isolates jobs. Required. | |
| 233 | + | RunnerIsolation isolation = 4; | |
| 234 | + | // Arch is the CPU architecture, in GitHub's names: "x64" or "arm64". Other | |
| 235 | + | // values are invalid_argument (the set may grow with compatible additions). | |
| 236 | + | string arch = 5; | |
| 237 | + | // Os is the operating system: "linux". Other values are invalid_argument | |
| 238 | + | // (the set may grow with compatible additions). | |
| 239 | + | string os = 6; | |
| 240 | + | } | |
| 241 | + | ||
| 242 | + | // RegisterRunnerResponse holds the new runner and its secret. | |
| 243 | + | message RegisterRunnerResponse { | |
| 244 | + | // RunnerId identifies the runner in ListRunners and RemoveRunner. It is not | |
| 245 | + | // a secret. | |
| 246 | + | string runner_id = 1; | |
| 247 | + | // RunnerToken is the secret of the runner, to present as "Authorization: | |
| 248 | + | // Bearer <runner_token>" to FetchJob. It starts with "ermiterun_" and is | |
| 249 | + | // returned once: the server keeps only its SHA-256, the runner must store | |
| 250 | + | // it (mode 0600). It stops working when the runner is removed. | |
| 251 | + | string runner_token = 2; | |
| 252 | + | } | |
| 253 | + | ||
| 254 | + | // FetchJobRequest asks for a job. | |
| 255 | + | message FetchJobRequest { | |
| 256 | + | // Labels narrows the labels offered for this call. Empty means all the | |
| 257 | + | // labels of the runner, which is the normal case. Otherwise each one must be | |
| 258 | + | // a label of the runner (invalid_argument if not) and only jobs whose runs-on | |
| 259 | + | // labels are all among them are offered. | |
| 260 | + | repeated string labels = 1; | |
| 261 | + | } | |
| 262 | + | ||
| 263 | + | // FetchJobResponse holds the job given to the runner, if any. | |
| 264 | + | message FetchJobResponse { | |
| 265 | + | // Job is the job to run. Unset when no job was available before the end of | |
| 266 | + | // the poll. | |
| 267 | + | Job job = 1; | |
| 268 | + | } | |
| 269 | + | ||
| 270 | + | // Job is a job given to a runner: everything needed to run it, nothing more. | |
| 271 | + | // The runner parses workflow_yaml itself; the protocol does not carry the | |
| 272 | + | // workflow model, so it does not change when the model grows. | |
| 273 | + | message Job { | |
| 274 | + | // JobId identifies the job on the server. It is opaque, unique across | |
| 275 | + | // repositories and runs, and the same for every attempt of the job. With | |
| 276 | + | // attempt it names one execution. | |
| 277 | + | string job_id = 1; | |
| 278 | + | // RunId is the run the job belongs to, the "github.run_id". It is global to | |
| 279 | + | // the server. | |
| 280 | + | int64 run_id = 2; | |
| 281 | + | // Attempt is the attempt of the run this job executes: 1 for the first, and | |
| 282 | + | // one more after each RunService.RerunRun. It is the "github.run_attempt". | |
| 283 | + | int32 attempt = 3; | |
| 284 | + | // JobToken is the secret that authenticates this job's UpdateJob, | |
| 285 | + | // AppendLog, Heartbeat and WatchJob calls ("Authorization: Bearer | |
| 286 | + | // <job_token>"). It is random, valid until the job ends, survives a server | |
| 287 | + | // restart and is shown only in this response. It must stay in the runner | |
| 288 | + | // host process and never reach the job's code. | |
| 289 | + | string job_token = 4; | |
| 290 | + | // WorkflowPath is the path of the workflow file in the repository, for | |
| 291 | + | // example ".github/workflows/ci.yml". | |
| 292 | + | string workflow_path = 5; | |
| 293 | + | // WorkflowYaml is the content of that file at sha, as is, at most 1 MiB | |
| 294 | + | // (larger files fail at planning). | |
| 295 | + | string workflow_yaml = 6; | |
| 296 | + | // JobKey is the key of the job under "jobs:" in the workflow, the | |
| 297 | + | // "github.job". | |
| 298 | + | string job_key = 7; | |
| 299 | + | // Matrix is the JSON object of the matrix values of this instance of the | |
| 300 | + | // job, for example {"os":"linux","go":"1.27"}. "{}" when the job has no | |
| 301 | + | // matrix. It is the "matrix" context. | |
| 302 | + | string matrix = 8; | |
| 303 | + | // Contexts is a JSON object with the contexts that the server computed for | |
| 304 | + | // this job: "github", "needs", "strategy", "matrix", "inputs" and "vars", in | |
| 305 | + | // the shape GitHub gives them. The contexts "job", "runner", "steps" and | |
| 306 | + | // "env" belong to the runner and are not in it. Other keys may be added | |
| 307 | + | // later; clients ignore those they do not know. | |
| 308 | + | string contexts = 9; | |
| 309 | + | // Labels are the runs-on labels of the job, evaluated. | |
| 310 | + | repeated string labels = 10; | |
| 311 | + | // TimeoutMinutes is the timeout of the job in minutes: the value of the | |
| 312 | + | // workflow's timeout-minutes, or 360 when it has none. At least 1. The | |
| 313 | + | // runner enforces it from the start of the job; the server ends a job that | |
| 314 | + | // exceeds it by announcing CANCEL_REASON_TIMEOUT. | |
| 315 | + | int32 timeout_minutes = 11; | |
| 316 | + | // Repo is the name of the repository. | |
| 317 | + | string repo = 12; | |
| 318 | + | // Sha is the full id of the commit the run is for, 40 lowercase | |
| 319 | + | // hexadecimal characters for SHA-1. The runner reads the code at it. | |
| 320 | + | string sha = 13; | |
| 321 | + | // Ref is the full ref name that triggered the run, for example | |
| 322 | + | // "refs/heads/main" or "refs/tags/v1.0.0". | |
| 323 | + | string ref = 14; | |
| 324 | + | } | |
| 325 | + | ||
| 326 | + | // UpdateJobRequest reports the steps and, in the last call, the end of a job. | |
| 327 | + | message UpdateJobRequest { | |
| 328 | + | // JobId is the Job.job_id. It must be the job of the token. | |
| 329 | + | string job_id = 1; | |
| 330 | + | // Steps are the steps whose state changed or that are new. Each one is a | |
| 331 | + | // full snapshot of its step, applied by number. At most 1,000 steps per job | |
| 332 | + | // (invalid_argument beyond). Steps are numbered like GitHub's log: the | |
| 333 | + | // implicit "Set up job" is 1, the steps of the workflow follow in order, and | |
| 334 | + | // the implicit "Complete job" is last. | |
| 335 | + | repeated StepUpdate steps = 2; | |
| 336 | + | // Outputs are the outputs of the job, evaluated by the runner. Only allowed | |
| 337 | + | // in the call that carries the conclusion (invalid_argument otherwise). | |
| 338 | + | // Names are 1 to 255 characters among letters, digits, "_" and "-" and | |
| 339 | + | // begin with a letter or "_" and are case-insensitive: two names that | |
| 340 | + | // differ only by case are invalid_argument. Each value is at most 1 MiB, and | |
| 341 | + | // all names and values together at most 1 MiB (1,048,576 bytes); beyond that | |
| 342 | + | // the whole call is rejected with invalid_argument and the runner reports | |
| 343 | + | // the failure of the job without outputs, with a line in the log. | |
| 344 | + | map<string, string> outputs = 3; | |
| 345 | + | // Conclusion ends the job when set. A runner reports CONCLUSION_SUCCESS, | |
| 346 | + | // CONCLUSION_FAILURE, CONCLUSION_CANCELLED or CONCLUSION_TIMED_OUT; any | |
| 347 | + | // other value is invalid_argument. CONCLUSION_UNSPECIFIED means the job goes | |
| 348 | + | // on. Steps that were not reported COMPLETED when the job ends are | |
| 349 | + | // completed by the server as CONCLUSION_CANCELLED (or SKIPPED when they | |
| 350 | + | // never started and the job did not fail by cancellation). | |
| 351 | + | Conclusion conclusion = 4; | |
| 352 | + | // RunnerShutdown is true when the runner ends the job because it was told to | |
| 353 | + | // stop (service stopped, signal). Only allowed with CONCLUSION_CANCELLED. | |
| 354 | + | // The server then annotates the job with GitHub's message "The runner has | |
| 355 | + | // received a shutdown signal. This can happen when the runner service is | |
| 356 | + | // stopped, or a manually started runner is canceled." | |
| 357 | + | bool runner_shutdown = 5; | |
| 358 | + | } | |
| 359 | + | ||
| 360 | + | // StepUpdate is the state of one step as the runner knows it. | |
| 361 | + | message StepUpdate { | |
| 362 | + | // Number is the 1-based number of the step in the job. It is the number | |
| 363 | + | // used by AppendLog. | |
| 364 | + | int32 number = 1; | |
| 365 | + | // Name is the display name of the step, evaluated: the "name" of the | |
| 366 | + | // workflow step, or "Run <first line of the script>", as GitHub shows it. | |
| 367 | + | // At most 1,024 characters; the server cuts longer names. | |
| 368 | + | string name = 2; | |
| 369 | + | // State is where the step is. | |
| 370 | + | StepState state = 3; | |
| 371 | + | // Conclusion is how the step ended. Required (not UNSPECIFIED) when state is | |
| 372 | + | // COMPLETED, must be UNSPECIFIED otherwise. Allowed values: success, | |
| 373 | + | // failure, cancelled, skipped. | |
| 374 | + | Conclusion conclusion = 4; | |
| 375 | + | // StartedAt is when the step started, by the clock of the runner. Required | |
| 376 | + | // from IN_PROGRESS on. | |
| 377 | + | google.protobuf.Timestamp started_at = 5; | |
| 378 | + | // CompletedAt is when the step ended, by the clock of the runner. Required | |
| 379 | + | // when state is COMPLETED. | |
| 380 | + | google.protobuf.Timestamp completed_at = 6; | |
| 381 | + | } | |
| 382 | + | ||
| 383 | + | // UpdateJobResponse is empty. | |
| 384 | + | message UpdateJobResponse {} | |
| 385 | + | ||
| 386 | + | // AppendLogRequest carries log lines of one step. | |
| 387 | + | message AppendLogRequest { | |
| 388 | + | // JobId is the Job.job_id. It must be the job of the token. | |
| 389 | + | string job_id = 1; | |
| 390 | + | // Step is the number of the step the lines belong to (see | |
| 391 | + | // UpdateJobRequest.steps), at least 1. A step that UpdateJob has not | |
| 392 | + | // announced yet is created by the first lines. | |
| 393 | + | int32 step = 2; | |
| 394 | + | // Offset is the number of the first line of the request in the log of the | |
| 395 | + | // job, counted from 0 across all steps. See the rules on AppendLog. | |
| 396 | + | int64 offset = 3; | |
| 397 | + | // Lines are the lines to append, in order: at most 1,000, at most 64 KiB of | |
| 398 | + | // text in total. | |
| 399 | + | repeated LogLine lines = 4; | |
| 400 | + | } | |
| 401 | + | ||
| 402 | + | // LogLine is one line of a job log. | |
| 403 | + | message LogLine { | |
| 404 | + | // Time is when the line was produced, by the clock of the runner. The server | |
| 405 | + | // keeps it with nanosecond precision (RFC 3339) and does not reorder lines. | |
| 406 | + | google.protobuf.Timestamp time = 1; | |
| 407 | + | // Text is the line without its line terminator, valid UTF-8 (the client | |
| 408 | + | // replaces invalid bytes with U+FFFD). It must not contain a line feed; a | |
| 409 | + | // carriage return is kept. It is the line as GitHub's raw log has it, | |
| 410 | + | // including "##[group]" and "##[endgroup]" markers and "##[error]" lines. | |
| 411 | + | string text = 2; | |
| 412 | + | } | |
| 413 | + | ||
| 414 | + | // AppendLogResponse tells how much of the log the server holds. | |
| 415 | + | message AppendLogResponse { | |
| 416 | + | // NextOffset is the number of lines of the job log after the call, which is | |
| 417 | + | // the offset of the next request. | |
| 418 | + | int64 next_offset = 1; | |
| 419 | + | } | |
| 420 | + | ||
| 421 | + | // HeartbeatRequest names the job that is alive. | |
| 422 | + | message HeartbeatRequest { | |
| 423 | + | // JobId is the Job.job_id. It must be the job of the token. | |
| 424 | + | string job_id = 1; | |
| 425 | + | } | |
| 426 | + | ||
| 427 | + | // HeartbeatResponse is empty. | |
| 428 | + | message HeartbeatResponse {} | |
| 429 | + | ||
| 430 | + | // WatchJobRequest names the job to watch. | |
| 431 | + | message WatchJobRequest { | |
| 432 | + | // JobId is the Job.job_id. It must be the job of the token. | |
| 433 | + | string job_id = 1; | |
| 434 | + | } | |
| 435 | + | ||
| 436 | + | // WatchJobResponse is one message of the stream: a request to cancel the job, | |
| 437 | + | // or a keep-alive. | |
| 438 | + | message WatchJobResponse { | |
| 439 | + | // Reason is why the job must stop. CANCEL_REASON_UNSPECIFIED marks a | |
| 440 | + | // keep-alive: the other fields are empty and the client ignores it. New | |
| 441 | + | // reasons may be added; a runner that does not know one cancels the job and | |
| 442 | + | // reports CONCLUSION_CANCELLED. | |
| 443 | + | CancelReason reason = 1; | |
| 444 | + | // Message is a short human-readable explanation, for the job log, for | |
| 445 | + | // example "Canceled by the owner". At most 1,024 characters. | |
| 446 | + | string message = 2; | |
| 447 | + | // RequestedAt is when the cancellation was requested, by the clock of the | |
| 448 | + | // server. | |
| 449 | + | google.protobuf.Timestamp requested_at = 3; | |
| 450 | + | } | |
| @@ -0,0 +1,450 @@ | |||||
| 1 | + | // SPDX-License-Identifier: MIT | |||
| 2 | + | ||||
| 3 | + | syntax = "proto3"; | |||
| 4 | + | ||||
| 5 | + | package ermite; | |||
| 6 | + | ||||
| 7 | + | import "google/protobuf/timestamp.proto"; | |||
| 8 | + | ||||
| 9 | + | // RunStatus is where a run, a job or a check is in its life. It is the same | |||
| 10 | + | // as GitHub's status, without the rarely used values. | |||
| 11 | + | enum RunStatus { | |||
| 12 | + | // Not set. In a filter it means every status. Never returned by the server. | |||
| 13 | + | RUN_STATUS_UNSPECIFIED = 0; | |||
| 14 | + | // Waiting: for a runner, for the jobs it needs, or for its turn | |||
| 15 | + | // (max-parallel). | |||
| 16 | + | RUN_STATUS_QUEUED = 1; | |||
| 17 | + | // At least one job has been given to a runner and has not ended. | |||
| 18 | + | RUN_STATUS_IN_PROGRESS = 2; | |||
| 19 | + | // Over. The conclusion says how it ended. | |||
| 20 | + | RUN_STATUS_COMPLETED = 3; | |||
| 21 | + | } | |||
| 22 | + | ||||
| 23 | + | // Conclusion is how a run, a job, a step or a check ended, with GitHub's | |||
| 24 | + | // values and meaning. It is unset (CONCLUSION_UNSPECIFIED) until the status is | |||
| 25 | + | // COMPLETED. | |||
| 26 | + | enum Conclusion { | |||
| 27 | + | // Not completed yet. Never returned by the server for a completed item. | |||
| 28 | + | CONCLUSION_UNSPECIFIED = 0; | |||
| 29 | + | // Everything it ran succeeded. | |||
| 30 | + | CONCLUSION_SUCCESS = 1; | |||
| 31 | + | // A step failed, or the job failed for another reason described in | |||
| 32 | + | // RunJob.error (runner lost, queue timeout, log limit, feature not | |||
| 33 | + | // supported yet, invalid workflow file). | |||
| 34 | + | CONCLUSION_FAILURE = 2; | |||
| 35 | + | // Cancelled by the owner, or by fail-fast, or because the runner shut down. | |||
| 36 | + | CONCLUSION_CANCELLED = 3; | |||
| 37 | + | // Never ran: its "if" was false, or a job it needs did not succeed, or it was | |||
| 38 | + | // not selected. Never reported by a runner. | |||
| 39 | + | CONCLUSION_SKIPPED = 4; | |||
| 40 | + | // Exceeded its timeout. | |||
| 41 | + | CONCLUSION_TIMED_OUT = 5; | |||
| 42 | + | // Neither a success nor a failure. Defined for parity with GitHub; no | |||
| 43 | + | // workflow job ends this way yet. For merging it counts as a success. | |||
| 44 | + | CONCLUSION_NEUTRAL = 6; | |||
| 45 | + | // Waits for a human action (for example the approval of an environment). | |||
| 46 | + | // Defined for parity with GitHub; the server does not produce it before | |||
| 47 | + | // environments exist. For merging it counts as a failure. | |||
| 48 | + | CONCLUSION_ACTION_REQUIRED = 7; | |||
| 49 | + | } | |||
| 50 | + | ||||
| 51 | + | // StepState is where a step is in its life. | |||
| 52 | + | enum StepState { | |||
| 53 | + | // Not set. Rejected in requests, never returned by the server. | |||
| 54 | + | STEP_STATE_UNSPECIFIED = 0; | |||
| 55 | + | // Known but not started. | |||
| 56 | + | STEP_STATE_PENDING = 1; | |||
| 57 | + | // Running. | |||
| 58 | + | STEP_STATE_IN_PROGRESS = 2; | |||
| 59 | + | // Over. The conclusion says how it ended. A step that did not run because | |||
| 60 | + | // of its "if" or an earlier failure is COMPLETED with CONCLUSION_SKIPPED. | |||
| 61 | + | STEP_STATE_COMPLETED = 3; | |||
| 62 | + | } | |||
| 63 | + | ||||
| 64 | + | // RunnerIsolation is how a runner isolates each job from the host and from | |||
| 65 | + | // other jobs. The runner chooses it (auto-detection included) and reports it | |||
| 66 | + | // at registration; the job page shows it. | |||
| 67 | + | enum RunnerIsolation { | |||
| 68 | + | // Not set. Rejected at registration, never returned by the server. | |||
| 69 | + | RUNNER_ISOLATION_UNSPECIFIED = 0; | |||
| 70 | + | // One micro-VM per job, destroyed at the end. | |||
| 71 | + | RUNNER_ISOLATION_VM = 1; | |||
| 72 | + | // One container per job with its own Docker daemon, run by the sysbox | |||
| 73 | + | // runtime. Shares the host kernel. | |||
| 74 | + | RUNNER_ISOLATION_CONTAINER_SYSBOX = 2; | |||
| 75 | + | // One privileged container per job with its own Docker daemon. Shares the | |||
| 76 | + | // host kernel and is equivalent to root on the host: the weakest mode. | |||
| 77 | + | RUNNER_ISOLATION_CONTAINER_PRIVILEGED = 3; | |||
| 78 | + | } | |||
| 79 | + | ||||
| 80 | + | // CancelReason says why the server asks a runner to stop a job. | |||
| 81 | + | enum CancelReason { | |||
| 82 | + | // Not set. A WatchJobResponse with this reason is a keep-alive and carries | |||
| 83 | + | // no request. | |||
| 84 | + | CANCEL_REASON_UNSPECIFIED = 0; | |||
| 85 | + | // The owner cancelled the run (RunService.CancelRun, web or CLI). The job | |||
| 86 | + | // ends 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 CONCLUSION_CANCELLED. | |||
| 90 | + | CANCEL_REASON_FAIL_FAST = 2; | |||
| 91 | + | // The job exceeded its timeout_minutes without the runner ending it. The | |||
| 92 | + | // job ends 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: | |||
| 100 | + | // | |||
| 101 | + | // - RegisterRunner: a stored token with the runner:register scope, or the | |||
| 102 | + | // root token, as "Authorization: Bearer <secret>". | |||
| 103 | + | // - FetchJob: the runner token returned by RegisterRunner, as | |||
| 104 | + | // "Authorization: Bearer <runner_token>". It identifies the runner. It | |||
| 105 | + | // only works for FetchJob. | |||
| 106 | + | // - UpdateJob, AppendLog, Heartbeat, WatchJob: the job token of the Job that | |||
| 107 | + | // FetchJob returned, as "Authorization: Bearer <job_token>". It is valid | |||
| 108 | + | // for that one job and attempt, from its assignment until it ends. The | |||
| 109 | + | // job_id of the request must be the one of the token. | |||
| 110 | + | // | |||
| 111 | + | // A request without a credential, with an unknown, expired or revoked one, or | |||
| 112 | + | // with a credential of the wrong kind for the RPC, gets unauthenticated. A | |||
| 113 | + | // job token that is valid but belongs to another job than the job_id of the | |||
| 114 | + | // request gets permission_denied. A job that has ended (concluded, cancelled, | |||
| 115 | + | // failed by the server, or whose runner was lost) no longer has a valid job | |||
| 116 | + | // token: every later call gets unauthenticated, which a runner treats as "the | |||
| 117 | + | // job is over" and stops sending anything for it. Repeated failed | |||
| 118 | + | // authentications are rate limited per client address. | |||
| 119 | + | // | |||
| 120 | + | // Runner tokens and job tokens are secrets: the server never logs them and | |||
| 121 | + | // clients must not log them. The job token stays in the runner host process; it | |||
| 122 | + | // is never given to the sandbox that runs the job's code. | |||
| 123 | + | // | |||
| 124 | + | // Servers that implement this service list "ci.runners" in | |||
| 125 | + | // GetVersionResponse.features. | |||
| 126 | + | service RunnerService { | |||
| 127 | + | // RegisterRunner registers a runner and returns its runner token, which is | |||
| 128 | + | // shown only in this response; the runner keeps it. The caller needs the | |||
| 129 | + | // runner:register scope on a token that is not restricted to repositories | |||
| 130 | + | // (a runner serves every repository), or the root token. Fails with | |||
| 131 | + | // unauthenticated without a valid token, permission_denied without the | |||
| 132 | + | // scope or with a repository-restricted token, invalid_argument if a field | |||
| 133 | + | // breaks a rule, and already_exists if name is taken by another runner. | |||
| 134 | + | rpc RegisterRunner(RegisterRunnerRequest) returns (RegisterRunnerResponse); | |||
| 135 | + | // FetchJob asks for a job to run (long poll). The server holds the call | |||
| 136 | + | // until a queued job matches the runner, or about 50 seconds (never more | |||
| 137 | + | // than 50) pass, then answers; without a job the response has no job and the | |||
| 138 | + | // runner calls again at once. Clients set a call timeout above 50 seconds | |||
| 139 | + | // (60 is suggested) and must not poll when they have no free capacity. | |||
| 140 | + | // | |||
| 141 | + | // A job matches when every label of its runs-on is among the labels of the | |||
| 142 | + | // runner (case-insensitive). A job is given to exactly one runner, even when | |||
| 143 | + | // many runners and many polls compete; the assignment is recorded before | |||
| 144 | + | // the response is sent, and the job is then in progress. If the response is | |||
| 145 | + | // lost the job is not offered again: with no Heartbeat for 2 minutes the | |||
| 146 | + | // server fails it as a lost runner. | |||
| 147 | + | // | |||
| 148 | + | // Needs the runner token. Fails with unauthenticated when the runner was | |||
| 149 | + | // removed (also ending a call in progress) and invalid_argument if labels | |||
| 150 | + | // breaks a rule. | |||
| 151 | + | rpc FetchJob(FetchJobRequest) returns (FetchJobResponse); | |||
| 152 | + | // UpdateJob reports the state of the steps of a job and, once, how the job | |||
| 153 | + | // ended. Needs the job token. Each call is applied whole or not at all, and | |||
| 154 | + | // repeating an identical call changes nothing, so a runner may retry after a | |||
| 155 | + | // network error. | |||
| 156 | + | // | |||
| 157 | + | // Fails with invalid_argument if a rule is broken (including the output | |||
| 158 | + | // limits), and with failed_precondition if a step goes back (a COMPLETED | |||
| 159 | + | // step cannot change, a step cannot go from IN_PROGRESS to PENDING). | |||
| 160 | + | // The call that carries a conclusion ends the job: its job token stops | |||
| 161 | + | // working, the server completes the job and no further call is accepted, | |||
| 162 | + | // so a retry of that last call after a lost response gets unauthenticated, | |||
| 163 | + | // which means it was applied. | |||
| 164 | + | rpc UpdateJob(UpdateJobRequest) returns (UpdateJobResponse); | |||
| 165 | + | // AppendLog appends lines to the log of a job. Needs the job token. | |||
| 166 | + | // | |||
| 167 | + | // The log of a job is one sequence of lines, numbered from 0 across all | |||
| 168 | + | // steps; offset is the number of lines the server holds when the request is | |||
| 169 | + | // applied, that is the number of the first line of the request. Calls must | |||
| 170 | + | // go in order, one at a time, and are idempotent: | |||
| 171 | + | // | |||
| 172 | + | // - offset equal to the server's line count: the lines are appended; | |||
| 173 | + | // - offset below it: the first (count - offset) lines were already stored | |||
| 174 | + | // (a retry after a lost response) and are skipped, and the rest is | |||
| 175 | + | // appended; a request entirely stored changes nothing; | |||
| 176 | + | // - offset above it: out_of_range (a gap is a bug of the client; the | |||
| 177 | + | // message gives the line count the server holds). | |||
| 178 | + | // | |||
| 179 | + | // The response gives the line count after the call, so a client can check | |||
| 180 | + | // itself. A request has at most 1,000 lines whose text adds up to at most | |||
| 181 | + | // 64 KiB (65,536 bytes), all in the same step; more is invalid_argument. A | |||
| 182 | + | // line of more than 64 KiB is cut by the client into several lines. | |||
| 183 | + | // | |||
| 184 | + | // The log of a job is limited to 512 MiB (536,870,912 bytes) as stored by | |||
| 185 | + | // the server (text, timestamps and separators). The call that would exceed | |||
| 186 | + | // it appends nothing; the server adds a last line saying that the limit was | |||
| 187 | + | // reached, ends the job with CONCLUSION_FAILURE and RunJob.error set, and | |||
| 188 | + | // answers resource_exhausted. The runner then stops the job and sends | |||
| 189 | + | // nothing more: its calls get unauthenticated. | |||
| 190 | + | // | |||
| 191 | + | // Fails with invalid_argument if a rule is broken (step number, text with a | |||
| 192 | + | // line feed or invalid UTF-8, missing time). | |||
| 193 | + | rpc AppendLog(AppendLogRequest) returns (AppendLogResponse); | |||
| 194 | + | // Heartbeat tells the server that the runner is alive and still working on | |||
| 195 | + | // the job. Needs the job token. The runner calls it every 30 seconds for as | |||
| 196 | + | // long as the job runs, from assignment to the end. Without a heartbeat for | |||
| 197 | + | // 2 minutes the server ends the job with CONCLUSION_FAILURE and the message | |||
| 198 | + | // "The self-hosted runner: <name> lost communication with the server...". | |||
| 199 | + | // Only Heartbeat counts: other calls do not keep a job alive. | |||
| 200 | + | rpc Heartbeat(HeartbeatRequest) returns (HeartbeatResponse); | |||
| 201 | + | // WatchJob streams the requests to stop a job. Needs the job token. The | |||
| 202 | + | // runner opens it when it starts the job and keeps it open; it reopens it | |||
| 203 | + | // after any error other than unauthenticated. | |||
| 204 | + | // | |||
| 205 | + | // When the stream opens and a cancellation has already been requested, the | |||
| 206 | + | // first message is that request, so a reconnect never misses one. Only the | |||
| 207 | + | // first reason is sent: a job already cancelled by the owner does not get a | |||
| 208 | + | // second message when its timeout expires. The server also sends a | |||
| 209 | + | // keep-alive message (reason CANCEL_REASON_UNSPECIFIED) at least every 30 | |||
| 210 | + | // seconds, which the client ignores. The stream ends without error when | |||
| 211 | + | // the job ends. The runner stops the job as the official runner does | |||
| 212 | + | // (interrupt, then terminate, then kill) and reports the conclusion given | |||
| 213 | + | // for the reason with UpdateJob. | |||
| 214 | + | rpc WatchJob(WatchJobRequest) returns (stream WatchJobResponse); | |||
| 215 | + | } | |||
| 216 | + | ||||
| 217 | + | // RegisterRunnerRequest describes the runner to register. | |||
| 218 | + | message RegisterRunnerRequest { | |||
| 219 | + | // Name identifies the runner for people (job page, logs, ListRunners) and is | |||
| 220 | + | // unique: 1 to 64 characters among letters, digits, ".", "_" and "-". It is | |||
| 221 | + | // not a label and never matches runs-on. | |||
| 222 | + | string name = 1; | |||
| 223 | + | // Labels are the labels the runner adds to its implicit ones: at most 64, | |||
| 224 | + | // each 1 to 64 characters among letters, digits, ".", "_" and "-", for | |||
| 225 | + | // example "ubuntu-latest". Comparison is case-insensitive and duplicates | |||
| 226 | + | // are removed. The server adds the implicit labels "self-hosted", the os | |||
| 227 | + | // and the arch ("linux", "x64" or "arm64"); the runner name is not a label. | |||
| 228 | + | repeated string labels = 2; | |||
| 229 | + | // Version is the release of the runner, for example "v0.1.0", or "devel". | |||
| 230 | + | // Informational, at most 64 characters. | |||
| 231 | + | string version = 3; | |||
| 232 | + | // Isolation is how the runner isolates jobs. Required. | |||
| 233 | + | RunnerIsolation isolation = 4; | |||
| 234 | + | // Arch is the CPU architecture, in GitHub's names: "x64" or "arm64". Other | |||
| 235 | + | // values are invalid_argument (the set may grow with compatible additions). | |||
| 236 | + | string arch = 5; | |||
| 237 | + | // Os is the operating system: "linux". Other values are invalid_argument | |||
| 238 | + | // (the set may grow with compatible additions). | |||
| 239 | + | string os = 6; | |||
| 240 | + | } | |||
| 241 | + | ||||
| 242 | + | // RegisterRunnerResponse holds the new runner and its secret. | |||
| 243 | + | message RegisterRunnerResponse { | |||
| 244 | + | // RunnerId identifies the runner in ListRunners and RemoveRunner. It is not | |||
| 245 | + | // a secret. | |||
| 246 | + | string runner_id = 1; | |||
| 247 | + | // RunnerToken is the secret of the runner, to present as "Authorization: | |||
| 248 | + | // Bearer <runner_token>" to FetchJob. It starts with "ermiterun_" and is | |||
| 249 | + | // returned once: the server keeps only its SHA-256, the runner must store | |||
| 250 | + | // it (mode 0600). It stops working when the runner is removed. | |||
| 251 | + | string runner_token = 2; | |||
| 252 | + | } | |||
| 253 | + | ||||
| 254 | + | // FetchJobRequest asks for a job. | |||
| 255 | + | message FetchJobRequest { | |||
| 256 | + | // Labels narrows the labels offered for this call. Empty means all the | |||
| 257 | + | // labels of the runner, which is the normal case. Otherwise each one must be | |||
| 258 | + | // a label of the runner (invalid_argument if not) and only jobs whose runs-on | |||
| 259 | + | // labels are all among them are offered. | |||
| 260 | + | repeated string labels = 1; | |||
| 261 | + | } | |||
| 262 | + | ||||
| 263 | + | // FetchJobResponse holds the job given to the runner, if any. | |||
| 264 | + | message FetchJobResponse { | |||
| 265 | + | // Job is the job to run. Unset when no job was available before the end of | |||
| 266 | + | // the poll. | |||
| 267 | + | Job job = 1; | |||
| 268 | + | } | |||
| 269 | + | ||||
| 270 | + | // Job is a job given to a runner: everything needed to run it, nothing more. | |||
| 271 | + | // The runner parses workflow_yaml itself; the protocol does not carry the | |||
| 272 | + | // workflow model, so it does not change when the model grows. | |||
| 273 | + | message Job { | |||
| 274 | + | // JobId identifies the job on the server. It is opaque, unique across | |||
| 275 | + | // repositories and runs, and the same for every attempt of the job. With | |||
| 276 | + | // attempt it names one execution. | |||
| 277 | + | string job_id = 1; | |||
| 278 | + | // RunId is the run the job belongs to, the "github.run_id". It is global to | |||
| 279 | + | // the server. | |||
| 280 | + | int64 run_id = 2; | |||
| 281 | + | // Attempt is the attempt of the run this job executes: 1 for the first, and | |||
| 282 | + | // one more after each RunService.RerunRun. It is the "github.run_attempt". | |||
| 283 | + | int32 attempt = 3; | |||
| 284 | + | // JobToken is the secret that authenticates this job's UpdateJob, | |||
| 285 | + | // AppendLog, Heartbeat and WatchJob calls ("Authorization: Bearer | |||
| 286 | + | // <job_token>"). It is random, valid until the job ends, survives a server | |||
| 287 | + | // restart and is shown only in this response. It must stay in the runner | |||
| 288 | + | // host process and never reach the job's code. | |||
| 289 | + | string job_token = 4; | |||
| 290 | + | // WorkflowPath is the path of the workflow file in the repository, for | |||
| 291 | + | // example ".github/workflows/ci.yml". | |||
| 292 | + | string workflow_path = 5; | |||
| 293 | + | // WorkflowYaml is the content of that file at sha, as is, at most 1 MiB | |||
| 294 | + | // (larger files fail at planning). | |||
| 295 | + | string workflow_yaml = 6; | |||
| 296 | + | // JobKey is the key of the job under "jobs:" in the workflow, the | |||
| 297 | + | // "github.job". | |||
| 298 | + | string job_key = 7; | |||
| 299 | + | // Matrix is the JSON object of the matrix values of this instance of the | |||
| 300 | + | // job, for example {"os":"linux","go":"1.27"}. "{}" when the job has no | |||
| 301 | + | // matrix. It is the "matrix" context. | |||
| 302 | + | string matrix = 8; | |||
| 303 | + | // Contexts is a JSON object with the contexts that the server computed for | |||
| 304 | + | // this job: "github", "needs", "strategy", "matrix", "inputs" and "vars", in | |||
| 305 | + | // the shape GitHub gives them. The contexts "job", "runner", "steps" and | |||
| 306 | + | // "env" belong to the runner and are not in it. Other keys may be added | |||
| 307 | + | // later; clients ignore those they do not know. | |||
| 308 | + | string contexts = 9; | |||
| 309 | + | // Labels are the runs-on labels of the job, evaluated. | |||
| 310 | + | repeated string labels = 10; | |||
| 311 | + | // TimeoutMinutes is the timeout of the job in minutes: the value of the | |||
| 312 | + | // workflow's timeout-minutes, or 360 when it has none. At least 1. The | |||
| 313 | + | // runner enforces it from the start of the job; the server ends a job that | |||
| 314 | + | // exceeds it by announcing CANCEL_REASON_TIMEOUT. | |||
| 315 | + | int32 timeout_minutes = 11; | |||
| 316 | + | // Repo is the name of the repository. | |||
| 317 | + | string repo = 12; | |||
| 318 | + | // Sha is the full id of the commit the run is for, 40 lowercase | |||
| 319 | + | // hexadecimal characters for SHA-1. The runner reads the code at it. | |||
| 320 | + | string sha = 13; | |||
| 321 | + | // Ref is the full ref name that triggered the run, for example | |||
| 322 | + | // "refs/heads/main" or "refs/tags/v1.0.0". | |||
| 323 | + | string ref = 14; | |||
| 324 | + | } | |||
| 325 | + | ||||
| 326 | + | // UpdateJobRequest reports the steps and, in the last call, the end of a job. | |||
| 327 | + | message UpdateJobRequest { | |||
| 328 | + | // JobId is the Job.job_id. It must be the job of the token. | |||
| 329 | + | string job_id = 1; | |||
| 330 | + | // Steps are the steps whose state changed or that are new. Each one is a | |||
| 331 | + | // full snapshot of its step, applied by number. At most 1,000 steps per job | |||
| 332 | + | // (invalid_argument beyond). Steps are numbered like GitHub's log: the | |||
| 333 | + | // implicit "Set up job" is 1, the steps of the workflow follow in order, and | |||
| 334 | + | // the implicit "Complete job" is last. | |||
| 335 | + | repeated StepUpdate steps = 2; | |||
| 336 | + | // Outputs are the outputs of the job, evaluated by the runner. Only allowed | |||
| 337 | + | // in the call that carries the conclusion (invalid_argument otherwise). | |||
| 338 | + | // Names are 1 to 255 characters among letters, digits, "_" and "-" and | |||
| 339 | + | // begin with a letter or "_" and are case-insensitive: two names that | |||
| 340 | + | // differ only by case are invalid_argument. Each value is at most 1 MiB, and | |||
| 341 | + | // all names and values together at most 1 MiB (1,048,576 bytes); beyond that | |||
| 342 | + | // the whole call is rejected with invalid_argument and the runner reports | |||
| 343 | + | // the failure of the job without outputs, with a line in the log. | |||
| 344 | + | map<string, string> outputs = 3; | |||
| 345 | + | // Conclusion ends the job when set. A runner reports CONCLUSION_SUCCESS, | |||
| 346 | + | // CONCLUSION_FAILURE, CONCLUSION_CANCELLED or CONCLUSION_TIMED_OUT; any | |||
| 347 | + | // other value is invalid_argument. CONCLUSION_UNSPECIFIED means the job goes | |||
| 348 | + | // on. Steps that were not reported COMPLETED when the job ends are | |||
| 349 | + | // completed by the server as CONCLUSION_CANCELLED (or SKIPPED when they | |||
| 350 | + | // never started and the job did not fail by cancellation). | |||
| 351 | + | Conclusion conclusion = 4; | |||
| 352 | + | // RunnerShutdown is true when the runner ends the job because it was told to | |||
| 353 | + | // stop (service stopped, signal). Only allowed with CONCLUSION_CANCELLED. | |||
| 354 | + | // The server then annotates the job with GitHub's message "The runner has | |||
| 355 | + | // received a shutdown signal. This can happen when the runner service is | |||
| 356 | + | // stopped, or a manually started runner is canceled." | |||
| 357 | + | bool runner_shutdown = 5; | |||
| 358 | + | } | |||
| 359 | + | ||||
| 360 | + | // StepUpdate is the state of one step as the runner knows it. | |||
| 361 | + | message StepUpdate { | |||
| 362 | + | // Number is the 1-based number of the step in the job. It is the number | |||
| 363 | + | // used by AppendLog. | |||
| 364 | + | int32 number = 1; | |||
| 365 | + | // Name is the display name of the step, evaluated: the "name" of the | |||
| 366 | + | // workflow step, or "Run <first line of the script>", as GitHub shows it. | |||
| 367 | + | // At most 1,024 characters; the server cuts longer names. | |||
| 368 | + | string name = 2; | |||
| 369 | + | // State is where the step is. | |||
| 370 | + | StepState state = 3; | |||
| 371 | + | // Conclusion is how the step ended. Required (not UNSPECIFIED) when state is | |||
| 372 | + | // COMPLETED, must be UNSPECIFIED otherwise. Allowed values: success, | |||
| 373 | + | // failure, cancelled, skipped. | |||
| 374 | + | Conclusion conclusion = 4; | |||
| 375 | + | // StartedAt is when the step started, by the clock of the runner. Required | |||
| 376 | + | // from IN_PROGRESS on. | |||
| 377 | + | google.protobuf.Timestamp started_at = 5; | |||
| 378 | + | // CompletedAt is when the step ended, by the clock of the runner. Required | |||
| 379 | + | // when state is COMPLETED. | |||
| 380 | + | google.protobuf.Timestamp completed_at = 6; | |||
| 381 | + | } | |||
| 382 | + | ||||
| 383 | + | // UpdateJobResponse is empty. | |||
| 384 | + | message UpdateJobResponse {} | |||
| 385 | + | ||||
| 386 | + | // AppendLogRequest carries log lines of one step. | |||
| 387 | + | message AppendLogRequest { | |||
| 388 | + | // JobId is the Job.job_id. It must be the job of the token. | |||
| 389 | + | string job_id = 1; | |||
| 390 | + | // Step is the number of the step the lines belong to (see | |||
| 391 | + | // UpdateJobRequest.steps), at least 1. A step that UpdateJob has not | |||
| 392 | + | // announced yet is created by the first lines. | |||
| 393 | + | int32 step = 2; | |||
| 394 | + | // Offset is the number of the first line of the request in the log of the | |||
| 395 | + | // job, counted from 0 across all steps. See the rules on AppendLog. | |||
| 396 | + | int64 offset = 3; | |||
| 397 | + | // Lines are the lines to append, in order: at most 1,000, at most 64 KiB of | |||
| 398 | + | // text in total. | |||
| 399 | + | repeated LogLine lines = 4; | |||
| 400 | + | } | |||
| 401 | + | ||||
| 402 | + | // LogLine is one line of a job log. | |||
| 403 | + | message LogLine { | |||
| 404 | + | // Time is when the line was produced, by the clock of the runner. The server | |||
| 405 | + | // keeps it with nanosecond precision (RFC 3339) and does not reorder lines. | |||
| 406 | + | google.protobuf.Timestamp time = 1; | |||
| 407 | + | // Text is the line without its line terminator, valid UTF-8 (the client | |||
| 408 | + | // replaces invalid bytes with U+FFFD). It must not contain a line feed; a | |||
| 409 | + | // carriage return is kept. It is the line as GitHub's raw log has it, | |||
| 410 | + | // including "##[group]" and "##[endgroup]" markers and "##[error]" lines. | |||
| 411 | + | string text = 2; | |||
| 412 | + | } | |||
| 413 | + | ||||
| 414 | + | // AppendLogResponse tells how much of the log the server holds. | |||
| 415 | + | message AppendLogResponse { | |||
| 416 | + | // NextOffset is the number of lines of the job log after the call, which is | |||
| 417 | + | // the offset of the next request. | |||
| 418 | + | int64 next_offset = 1; | |||
| 419 | + | } | |||
| 420 | + | ||||
| 421 | + | // HeartbeatRequest names the job that is alive. | |||
| 422 | + | message HeartbeatRequest { | |||
| 423 | + | // JobId is the Job.job_id. It must be the job of the token. | |||
| 424 | + | string job_id = 1; | |||
| 425 | + | } | |||
| 426 | + | ||||
| 427 | + | // HeartbeatResponse is empty. | |||
| 428 | + | message HeartbeatResponse {} | |||
| 429 | + | ||||
| 430 | + | // WatchJobRequest names the job to watch. | |||
| 431 | + | message WatchJobRequest { | |||
| 432 | + | // JobId is the Job.job_id. It must be the job of the token. | |||
| 433 | + | string job_id = 1; | |||
| 434 | + | } | |||
| 435 | + | ||||
| 436 | + | // WatchJobResponse is one message of the stream: a request to cancel the job, | |||
| 437 | + | // or a keep-alive. | |||
| 438 | + | message WatchJobResponse { | |||
| 439 | + | // Reason is why the job must stop. CANCEL_REASON_UNSPECIFIED marks a | |||
| 440 | + | // keep-alive: the other fields are empty and the client ignores it. New | |||
| 441 | + | // reasons may be added; a runner that does not know one cancels the job and | |||
| 442 | + | // reports CONCLUSION_CANCELLED. | |||
| 443 | + | CancelReason reason = 1; | |||
| 444 | + | // Message is a short human-readable explanation, for the job log, for | |||
| 445 | + | // example "Canceled by the owner". At most 1,024 characters. | |||
| 446 | + | string message = 2; | |||
| 447 | + | // RequestedAt is when the cancellation was requested, by the clock of the | |||
| 448 | + | // server. | |||
| 449 | + | google.protobuf.Timestamp requested_at = 3; | |||
| 450 | + | } | |||
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 | ||||