detached 97a9365

Merge pull request 'feat(api): add the CI runner and run services' (#5) from feat/ci-protocol into main

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

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 18

↑↓ moveenter openesc close