detached abba0da

feat(api): add CompleteJob, JobEnded, poll ids and versions to the CI protocol

Review fixes for the runner protocol:
- CompleteJob ends a job and is idempotent (stored result, applied flag);
  UpdateJob no longer carries the conclusion or the outputs.
- After the end of a job, calls answer failed_precondition with a JobEnded
  detail; unauthenticated means a bad credential only. The job token is
  recognised for 24 hours after the end.
- Server-side enforcement of cancellation (60 s) and timeouts (assignment
  clock plus slack).
- FetchJob poll_id, retry_after, acknowledgement by the first Heartbeat,
  heartbeat deadline from max(last heartbeat, server start).
- WatchJob messages are a oneof (keepalive, cancel, ended).
- Run.version and RunJob.version replace the event ordering promise.
- AppendLog step ordering rule, LogLine.continued for long lines, log limit
  is failed_precondition.
- Token formats, runner:register trust note, RegisterRunner replace.
commit
abba0da6a75834ae23e6676263945d1eb72e51a4
author
brice · · 2026-10-12 14:16 UTC
parent
0e0b3ac
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
10 files · +1297 -370
M README.md +1 -1
@@ -22,7 +22,7 @@ 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); for CI runners |
25+| `RunnerService` | `RegisterRunner`, `FetchJob` (long poll), `UpdateJob`, `AppendLog` (offset-idempotent), `Heartbeat`, `WatchJob` (cancel requests, streamed), `CompleteJob` (idempotent); for CI runners |
2626 | `RunService` | `ListRuns`, `GetRun`, `GetJob`, `GetJobLog` (streamed, follow), `CancelRun`, `RerunRun`, `ListChecks`, `ListRunners`, `RemoveRunner` |
2727 | `ServerService` | `GetVersion` (server version and feature list), `GetSigningKey` (commit signing key) |
2828
@@ -22,7 +22,7 @@ Handlers are mounted under `/-/api/` by the server. Errors are Connect codes.
22 | `MergeRequestService` | `CreateMergeRequest`, `ListMergeRequests`, `GetMergeRequest` (commits, diff, comments), `AddComment`, `ResolveComment`, `CloseMergeRequest`, `MergeMergeRequest`, `MarkRead` |22 | `MergeRequestService` | `CreateMergeRequest`, `ListMergeRequests`, `GetMergeRequest` (commits, diff, comments), `AddComment`, `ResolveComment`, `CloseMergeRequest`, `MergeMergeRequest`, `MarkRead` |
23 | `SessionService` | `CreateLoginLink`, `SignOutEverywhere` (owner only) |23 | `SessionService` | `CreateLoginLink`, `SignOutEverywhere` (owner only) |
24 | `EventService` | `Watch` (streamed, resumable) |24 | `EventService` | `Watch` (streamed, resumable) |
25-| `RunnerService` | `RegisterRunner`, `FetchJob` (long poll), `UpdateJob`, `AppendLog` (offset-idempotent), `Heartbeat`, `WatchJob` (cancel requests, streamed); for CI runners |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` |26 | `RunService` | `ListRuns`, `GetRun`, `GetJob`, `GetJobLog` (streamed, follow), `CancelRun`, `RerunRun`, `ListChecks`, `ListRunners`, `RemoveRunner` |
27 | `ServerService` | `GetVersion` (server version and feature list), `GetSigningKey` (commit signing key) |27 | `ServerService` | `GetVersion` (server version and feature list), `GetSigningKey` (commit signing key) |
28 28
M gen/ermite/ermiteconnect/event.connect.go +8 -4
@@ -56,8 +56,10 @@ type EventServiceClient interface {
5656 // "home"), subject to the same visibility. They carry the state after the
5757 // change and are published when a run or a job is created, changes status
5858 // or conclusion, or when one of its steps starts or ends (never per log
59- // line: follow a log with RunService.GetJobLog). A job update is published
60- // before the update of its run. There is no event for a change of title or
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
6163 // description, nor for a resolved comment: after a gap, or to see such
6264 // changes, clients read the merge request again. A merge publishes
6365 // merge_request_merged, then pushed for the target branch.
@@ -126,8 +128,10 @@ type EventServiceHandler interface {
126128 // "home"), subject to the same visibility. They carry the state after the
127129 // change and are published when a run or a job is created, changes status
128130 // or conclusion, or when one of its steps starts or ends (never per log
129- // line: follow a log with RunService.GetJobLog). A job update is published
130- // before the update of its run. There is no event for a change of title or
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
131135 // description, nor for a resolved comment: after a gap, or to see such
132136 // changes, clients read the merge request again. A merge publishes
133137 // merge_request_merged, then pushed for the target branch.
@@ -56,8 +56,10 @@ type EventServiceClient interface {
56 // "home"), subject to the same visibility. They carry the state after the56 // "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 status57 // 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 log58 // or conclusion, or when one of its steps starts or ends (never per log
59- // line: follow a log with RunService.GetJobLog). A job update is published59+ // line: follow a log with RunService.GetJobLog). The order of these events
60- // before the update of its run. There is no event for a change of title or60+ // 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
61 // 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
62 // changes, clients read the merge request again. A merge publishes64 // changes, clients read the merge request again. A merge publishes
63 // merge_request_merged, then pushed for the target branch.65 // merge_request_merged, then pushed for the target branch.
@@ -126,8 +128,10 @@ type EventServiceHandler interface {
126 // "home"), subject to the same visibility. They carry the state after the128 // "home"), subject to the same visibility. They carry the state after the
127 // change and are published when a run or a job is created, changes status129 // change and are published when a run or a job is created, changes status
128 // or conclusion, or when one of its steps starts or ends (never per log130 // or conclusion, or when one of its steps starts or ends (never per log
129- // line: follow a log with RunService.GetJobLog). A job update is published131+ // line: follow a log with RunService.GetJobLog). The order of these events
130- // before the update of its run. There is no event for a change of title or132+ // 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
131 // 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
132 // changes, clients read the merge request again. A merge publishes136 // changes, clients read the merge request again. A merge publishes
133 // merge_request_merged, then pushed for the target branch.137 // merge_request_merged, then pushed for the target branch.
M gen/ermite/ermiteconnect/runner.connect.go +247 -74
@@ -48,6 +48,9 @@ const (
4848 RunnerServiceHeartbeatProcedure = "/ermite.RunnerService/Heartbeat"
4949 // RunnerServiceWatchJobProcedure is the fully-qualified name of the RunnerService's WatchJob RPC.
5050 RunnerServiceWatchJobProcedure = "/ermite.RunnerService/WatchJob"
51+ // RunnerServiceCompleteJobProcedure is the fully-qualified name of the RunnerService's CompleteJob
52+ // RPC.
53+ RunnerServiceCompleteJobProcedure = "/ermite.RunnerService/CompleteJob"
5154 )
5255
5356 // RunnerServiceClient is a client for the ermite.RunnerService service.
@@ -58,37 +61,54 @@ type RunnerServiceClient interface {
5861 // (a runner serves every repository), or the root token. Fails with
5962 // unauthenticated without a valid token, permission_denied without the
6063 // scope or with a repository-restricted token, invalid_argument if a field
61- // breaks a rule, and already_exists if name is taken by another runner.
64+ // breaks a rule, and already_exists if name is taken by another runner and
65+ // replace is not set.
6266 RegisterRunner(context.Context, *connect.Request[ermite.RegisterRunnerRequest]) (*connect.Response[ermite.RegisterRunnerResponse], error)
6367 // FetchJob asks for a job to run (long poll). The server holds the call
6468 // until a queued job matches the runner, or about 50 seconds (never more
6569 // than 50) pass, then answers; without a job the response has no job and the
66- // runner calls again at once. Clients set a call timeout above 50 seconds
67- // (60 is suggested) and must not poll when they have no free capacity.
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.
6873 //
6974 // A job matches when every label of its runs-on is among the labels of the
7075 // runner (case-insensitive). A job is given to exactly one runner, even when
7176 // many runners and many polls compete; the assignment is recorded before
72- // the response is sent, and the job is then in progress. If the response is
73- // lost the job is not offered again: with no Heartbeat for 2 minutes the
74- // server fails it as a lost runner.
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).
7598 //
7699 // Needs the runner token. Fails with unauthenticated when the runner was
77100 // removed (also ending a call in progress) and invalid_argument if labels
78- // breaks a rule.
101+ // or poll_id break a rule.
79102 FetchJob(context.Context, *connect.Request[ermite.FetchJobRequest]) (*connect.Response[ermite.FetchJobResponse], error)
80- // UpdateJob reports the state of the steps of a job and, once, how the job
81- // ended. Needs the job token. Each call is applied whole or not at all, and
82- // repeating an identical call changes nothing, so a runner may retry after a
83- // network error.
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.
84107 //
85- // Fails with invalid_argument if a rule is broken (including the output
86- // limits), and with failed_precondition if a step goes back (a COMPLETED
87- // step cannot change, a step cannot go from IN_PROGRESS to PENDING).
88- // The call that carries a conclusion ends the job: its job token stops
89- // working, the server completes the job and no further call is accepted,
90- // so a retry of that last call after a lost response gets unauthenticated,
91- // which means it was applied.
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.
92112 UpdateJob(context.Context, *connect.Request[ermite.UpdateJobRequest]) (*connect.Response[ermite.UpdateJobResponse], error)
93113 // AppendLog appends lines to the log of a job. Needs the job token.
94114 //
@@ -100,46 +120,102 @@ type RunnerServiceClient interface {
100120 // - offset equal to the server's line count: the lines are appended;
101121 // - offset below it: the first (count - offset) lines were already stored
102122 // (a retry after a lost response) and are skipped, and the rest is
103- // appended; a request entirely stored changes nothing;
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;
104127 // - offset above it: out_of_range (a gap is a bug of the client; the
105128 // message gives the line count the server holds).
106129 //
107130 // The response gives the line count after the call, so a client can check
108- // itself. A request has at most 1,000 lines whose text adds up to at most
109- // 64 KiB (65,536 bytes), all in the same step; more is invalid_argument. A
110- // line of more than 64 KiB is cut by the client into several lines.
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.
111144 //
112145 // The log of a job is limited to 512 MiB (536,870,912 bytes) as stored by
113146 // the server (text, timestamps and separators). The call that would exceed
114147 // it appends nothing; the server adds a last line saying that the limit was
115148 // reached, ends the job with CI_CONCLUSION_FAILURE and RunJob.error set, and
116- // answers resource_exhausted. The runner then stops the job and sends
117- // nothing more: its calls get unauthenticated.
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.
118154 //
119155 // Fails with invalid_argument if a rule is broken (step number, text with a
120- // line feed or invalid UTF-8, missing time).
156+ // line feed or invalid UTF-8, missing time, a continued first line).
121157 AppendLog(context.Context, *connect.Request[ermite.AppendLogRequest]) (*connect.Response[ermite.AppendLogResponse], error)
122158 // Heartbeat tells the server that the runner is alive and still working on
123- // the job. Needs the job token. The runner calls it every 30 seconds for as
124- // long as the job runs, from assignment to the end. Without a heartbeat for
125- // 2 minutes the server ends the job with CI_CONCLUSION_FAILURE and the message
126- // "The self-hosted runner: <name> lost communication with the server...".
127- // Only Heartbeat counts: other calls do not keep a job alive.
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.
128171 Heartbeat(context.Context, *connect.Request[ermite.HeartbeatRequest]) (*connect.Response[ermite.HeartbeatResponse], error)
129- // WatchJob streams the requests to stop a job. Needs the job token. The
172+ // WatchJob streams what the runner must react to. Needs the job token. The
130173 // runner opens it when it starts the job and keeps it open; it reopens it
131- // after any error other than unauthenticated.
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.
132177 //
133178 // When the stream opens and a cancellation has already been requested, the
134179 // first message is that request, so a reconnect never misses one. Only the
135180 // first reason is sent: a job already cancelled by the owner does not get a
136- // second message when its timeout expires. The server also sends a
137- // keep-alive message (reason CANCEL_REASON_UNSPECIFIED) at least every 30
138- // seconds, which the client ignores. The stream ends without error when
139- // the job ends. The runner stops the job as the official runner does
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
140188 // (interrupt, then terminate, then kill) and reports the conclusion given
141- // for the reason with UpdateJob.
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.
142197 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)
143219 }
144220
145221 // NewRunnerServiceClient constructs a client for the ermite.RunnerService service. By default, it
@@ -189,6 +265,12 @@ func NewRunnerServiceClient(httpClient connect.HTTPClient, baseURL string, opts
189265 connect.WithSchema(runnerServiceMethods.ByName("WatchJob")),
190266 connect.WithClientOptions(opts...),
191267 ),
268+ completeJob: connect.NewClient[ermite.CompleteJobRequest, ermite.CompleteJobResponse](
269+ httpClient,
270+ baseURL+RunnerServiceCompleteJobProcedure,
271+ connect.WithSchema(runnerServiceMethods.ByName("CompleteJob")),
272+ connect.WithClientOptions(opts...),
273+ ),
192274 }
193275 }
194276
@@ -200,6 +282,7 @@ type runnerServiceClient struct {
200282 appendLog *connect.Client[ermite.AppendLogRequest, ermite.AppendLogResponse]
201283 heartbeat *connect.Client[ermite.HeartbeatRequest, ermite.HeartbeatResponse]
202284 watchJob *connect.Client[ermite.WatchJobRequest, ermite.WatchJobResponse]
285+ completeJob *connect.Client[ermite.CompleteJobRequest, ermite.CompleteJobResponse]
203286 }
204287
205288 // RegisterRunner calls ermite.RunnerService.RegisterRunner.
@@ -232,6 +315,11 @@ func (c *runnerServiceClient) WatchJob(ctx context.Context, req *connect.Request
232315 return c.watchJob.CallServerStream(ctx, req)
233316 }
234317
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+
235323 // RunnerServiceHandler is an implementation of the ermite.RunnerService service.
236324 type RunnerServiceHandler interface {
237325 // RegisterRunner registers a runner and returns its runner token, which is
@@ -240,37 +328,54 @@ type RunnerServiceHandler interface {
240328 // (a runner serves every repository), or the root token. Fails with
241329 // unauthenticated without a valid token, permission_denied without the
242330 // scope or with a repository-restricted token, invalid_argument if a field
243- // breaks a rule, and already_exists if name is taken by another runner.
331+ // breaks a rule, and already_exists if name is taken by another runner and
332+ // replace is not set.
244333 RegisterRunner(context.Context, *connect.Request[ermite.RegisterRunnerRequest]) (*connect.Response[ermite.RegisterRunnerResponse], error)
245334 // FetchJob asks for a job to run (long poll). The server holds the call
246335 // until a queued job matches the runner, or about 50 seconds (never more
247336 // than 50) pass, then answers; without a job the response has no job and the
248- // runner calls again at once. Clients set a call timeout above 50 seconds
249- // (60 is suggested) and must not poll when they have no free capacity.
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.
250340 //
251341 // A job matches when every label of its runs-on is among the labels of the
252342 // runner (case-insensitive). A job is given to exactly one runner, even when
253343 // many runners and many polls compete; the assignment is recorded before
254- // the response is sent, and the job is then in progress. If the response is
255- // lost the job is not offered again: with no Heartbeat for 2 minutes the
256- // server fails it as a lost runner.
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).
257365 //
258366 // Needs the runner token. Fails with unauthenticated when the runner was
259367 // removed (also ending a call in progress) and invalid_argument if labels
260- // breaks a rule.
368+ // or poll_id break a rule.
261369 FetchJob(context.Context, *connect.Request[ermite.FetchJobRequest]) (*connect.Response[ermite.FetchJobResponse], error)
262- // UpdateJob reports the state of the steps of a job and, once, how the job
263- // ended. Needs the job token. Each call is applied whole or not at all, and
264- // repeating an identical call changes nothing, so a runner may retry after a
265- // network error.
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.
266374 //
267- // Fails with invalid_argument if a rule is broken (including the output
268- // limits), and with failed_precondition if a step goes back (a COMPLETED
269- // step cannot change, a step cannot go from IN_PROGRESS to PENDING).
270- // The call that carries a conclusion ends the job: its job token stops
271- // working, the server completes the job and no further call is accepted,
272- // so a retry of that last call after a lost response gets unauthenticated,
273- // which means it was applied.
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.
274379 UpdateJob(context.Context, *connect.Request[ermite.UpdateJobRequest]) (*connect.Response[ermite.UpdateJobResponse], error)
275380 // AppendLog appends lines to the log of a job. Needs the job token.
276381 //
@@ -282,46 +387,102 @@ type RunnerServiceHandler interface {
282387 // - offset equal to the server's line count: the lines are appended;
283388 // - offset below it: the first (count - offset) lines were already stored
284389 // (a retry after a lost response) and are skipped, and the rest is
285- // appended; a request entirely stored changes nothing;
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;
286394 // - offset above it: out_of_range (a gap is a bug of the client; the
287395 // message gives the line count the server holds).
288396 //
289397 // The response gives the line count after the call, so a client can check
290- // itself. A request has at most 1,000 lines whose text adds up to at most
291- // 64 KiB (65,536 bytes), all in the same step; more is invalid_argument. A
292- // line of more than 64 KiB is cut by the client into several lines.
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.
293411 //
294412 // The log of a job is limited to 512 MiB (536,870,912 bytes) as stored by
295413 // the server (text, timestamps and separators). The call that would exceed
296414 // it appends nothing; the server adds a last line saying that the limit was
297415 // reached, ends the job with CI_CONCLUSION_FAILURE and RunJob.error set, and
298- // answers resource_exhausted. The runner then stops the job and sends
299- // nothing more: its calls get unauthenticated.
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.
300421 //
301422 // Fails with invalid_argument if a rule is broken (step number, text with a
302- // line feed or invalid UTF-8, missing time).
423+ // line feed or invalid UTF-8, missing time, a continued first line).
303424 AppendLog(context.Context, *connect.Request[ermite.AppendLogRequest]) (*connect.Response[ermite.AppendLogResponse], error)
304425 // Heartbeat tells the server that the runner is alive and still working on
305- // the job. Needs the job token. The runner calls it every 30 seconds for as
306- // long as the job runs, from assignment to the end. Without a heartbeat for
307- // 2 minutes the server ends the job with CI_CONCLUSION_FAILURE and the message
308- // "The self-hosted runner: <name> lost communication with the server...".
309- // Only Heartbeat counts: other calls do not keep a job alive.
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.
310438 Heartbeat(context.Context, *connect.Request[ermite.HeartbeatRequest]) (*connect.Response[ermite.HeartbeatResponse], error)
311- // WatchJob streams the requests to stop a job. Needs the job token. The
439+ // WatchJob streams what the runner must react to. Needs the job token. The
312440 // runner opens it when it starts the job and keeps it open; it reopens it
313- // after any error other than unauthenticated.
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.
314444 //
315445 // When the stream opens and a cancellation has already been requested, the
316446 // first message is that request, so a reconnect never misses one. Only the
317447 // first reason is sent: a job already cancelled by the owner does not get a
318- // second message when its timeout expires. The server also sends a
319- // keep-alive message (reason CANCEL_REASON_UNSPECIFIED) at least every 30
320- // seconds, which the client ignores. The stream ends without error when
321- // the job ends. The runner stops the job as the official runner does
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
322455 // (interrupt, then terminate, then kill) and reports the conclusion given
323- // for the reason with UpdateJob.
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.
324464 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)
325486 }
326487
327488 // NewRunnerServiceHandler builds an HTTP handler from the service implementation. It returns the
@@ -367,6 +528,12 @@ func NewRunnerServiceHandler(svc RunnerServiceHandler, opts ...connect.HandlerOp
367528 connect.WithSchema(runnerServiceMethods.ByName("WatchJob")),
368529 connect.WithHandlerOptions(opts...),
369530 )
531+ runnerServiceCompleteJobHandler := connect.NewUnaryHandler(
532+ RunnerServiceCompleteJobProcedure,
533+ svc.CompleteJob,
534+ connect.WithSchema(runnerServiceMethods.ByName("CompleteJob")),
535+ connect.WithHandlerOptions(opts...),
536+ )
370537 return "/ermite.RunnerService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
371538 switch r.URL.Path {
372539 case RunnerServiceRegisterRunnerProcedure:
@@ -381,6 +548,8 @@ func NewRunnerServiceHandler(svc RunnerServiceHandler, opts ...connect.HandlerOp
381548 runnerServiceHeartbeatHandler.ServeHTTP(w, r)
382549 case RunnerServiceWatchJobProcedure:
383550 runnerServiceWatchJobHandler.ServeHTTP(w, r)
551+ case RunnerServiceCompleteJobProcedure:
552+ runnerServiceCompleteJobHandler.ServeHTTP(w, r)
384553 default:
385554 http.NotFound(w, r)
386555 }
@@ -413,3 +582,7 @@ func (UnimplementedRunnerServiceHandler) Heartbeat(context.Context, *connect.Req
413582 func (UnimplementedRunnerServiceHandler) WatchJob(context.Context, *connect.Request[ermite.WatchJobRequest], *connect.ServerStream[ermite.WatchJobResponse]) error {
414583 return connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunnerService.WatchJob is not implemented"))
415584 }
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+}
@@ -48,6 +48,9 @@ const (
48 RunnerServiceHeartbeatProcedure = "/ermite.RunnerService/Heartbeat"48 RunnerServiceHeartbeatProcedure = "/ermite.RunnerService/Heartbeat"
49 // RunnerServiceWatchJobProcedure is the fully-qualified name of the RunnerService's WatchJob RPC.49 // RunnerServiceWatchJobProcedure is the fully-qualified name of the RunnerService's WatchJob RPC.
50 RunnerServiceWatchJobProcedure = "/ermite.RunnerService/WatchJob"50 RunnerServiceWatchJobProcedure = "/ermite.RunnerService/WatchJob"
51+ // RunnerServiceCompleteJobProcedure is the fully-qualified name of the RunnerService's CompleteJob
52+ // RPC.
53+ RunnerServiceCompleteJobProcedure = "/ermite.RunnerService/CompleteJob"
51 )54 )
52 55
53 // RunnerServiceClient is a client for the ermite.RunnerService service.56 // RunnerServiceClient is a client for the ermite.RunnerService service.
@@ -58,37 +61,54 @@ type RunnerServiceClient interface {
58 // (a runner serves every repository), or the root token. Fails with61 // (a runner serves every repository), or the root token. Fails with
59 // unauthenticated without a valid token, permission_denied without the62 // unauthenticated without a valid token, permission_denied without the
60 // scope or with a repository-restricted token, invalid_argument if a field63 // scope or with a repository-restricted token, invalid_argument if a field
61- // breaks a rule, and already_exists if name is taken by another runner.64+ // breaks a rule, and already_exists if name is taken by another runner and
65+ // replace is not set.
62 RegisterRunner(context.Context, *connect.Request[ermite.RegisterRunnerRequest]) (*connect.Response[ermite.RegisterRunnerResponse], error)66 RegisterRunner(context.Context, *connect.Request[ermite.RegisterRunnerRequest]) (*connect.Response[ermite.RegisterRunnerResponse], error)
63 // FetchJob asks for a job to run (long poll). The server holds the call67 // FetchJob asks for a job to run (long poll). The server holds the call
64 // until a queued job matches the runner, or about 50 seconds (never more68 // until a queued job matches the runner, or about 50 seconds (never more
65 // than 50) pass, then answers; without a job the response has no job and the69 // than 50) pass, then answers; without a job the response has no job and the
66- // runner calls again at once. Clients set a call timeout above 50 seconds70+ // runner calls again after retry_after. Clients set a call timeout above 50
67- // (60 is suggested) and must not poll when they have no free capacity.71+ // seconds (60 is suggested) and must not poll when they have no free
72+ // capacity.
68 //73 //
69 // A job matches when every label of its runs-on is among the labels of the74 // A job matches when every label of its runs-on is among the labels of the
70 // runner (case-insensitive). A job is given to exactly one runner, even when75 // runner (case-insensitive). A job is given to exactly one runner, even when
71 // many runners and many polls compete; the assignment is recorded before76 // many runners and many polls compete; the assignment is recorded before
72- // the response is sent, and the job is then in progress. If the response is77+ // the response is sent. The job stays QUEUED for the owner until it is
73- // lost the job is not offered again: with no Heartbeat for 2 minutes the78+ // acknowledged.
74- // server fails it as a lost runner.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).
75 //98 //
76 // Needs the runner token. Fails with unauthenticated when the runner was99 // Needs the runner token. Fails with unauthenticated when the runner was
77 // removed (also ending a call in progress) and invalid_argument if labels100 // removed (also ending a call in progress) and invalid_argument if labels
78- // breaks a rule.101+ // or poll_id break a rule.
79 FetchJob(context.Context, *connect.Request[ermite.FetchJobRequest]) (*connect.Response[ermite.FetchJobResponse], error)102 FetchJob(context.Context, *connect.Request[ermite.FetchJobRequest]) (*connect.Response[ermite.FetchJobResponse], error)
80- // UpdateJob reports the state of the steps of a job and, once, how the job103+ // UpdateJob reports the state of the steps of a job while it runs. Needs the
81- // ended. Needs the job token. Each call is applied whole or not at all, and104+ // job token. Each call is applied whole or not at all, and repeating an
82- // repeating an identical call changes nothing, so a runner may retry after a105+ // identical call changes nothing, so a runner may retry after a network
83- // network error.106+ // error. To end the job use CompleteJob.
84 //107 //
85- // Fails with invalid_argument if a rule is broken (including the output108+ // Fails with invalid_argument if a rule is broken, with failed_precondition
86- // limits), and with failed_precondition if a step goes back (a COMPLETED109+ // if a step goes back (a COMPLETED step cannot change, a step cannot go from
87- // step cannot change, a step cannot go from IN_PROGRESS to PENDING).110+ // IN_PROGRESS to PENDING) and with failed_precondition and a JobEnded detail
88- // The call that carries a conclusion ends the job: its job token stops111+ // if the job has ended.
89- // working, the server completes the job and no further call is accepted,
90- // so a retry of that last call after a lost response gets unauthenticated,
91- // which means it was applied.
92 UpdateJob(context.Context, *connect.Request[ermite.UpdateJobRequest]) (*connect.Response[ermite.UpdateJobResponse], error)112 UpdateJob(context.Context, *connect.Request[ermite.UpdateJobRequest]) (*connect.Response[ermite.UpdateJobResponse], error)
93 // AppendLog appends lines to the log of a job. Needs the job token.113 // AppendLog appends lines to the log of a job. Needs the job token.
94 //114 //
@@ -100,46 +120,102 @@ type RunnerServiceClient interface {
100 // - offset equal to the server's line count: the lines are appended;120 // - offset equal to the server's line count: the lines are appended;
101 // - offset below it: the first (count - offset) lines were already stored121 // - offset below it: the first (count - offset) lines were already stored
102 // (a retry after a lost response) and are skipped, and the rest is122 // (a retry after a lost response) and are skipped, and the rest is
103- // appended; a request entirely stored changes nothing;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;
104 // - offset above it: out_of_range (a gap is a bug of the client; the127 // - offset above it: out_of_range (a gap is a bug of the client; the
105 // message gives the line count the server holds).128 // message gives the line count the server holds).
106 //129 //
107 // The response gives the line count after the call, so a client can check130 // The response gives the line count after the call, so a client can check
108- // itself. A request has at most 1,000 lines whose text adds up to at most131+ // itself. Offsets resume only after a network error within the same runner
109- // 64 KiB (65,536 bytes), all in the same step; more is invalid_argument. A132+ // process; they are not a way to resume a job after a crash.
110- // line of more than 64 KiB is cut by the client into several lines.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.
111 //144 //
112 // The log of a job is limited to 512 MiB (536,870,912 bytes) as stored by145 // The log of a job is limited to 512 MiB (536,870,912 bytes) as stored by
113 // the server (text, timestamps and separators). The call that would exceed146 // the server (text, timestamps and separators). The call that would exceed
114 // it appends nothing; the server adds a last line saying that the limit was147 // it appends nothing; the server adds a last line saying that the limit was
115 // reached, ends the job with CI_CONCLUSION_FAILURE and RunJob.error set, and148 // reached, ends the job with CI_CONCLUSION_FAILURE and RunJob.error set, and
116- // answers resource_exhausted. The runner then stops the job and sends149+ // answers failed_precondition with a JobEnded detail like any call after the
117- // nothing more: its calls get unauthenticated.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.
118 //154 //
119 // Fails with invalid_argument if a rule is broken (step number, text with a155 // Fails with invalid_argument if a rule is broken (step number, text with a
120- // line feed or invalid UTF-8, missing time).156+ // line feed or invalid UTF-8, missing time, a continued first line).
121 AppendLog(context.Context, *connect.Request[ermite.AppendLogRequest]) (*connect.Response[ermite.AppendLogResponse], error)157 AppendLog(context.Context, *connect.Request[ermite.AppendLogRequest]) (*connect.Response[ermite.AppendLogResponse], error)
122 // Heartbeat tells the server that the runner is alive and still working on158 // Heartbeat tells the server that the runner is alive and still working on
123- // the job. Needs the job token. The runner calls it every 30 seconds for as159+ // the job. Needs the job token. The first one acknowledges the assignment
124- // long as the job runs, from assignment to the end. Without a heartbeat for160+ // (see FetchJob). The runner then calls it every 30 seconds for as long as
125- // 2 minutes the server ends the job with CI_CONCLUSION_FAILURE and the message161+ // the job runs. The deadline is 2 minutes after the later of the last
126- // "The self-hosted runner: <name> lost communication with the server...".162+ // heartbeat and the start of the server (so a server restart does not fail
127- // Only Heartbeat counts: other calls do not keep a job alive.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.
128 Heartbeat(context.Context, *connect.Request[ermite.HeartbeatRequest]) (*connect.Response[ermite.HeartbeatResponse], error)171 Heartbeat(context.Context, *connect.Request[ermite.HeartbeatRequest]) (*connect.Response[ermite.HeartbeatResponse], error)
129- // WatchJob streams the requests to stop a job. Needs the job token. The172+ // WatchJob streams what the runner must react to. Needs the job token. The
130 // runner opens it when it starts the job and keeps it open; it reopens it173 // runner opens it when it starts the job and keeps it open; it reopens it
131- // after any error other than unauthenticated.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.
132 //177 //
133 // When the stream opens and a cancellation has already been requested, the178 // When the stream opens and a cancellation has already been requested, the
134 // first message is that request, so a reconnect never misses one. Only the179 // first message is that request, so a reconnect never misses one. Only the
135 // first reason is sent: a job already cancelled by the owner does not get a180 // first reason is sent: a job already cancelled by the owner does not get a
136- // second message when its timeout expires. The server also sends a181+ // second message when its time budget expires. The server also sends a
137- // keep-alive message (reason CANCEL_REASON_UNSPECIFIED) at least every 30182+ // Keepalive at least every 30 seconds, which the client ignores. When the
138- // seconds, which the client ignores. The stream ends without error when183+ // job ends the server sends JobEnded, then closes the stream without error.
139- // the job ends. The runner stops the job as the official runner does184+ // 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
140 // (interrupt, then terminate, then kill) and reports the conclusion given188 // (interrupt, then terminate, then kill) and reports the conclusion given
141- // for the reason with UpdateJob.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.
142 WatchJob(context.Context, *connect.Request[ermite.WatchJobRequest]) (*connect.ServerStreamForClient[ermite.WatchJobResponse], error)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)
143 }219 }
144 220
145 // NewRunnerServiceClient constructs a client for the ermite.RunnerService service. By default, it221 // NewRunnerServiceClient constructs a client for the ermite.RunnerService service. By default, it
@@ -189,6 +265,12 @@ func NewRunnerServiceClient(httpClient connect.HTTPClient, baseURL string, opts
189 connect.WithSchema(runnerServiceMethods.ByName("WatchJob")),265 connect.WithSchema(runnerServiceMethods.ByName("WatchJob")),
190 connect.WithClientOptions(opts...),266 connect.WithClientOptions(opts...),
191 ),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+ ),
192 }274 }
193 }275 }
194 276
@@ -200,6 +282,7 @@ type runnerServiceClient struct {
200 appendLog *connect.Client[ermite.AppendLogRequest, ermite.AppendLogResponse]282 appendLog *connect.Client[ermite.AppendLogRequest, ermite.AppendLogResponse]
201 heartbeat *connect.Client[ermite.HeartbeatRequest, ermite.HeartbeatResponse]283 heartbeat *connect.Client[ermite.HeartbeatRequest, ermite.HeartbeatResponse]
202 watchJob *connect.Client[ermite.WatchJobRequest, ermite.WatchJobResponse]284 watchJob *connect.Client[ermite.WatchJobRequest, ermite.WatchJobResponse]
285+ completeJob *connect.Client[ermite.CompleteJobRequest, ermite.CompleteJobResponse]
203 }286 }
204 287
205 // RegisterRunner calls ermite.RunnerService.RegisterRunner.288 // RegisterRunner calls ermite.RunnerService.RegisterRunner.
@@ -232,6 +315,11 @@ func (c *runnerServiceClient) WatchJob(ctx context.Context, req *connect.Request
232 return c.watchJob.CallServerStream(ctx, req)315 return c.watchJob.CallServerStream(ctx, req)
233 }316 }
234 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+
235 // RunnerServiceHandler is an implementation of the ermite.RunnerService service.323 // RunnerServiceHandler is an implementation of the ermite.RunnerService service.
236 type RunnerServiceHandler interface {324 type RunnerServiceHandler interface {
237 // RegisterRunner registers a runner and returns its runner token, which is325 // RegisterRunner registers a runner and returns its runner token, which is
@@ -240,37 +328,54 @@ type RunnerServiceHandler interface {
240 // (a runner serves every repository), or the root token. Fails with328 // (a runner serves every repository), or the root token. Fails with
241 // unauthenticated without a valid token, permission_denied without the329 // unauthenticated without a valid token, permission_denied without the
242 // scope or with a repository-restricted token, invalid_argument if a field330 // scope or with a repository-restricted token, invalid_argument if a field
243- // breaks a rule, and already_exists if name is taken by another runner.331+ // breaks a rule, and already_exists if name is taken by another runner and
332+ // replace is not set.
244 RegisterRunner(context.Context, *connect.Request[ermite.RegisterRunnerRequest]) (*connect.Response[ermite.RegisterRunnerResponse], error)333 RegisterRunner(context.Context, *connect.Request[ermite.RegisterRunnerRequest]) (*connect.Response[ermite.RegisterRunnerResponse], error)
245 // FetchJob asks for a job to run (long poll). The server holds the call334 // FetchJob asks for a job to run (long poll). The server holds the call
246 // until a queued job matches the runner, or about 50 seconds (never more335 // until a queued job matches the runner, or about 50 seconds (never more
247 // than 50) pass, then answers; without a job the response has no job and the336 // than 50) pass, then answers; without a job the response has no job and the
248- // runner calls again at once. Clients set a call timeout above 50 seconds337+ // runner calls again after retry_after. Clients set a call timeout above 50
249- // (60 is suggested) and must not poll when they have no free capacity.338+ // seconds (60 is suggested) and must not poll when they have no free
339+ // capacity.
250 //340 //
251 // A job matches when every label of its runs-on is among the labels of the341 // A job matches when every label of its runs-on is among the labels of the
252 // runner (case-insensitive). A job is given to exactly one runner, even when342 // runner (case-insensitive). A job is given to exactly one runner, even when
253 // many runners and many polls compete; the assignment is recorded before343 // many runners and many polls compete; the assignment is recorded before
254- // the response is sent, and the job is then in progress. If the response is344+ // the response is sent. The job stays QUEUED for the owner until it is
255- // lost the job is not offered again: with no Heartbeat for 2 minutes the345+ // acknowledged.
256- // server fails it as a lost runner.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).
257 //365 //
258 // Needs the runner token. Fails with unauthenticated when the runner was366 // Needs the runner token. Fails with unauthenticated when the runner was
259 // removed (also ending a call in progress) and invalid_argument if labels367 // removed (also ending a call in progress) and invalid_argument if labels
260- // breaks a rule.368+ // or poll_id break a rule.
261 FetchJob(context.Context, *connect.Request[ermite.FetchJobRequest]) (*connect.Response[ermite.FetchJobResponse], error)369 FetchJob(context.Context, *connect.Request[ermite.FetchJobRequest]) (*connect.Response[ermite.FetchJobResponse], error)
262- // UpdateJob reports the state of the steps of a job and, once, how the job370+ // UpdateJob reports the state of the steps of a job while it runs. Needs the
263- // ended. Needs the job token. Each call is applied whole or not at all, and371+ // job token. Each call is applied whole or not at all, and repeating an
264- // repeating an identical call changes nothing, so a runner may retry after a372+ // identical call changes nothing, so a runner may retry after a network
265- // network error.373+ // error. To end the job use CompleteJob.
266 //374 //
267- // Fails with invalid_argument if a rule is broken (including the output375+ // Fails with invalid_argument if a rule is broken, with failed_precondition
268- // limits), and with failed_precondition if a step goes back (a COMPLETED376+ // if a step goes back (a COMPLETED step cannot change, a step cannot go from
269- // step cannot change, a step cannot go from IN_PROGRESS to PENDING).377+ // IN_PROGRESS to PENDING) and with failed_precondition and a JobEnded detail
270- // The call that carries a conclusion ends the job: its job token stops378+ // if the job has ended.
271- // working, the server completes the job and no further call is accepted,
272- // so a retry of that last call after a lost response gets unauthenticated,
273- // which means it was applied.
274 UpdateJob(context.Context, *connect.Request[ermite.UpdateJobRequest]) (*connect.Response[ermite.UpdateJobResponse], error)379 UpdateJob(context.Context, *connect.Request[ermite.UpdateJobRequest]) (*connect.Response[ermite.UpdateJobResponse], error)
275 // AppendLog appends lines to the log of a job. Needs the job token.380 // AppendLog appends lines to the log of a job. Needs the job token.
276 //381 //
@@ -282,46 +387,102 @@ type RunnerServiceHandler interface {
282 // - offset equal to the server's line count: the lines are appended;387 // - offset equal to the server's line count: the lines are appended;
283 // - offset below it: the first (count - offset) lines were already stored388 // - offset below it: the first (count - offset) lines were already stored
284 // (a retry after a lost response) and are skipped, and the rest is389 // (a retry after a lost response) and are skipped, and the rest is
285- // appended; a request entirely stored changes nothing;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;
286 // - offset above it: out_of_range (a gap is a bug of the client; the394 // - offset above it: out_of_range (a gap is a bug of the client; the
287 // message gives the line count the server holds).395 // message gives the line count the server holds).
288 //396 //
289 // The response gives the line count after the call, so a client can check397 // The response gives the line count after the call, so a client can check
290- // itself. A request has at most 1,000 lines whose text adds up to at most398+ // itself. Offsets resume only after a network error within the same runner
291- // 64 KiB (65,536 bytes), all in the same step; more is invalid_argument. A399+ // process; they are not a way to resume a job after a crash.
292- // line of more than 64 KiB is cut by the client into several lines.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.
293 //411 //
294 // The log of a job is limited to 512 MiB (536,870,912 bytes) as stored by412 // The log of a job is limited to 512 MiB (536,870,912 bytes) as stored by
295 // the server (text, timestamps and separators). The call that would exceed413 // the server (text, timestamps and separators). The call that would exceed
296 // it appends nothing; the server adds a last line saying that the limit was414 // it appends nothing; the server adds a last line saying that the limit was
297 // reached, ends the job with CI_CONCLUSION_FAILURE and RunJob.error set, and415 // reached, ends the job with CI_CONCLUSION_FAILURE and RunJob.error set, and
298- // answers resource_exhausted. The runner then stops the job and sends416+ // answers failed_precondition with a JobEnded detail like any call after the
299- // nothing more: its calls get unauthenticated.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.
300 //421 //
301 // Fails with invalid_argument if a rule is broken (step number, text with a422 // Fails with invalid_argument if a rule is broken (step number, text with a
302- // line feed or invalid UTF-8, missing time).423+ // line feed or invalid UTF-8, missing time, a continued first line).
303 AppendLog(context.Context, *connect.Request[ermite.AppendLogRequest]) (*connect.Response[ermite.AppendLogResponse], error)424 AppendLog(context.Context, *connect.Request[ermite.AppendLogRequest]) (*connect.Response[ermite.AppendLogResponse], error)
304 // Heartbeat tells the server that the runner is alive and still working on425 // Heartbeat tells the server that the runner is alive and still working on
305- // the job. Needs the job token. The runner calls it every 30 seconds for as426+ // the job. Needs the job token. The first one acknowledges the assignment
306- // long as the job runs, from assignment to the end. Without a heartbeat for427+ // (see FetchJob). The runner then calls it every 30 seconds for as long as
307- // 2 minutes the server ends the job with CI_CONCLUSION_FAILURE and the message428+ // the job runs. The deadline is 2 minutes after the later of the last
308- // "The self-hosted runner: <name> lost communication with the server...".429+ // heartbeat and the start of the server (so a server restart does not fail
309- // Only Heartbeat counts: other calls do not keep a job alive.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.
310 Heartbeat(context.Context, *connect.Request[ermite.HeartbeatRequest]) (*connect.Response[ermite.HeartbeatResponse], error)438 Heartbeat(context.Context, *connect.Request[ermite.HeartbeatRequest]) (*connect.Response[ermite.HeartbeatResponse], error)
311- // WatchJob streams the requests to stop a job. Needs the job token. The439+ // WatchJob streams what the runner must react to. Needs the job token. The
312 // runner opens it when it starts the job and keeps it open; it reopens it440 // runner opens it when it starts the job and keeps it open; it reopens it
313- // after any error other than unauthenticated.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.
314 //444 //
315 // When the stream opens and a cancellation has already been requested, the445 // When the stream opens and a cancellation has already been requested, the
316 // first message is that request, so a reconnect never misses one. Only the446 // first message is that request, so a reconnect never misses one. Only the
317 // first reason is sent: a job already cancelled by the owner does not get a447 // first reason is sent: a job already cancelled by the owner does not get a
318- // second message when its timeout expires. The server also sends a448+ // second message when its time budget expires. The server also sends a
319- // keep-alive message (reason CANCEL_REASON_UNSPECIFIED) at least every 30449+ // Keepalive at least every 30 seconds, which the client ignores. When the
320- // seconds, which the client ignores. The stream ends without error when450+ // job ends the server sends JobEnded, then closes the stream without error.
321- // the job ends. The runner stops the job as the official runner does451+ // 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
322 // (interrupt, then terminate, then kill) and reports the conclusion given455 // (interrupt, then terminate, then kill) and reports the conclusion given
323- // for the reason with UpdateJob.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.
324 WatchJob(context.Context, *connect.Request[ermite.WatchJobRequest], *connect.ServerStream[ermite.WatchJobResponse]) error464 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)
325 }486 }
326 487
327 // NewRunnerServiceHandler builds an HTTP handler from the service implementation. It returns the488 // NewRunnerServiceHandler builds an HTTP handler from the service implementation. It returns the
@@ -367,6 +528,12 @@ func NewRunnerServiceHandler(svc RunnerServiceHandler, opts ...connect.HandlerOp
367 connect.WithSchema(runnerServiceMethods.ByName("WatchJob")),528 connect.WithSchema(runnerServiceMethods.ByName("WatchJob")),
368 connect.WithHandlerOptions(opts...),529 connect.WithHandlerOptions(opts...),
369 )530 )
531+ runnerServiceCompleteJobHandler := connect.NewUnaryHandler(
532+ RunnerServiceCompleteJobProcedure,
533+ svc.CompleteJob,
534+ connect.WithSchema(runnerServiceMethods.ByName("CompleteJob")),
535+ connect.WithHandlerOptions(opts...),
536+ )
370 return "/ermite.RunnerService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {537 return "/ermite.RunnerService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
371 switch r.URL.Path {538 switch r.URL.Path {
372 case RunnerServiceRegisterRunnerProcedure:539 case RunnerServiceRegisterRunnerProcedure:
@@ -381,6 +548,8 @@ func NewRunnerServiceHandler(svc RunnerServiceHandler, opts ...connect.HandlerOp
381 runnerServiceHeartbeatHandler.ServeHTTP(w, r)548 runnerServiceHeartbeatHandler.ServeHTTP(w, r)
382 case RunnerServiceWatchJobProcedure:549 case RunnerServiceWatchJobProcedure:
383 runnerServiceWatchJobHandler.ServeHTTP(w, r)550 runnerServiceWatchJobHandler.ServeHTTP(w, r)
551+ case RunnerServiceCompleteJobProcedure:
552+ runnerServiceCompleteJobHandler.ServeHTTP(w, r)
384 default:553 default:
385 http.NotFound(w, r)554 http.NotFound(w, r)
386 }555 }
@@ -413,3 +582,7 @@ func (UnimplementedRunnerServiceHandler) Heartbeat(context.Context, *connect.Req
413 func (UnimplementedRunnerServiceHandler) WatchJob(context.Context, *connect.Request[ermite.WatchJobRequest], *connect.ServerStream[ermite.WatchJobResponse]) error {582 func (UnimplementedRunnerServiceHandler) WatchJob(context.Context, *connect.Request[ermite.WatchJobRequest], *connect.ServerStream[ermite.WatchJobResponse]) error {
414 return connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunnerService.WatchJob is not implemented"))583 return connect.NewError(connect.CodeUnimplemented, errors.New("ermite.RunnerService.WatchJob is not implemented"))
415 }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 +1 -1 generated file: collapsed
M gen/ermite/run.pb.go +48 -8 generated file: collapsed
M gen/ermite/runner.pb.go +593 -164
@@ -11,6 +11,7 @@ package ermite
1111 import (
1212 protoreflect "google.golang.org/protobuf/reflect/protoreflect"
1313 protoimpl "google.golang.org/protobuf/runtime/protoimpl"
14+ durationpb "google.golang.org/protobuf/types/known/durationpb"
1415 timestamppb "google.golang.org/protobuf/types/known/timestamppb"
1516 reflect "reflect"
1617 sync "sync"
@@ -287,8 +288,7 @@ func (RunnerIsolation) EnumDescriptor() ([]byte, []int) {
287288 type CancelReason int32
288289
289290 const (
290- // Not set. A WatchJobResponse with this reason is a keep-alive and carries
291- // no request.
291+ // Not set. Never sent in a CancelRequest.
292292 CancelReason_CANCEL_REASON_UNSPECIFIED CancelReason = 0
293293 // The owner cancelled the run (RunService.CancelRun, web or CLI). The job
294294 // ends CI_CONCLUSION_CANCELLED.
@@ -296,8 +296,8 @@ const (
296296 // Another job of the same matrix failed and the strategy has fail-fast. The
297297 // job ends CI_CONCLUSION_CANCELLED.
298298 CancelReason_CANCEL_REASON_FAIL_FAST CancelReason = 2
299- // The job exceeded its timeout_minutes without the runner ending it. The
300- // job ends CI_CONCLUSION_TIMED_OUT.
299+ // The job exceeded its time budget (see Job.timeout_minutes). The job ends
300+ // CI_CONCLUSION_TIMED_OUT.
301301 CancelReason_CANCEL_REASON_TIMEOUT CancelReason = 3
302302 )
303303
@@ -367,7 +367,14 @@ type RegisterRunnerRequest struct {
367367 Arch string `protobuf:"bytes,5,opt,name=arch,proto3" json:"arch,omitempty"`
368368 // Os is the operating system: "linux". Other values are invalid_argument
369369 // (the set may grow with compatible additions).
370- Os string `protobuf:"bytes,6,opt,name=os,proto3" json:"os,omitempty"`
370+ Os string `protobuf:"bytes,6,opt,name=os,proto3" json:"os,omitempty"`
371+ // Replace removes the runner that already has this name, if any, before
372+ // registering this one: its runner token is revoked at once, as in
373+ // RunService.RemoveRunner, and a job it is running is not affected. The new
374+ // runner gets a new runner_id. Needs the same scope as registering. Without
375+ // replace, a name in use is already_exists. It is how a runner re-registers
376+ // after losing its state.
377+ Replace bool `protobuf:"varint,7,opt,name=replace,proto3" json:"replace,omitempty"`
371378 unknownFields protoimpl.UnknownFields
372379 sizeCache protoimpl.SizeCache
373380 }
@@ -444,6 +451,13 @@ func (x *RegisterRunnerRequest) GetOs() string {
444451 return ""
445452 }
446453
454+func (x *RegisterRunnerRequest) GetReplace() bool {
455+ if x != nil {
456+ return x.Replace
457+ }
458+ return false
459+}
460+
447461 // RegisterRunnerResponse holds the new runner and its secret.
448462 type RegisterRunnerResponse struct {
449463 state protoimpl.MessageState `protogen:"open.v1"`
@@ -451,9 +465,10 @@ type RegisterRunnerResponse struct {
451465 // a secret.
452466 RunnerId string `protobuf:"bytes,1,opt,name=runner_id,json=runnerId,proto3" json:"runner_id,omitempty"`
453467 // RunnerToken is the secret of the runner, to present as "Authorization:
454- // Bearer <runner_token>" to FetchJob. It starts with "ermiterun_" and is
455- // returned once: the server keeps only its SHA-256, the runner must store
456- // it (mode 0600). It stops working when the runner is removed.
468+ // Bearer <runner_token>" to FetchJob: "ermiterun_" and 43 base64url
469+ // characters (32 random bytes, no padding). It is returned once: the server
470+ // keeps only its SHA-256, the runner must store it (mode 0600). It stops
471+ // working when the runner is removed or replaced.
457472 RunnerToken string `protobuf:"bytes,2,opt,name=runner_token,json=runnerToken,proto3" json:"runner_token,omitempty"`
458473 unknownFields protoimpl.UnknownFields
459474 sizeCache protoimpl.SizeCache
@@ -510,7 +525,14 @@ type FetchJobRequest struct {
510525 // labels of the runner, which is the normal case. Otherwise each one must be
511526 // a label of the runner (invalid_argument if not) and only jobs whose runs-on
512527 // labels are all among them are offered.
513- Labels []string `protobuf:"bytes,1,rep,name=labels,proto3" json:"labels,omitempty"`
528+ Labels []string `protobuf:"bytes,1,rep,name=labels,proto3" json:"labels,omitempty"`
529+ // PollId identifies this poll, chosen by the client (for example 16 random
530+ // bytes in base64url), 1 to 64 characters among letters, digits, "_" and
531+ // "-". A client that retries a call whose response it did not get sends the
532+ // same poll_id, and gets the assignment the first call made, if it made one
533+ // (see FetchJob). A new poll uses a new id. Empty means no retry protection:
534+ // a lost assignment is then recovered by the acknowledgement timeout alone.
535+ PollId string `protobuf:"bytes,2,opt,name=poll_id,json=pollId,proto3" json:"poll_id,omitempty"`
514536 unknownFields protoimpl.UnknownFields
515537 sizeCache protoimpl.SizeCache
516538 }
@@ -552,12 +574,24 @@ func (x *FetchJobRequest) GetLabels() []string {
552574 return nil
553575 }
554576
577+func (x *FetchJobRequest) GetPollId() string {
578+ if x != nil {
579+ return x.PollId
580+ }
581+ return ""
582+}
583+
555584 // FetchJobResponse holds the job given to the runner, if any.
556585 type FetchJobResponse struct {
557586 state protoimpl.MessageState `protogen:"open.v1"`
558587 // Job is the job to run. Unset when no job was available before the end of
559588 // the poll.
560- Job *Job `protobuf:"bytes,1,opt,name=job,proto3" json:"job,omitempty"`
589+ Job *Job `protobuf:"bytes,1,opt,name=job,proto3" json:"job,omitempty"`
590+ // RetryAfter is how long the client waits before its next FetchJob. Unset or
591+ // zero means call again at once. The server sets it when it has no job for
592+ // the runner and wants to slow polling down (load, shutdown in progress);
593+ // clients obey it and may wait longer, never less. Unset when job is set.
594+ RetryAfter *durationpb.Duration `protobuf:"bytes,2,opt,name=retry_after,json=retryAfter,proto3" json:"retry_after,omitempty"`
561595 unknownFields protoimpl.UnknownFields
562596 sizeCache protoimpl.SizeCache
563597 }
@@ -599,6 +633,13 @@ func (x *FetchJobResponse) GetJob() *Job {
599633 return nil
600634 }
601635
636+func (x *FetchJobResponse) GetRetryAfter() *durationpb.Duration {
637+ if x != nil {
638+ return x.RetryAfter
639+ }
640+ return nil
641+}
642+
602643 // Job is a job given to a runner: everything needed to run it, nothing more.
603644 // The runner parses workflow_yaml itself; the protocol does not carry the
604645 // workflow model, so it does not change when the model grows.
@@ -615,10 +656,13 @@ type Job struct {
615656 // one more after each RunService.RerunRun. It is the "github.run_attempt".
616657 Attempt int32 `protobuf:"varint,3,opt,name=attempt,proto3" json:"attempt,omitempty"`
617658 // JobToken is the secret that authenticates this job's UpdateJob,
618- // AppendLog, Heartbeat and WatchJob calls ("Authorization: Bearer
619- // <job_token>"). It is random, valid until the job ends, survives a server
620- // restart and is shown only in this response. It must stay in the runner
621- // host process and never reach the job's code.
659+ // AppendLog, Heartbeat, WatchJob and CompleteJob calls ("Authorization:
660+ // Bearer <job_token>"): "ermitejob_" and 43 base64url characters (32 random
661+ // bytes, no padding). It is random, valid from the assignment until the job
662+ // ends (and for CompleteJob 24 hours more), survives a server restart and
663+ // is shown only in this response, or in a repeat of the same poll_id. It
664+ // must stay in the runner host process and never reach the job's code. A new
665+ // assignment of the same job has a new token.
622666 JobToken string `protobuf:"bytes,4,opt,name=job_token,json=jobToken,proto3" json:"job_token,omitempty"`
623667 // WorkflowPath is the path of the workflow file in the repository, for
624668 // example ".github/workflows/ci.yml".
@@ -643,8 +687,11 @@ type Job struct {
643687 Labels []string `protobuf:"bytes,10,rep,name=labels,proto3" json:"labels,omitempty"`
644688 // TimeoutMinutes is the timeout of the job in minutes: the value of the
645689 // workflow's timeout-minutes, or 360 when it has none. At least 1. The
646- // runner enforces it from the start of the job; the server ends a job that
647- // exceeds it by announcing CANCEL_REASON_TIMEOUT.
690+ // runner enforces it from the moment it receives the job. The server counts
691+ // from the moment of the assignment and adds 60 seconds of slack, so the
692+ // runner's own timeout always comes first: when the server's deadline
693+ // passes it announces CANCEL_REASON_TIMEOUT, and ends the job itself after
694+ // 60 more seconds without a conclusion (see WatchJob).
648695 TimeoutMinutes int32 `protobuf:"varint,11,opt,name=timeout_minutes,json=timeoutMinutes,proto3" json:"timeout_minutes,omitempty"`
649696 // Repo is the name of the repository.
650697 Repo string `protobuf:"bytes,12,opt,name=repo,proto3" json:"repo,omitempty"`
@@ -786,7 +833,7 @@ func (x *Job) GetRef() string {
786833 return ""
787834 }
788835
789-// UpdateJobRequest reports the steps and, in the last call, the end of a job.
836+// UpdateJobRequest reports the steps of a running job.
790837 type UpdateJobRequest struct {
791838 state protoimpl.MessageState `protogen:"open.v1"`
792839 // JobId is the Job.job_id. It must be the job of the token.
@@ -796,31 +843,9 @@ type UpdateJobRequest struct {
796843 // (invalid_argument beyond). Steps are numbered like GitHub's log: the
797844 // implicit "Set up job" is 1, the steps of the workflow follow in order, and
798845 // the implicit "Complete job" is last.
799- Steps []*StepUpdate `protobuf:"bytes,2,rep,name=steps,proto3" json:"steps,omitempty"`
800- // Outputs are the outputs of the job, evaluated by the runner. Only allowed
801- // in the call that carries the conclusion (invalid_argument otherwise).
802- // Names are 1 to 255 characters among letters, digits, "_" and "-" and
803- // begin with a letter or "_" and are case-insensitive: two names that
804- // differ only by case are invalid_argument. Each value is at most 1 MiB, and
805- // all names and values together at most 1 MiB (1,048,576 bytes); beyond that
806- // the whole call is rejected with invalid_argument and the runner reports
807- // the failure of the job without outputs, with a line in the log.
808- Outputs map[string]string `protobuf:"bytes,3,rep,name=outputs,proto3" json:"outputs,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
809- // Conclusion ends the job when set. A runner reports CI_CONCLUSION_SUCCESS,
810- // CI_CONCLUSION_FAILURE, CI_CONCLUSION_CANCELLED or CI_CONCLUSION_TIMED_OUT; any
811- // other value is invalid_argument. CI_CONCLUSION_UNSPECIFIED means the job goes
812- // on. Steps that were not reported COMPLETED when the job ends are
813- // completed by the server as CI_CONCLUSION_CANCELLED (or SKIPPED when they
814- // never started and the job did not fail by cancellation).
815- Conclusion CiConclusion `protobuf:"varint,4,opt,name=conclusion,proto3,enum=ermite.CiConclusion" json:"conclusion,omitempty"`
816- // RunnerShutdown is true when the runner ends the job because it was told to
817- // stop (service stopped, signal). Only allowed with CI_CONCLUSION_CANCELLED.
818- // The server then annotates the job with GitHub's message "The runner has
819- // received a shutdown signal. This can happen when the runner service is
820- // stopped, or a manually started runner is canceled."
821- RunnerShutdown bool `protobuf:"varint,5,opt,name=runner_shutdown,json=runnerShutdown,proto3" json:"runner_shutdown,omitempty"`
822- unknownFields protoimpl.UnknownFields
823- sizeCache protoimpl.SizeCache
846+ Steps []*StepUpdate `protobuf:"bytes,2,rep,name=steps,proto3" json:"steps,omitempty"`
847+ unknownFields protoimpl.UnknownFields
848+ sizeCache protoimpl.SizeCache
824849 }
825850
826851 func (x *UpdateJobRequest) Reset() {
@@ -867,27 +892,6 @@ func (x *UpdateJobRequest) GetSteps() []*StepUpdate {
867892 return nil
868893 }
869894
870-func (x *UpdateJobRequest) GetOutputs() map[string]string {
871- if x != nil {
872- return x.Outputs
873- }
874- return nil
875-}
876-
877-func (x *UpdateJobRequest) GetConclusion() CiConclusion {
878- if x != nil {
879- return x.Conclusion
880- }
881- return CiConclusion_CI_CONCLUSION_UNSPECIFIED
882-}
883-
884-func (x *UpdateJobRequest) GetRunnerShutdown() bool {
885- if x != nil {
886- return x.RunnerShutdown
887- }
888- return false
889-}
890-
891895 // StepUpdate is the state of one step as the runner knows it.
892896 type StepUpdate struct {
893897 state protoimpl.MessageState `protogen:"open.v1"`
@@ -1023,14 +1027,237 @@ func (*UpdateJobResponse) Descriptor() ([]byte, []int) {
10231027 return file_ermite_runner_proto_rawDescGZIP(), []int{7}
10241028 }
10251029
1030+// CompleteJobRequest ends a job.
1031+type CompleteJobRequest struct {
1032+ state protoimpl.MessageState `protogen:"open.v1"`
1033+ // JobId is the Job.job_id. It must be the job of the token.
1034+ JobId string `protobuf:"bytes,1,opt,name=job_id,json=jobId,proto3" json:"job_id,omitempty"`
1035+ // Conclusion is how the job ended. Required: a runner reports
1036+ // CI_CONCLUSION_SUCCESS, CI_CONCLUSION_FAILURE, CI_CONCLUSION_CANCELLED or
1037+ // CI_CONCLUSION_TIMED_OUT; any other value is invalid_argument. Skipped is
1038+ // decided by the server and never reported by a runner.
1039+ Conclusion CiConclusion `protobuf:"varint,2,opt,name=conclusion,proto3,enum=ermite.CiConclusion" json:"conclusion,omitempty"`
1040+ // Outputs are the outputs of the job, evaluated by the runner. Names are 1
1041+ // to 255 characters among letters, digits, "_" and "-" and begin with a
1042+ // letter or "_" and are case-insensitive: two names that differ only by case
1043+ // are invalid_argument. Each value is at most 1 MiB, and all names and
1044+ // values together at most 1 MiB (1,048,576 bytes); beyond that the whole call
1045+ // is rejected with invalid_argument and the runner reports the failure of
1046+ // the job without outputs, with a line in the log.
1047+ Outputs map[string]string `protobuf:"bytes,3,rep,name=outputs,proto3" json:"outputs,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
1048+ // RunnerShutdown is true when the runner ends the job because it was told to
1049+ // stop (service stopped, signal). Only allowed with CI_CONCLUSION_CANCELLED.
1050+ // The server then annotates the job with GitHub's message "The runner has
1051+ // received a shutdown signal. This can happen when the runner service is
1052+ // stopped, or a manually started runner is canceled." The conclusion
1053+ // CANCELLED for this case is to be verified against a recording of GitHub.
1054+ RunnerShutdown bool `protobuf:"varint,4,opt,name=runner_shutdown,json=runnerShutdown,proto3" json:"runner_shutdown,omitempty"`
1055+ // Steps are the final steps, with the same meaning as UpdateJobRequest.steps,
1056+ // applied in the same atomic step as the conclusion.
1057+ Steps []*StepUpdate `protobuf:"bytes,5,rep,name=steps,proto3" json:"steps,omitempty"`
1058+ unknownFields protoimpl.UnknownFields
1059+ sizeCache protoimpl.SizeCache
1060+}
1061+
1062+func (x *CompleteJobRequest) Reset() {
1063+ *x = CompleteJobRequest{}
1064+ mi := &file_ermite_runner_proto_msgTypes[8]
1065+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1066+ ms.StoreMessageInfo(mi)
1067+}
1068+
1069+func (x *CompleteJobRequest) String() string {
1070+ return protoimpl.X.MessageStringOf(x)
1071+}
1072+
1073+func (*CompleteJobRequest) ProtoMessage() {}
1074+
1075+func (x *CompleteJobRequest) ProtoReflect() protoreflect.Message {
1076+ mi := &file_ermite_runner_proto_msgTypes[8]
1077+ if x != nil {
1078+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1079+ if ms.LoadMessageInfo() == nil {
1080+ ms.StoreMessageInfo(mi)
1081+ }
1082+ return ms
1083+ }
1084+ return mi.MessageOf(x)
1085+}
1086+
1087+// Deprecated: Use CompleteJobRequest.ProtoReflect.Descriptor instead.
1088+func (*CompleteJobRequest) Descriptor() ([]byte, []int) {
1089+ return file_ermite_runner_proto_rawDescGZIP(), []int{8}
1090+}
1091+
1092+func (x *CompleteJobRequest) GetJobId() string {
1093+ if x != nil {
1094+ return x.JobId
1095+ }
1096+ return ""
1097+}
1098+
1099+func (x *CompleteJobRequest) GetConclusion() CiConclusion {
1100+ if x != nil {
1101+ return x.Conclusion
1102+ }
1103+ return CiConclusion_CI_CONCLUSION_UNSPECIFIED
1104+}
1105+
1106+func (x *CompleteJobRequest) GetOutputs() map[string]string {
1107+ if x != nil {
1108+ return x.Outputs
1109+ }
1110+ return nil
1111+}
1112+
1113+func (x *CompleteJobRequest) GetRunnerShutdown() bool {
1114+ if x != nil {
1115+ return x.RunnerShutdown
1116+ }
1117+ return false
1118+}
1119+
1120+func (x *CompleteJobRequest) GetSteps() []*StepUpdate {
1121+ if x != nil {
1122+ return x.Steps
1123+ }
1124+ return nil
1125+}
1126+
1127+// CompleteJobResponse is the stored result of a job.
1128+type CompleteJobResponse struct {
1129+ state protoimpl.MessageState `protogen:"open.v1"`
1130+ // Conclusion is the conclusion of the job as the server stored it. It is the
1131+ // one of the request when applied is true.
1132+ Conclusion CiConclusion `protobuf:"varint,1,opt,name=conclusion,proto3,enum=ermite.CiConclusion" json:"conclusion,omitempty"`
1133+ // Error is the error text stored with the job, as RunJob.error. Empty when
1134+ // there is none.
1135+ Error string `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"`
1136+ // Applied is true when this call ended the job, false when the job had
1137+ // already ended (a retry, or the server ended it first) and nothing changed.
1138+ Applied bool `protobuf:"varint,3,opt,name=applied,proto3" json:"applied,omitempty"`
1139+ unknownFields protoimpl.UnknownFields
1140+ sizeCache protoimpl.SizeCache
1141+}
1142+
1143+func (x *CompleteJobResponse) Reset() {
1144+ *x = CompleteJobResponse{}
1145+ mi := &file_ermite_runner_proto_msgTypes[9]
1146+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1147+ ms.StoreMessageInfo(mi)
1148+}
1149+
1150+func (x *CompleteJobResponse) String() string {
1151+ return protoimpl.X.MessageStringOf(x)
1152+}
1153+
1154+func (*CompleteJobResponse) ProtoMessage() {}
1155+
1156+func (x *CompleteJobResponse) ProtoReflect() protoreflect.Message {
1157+ mi := &file_ermite_runner_proto_msgTypes[9]
1158+ if x != nil {
1159+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1160+ if ms.LoadMessageInfo() == nil {
1161+ ms.StoreMessageInfo(mi)
1162+ }
1163+ return ms
1164+ }
1165+ return mi.MessageOf(x)
1166+}
1167+
1168+// Deprecated: Use CompleteJobResponse.ProtoReflect.Descriptor instead.
1169+func (*CompleteJobResponse) Descriptor() ([]byte, []int) {
1170+ return file_ermite_runner_proto_rawDescGZIP(), []int{9}
1171+}
1172+
1173+func (x *CompleteJobResponse) GetConclusion() CiConclusion {
1174+ if x != nil {
1175+ return x.Conclusion
1176+ }
1177+ return CiConclusion_CI_CONCLUSION_UNSPECIFIED
1178+}
1179+
1180+func (x *CompleteJobResponse) GetError() string {
1181+ if x != nil {
1182+ return x.Error
1183+ }
1184+ return ""
1185+}
1186+
1187+func (x *CompleteJobResponse) GetApplied() bool {
1188+ if x != nil {
1189+ return x.Applied
1190+ }
1191+ return false
1192+}
1193+
1194+// JobEnded says that a job has ended. It is sent in WatchJob and attached, as
1195+// an error detail, to the failed_precondition errors that RunnerService returns
1196+// for a job that has ended.
1197+type JobEnded struct {
1198+ state protoimpl.MessageState `protogen:"open.v1"`
1199+ // Conclusion is how the job ended.
1200+ Conclusion CiConclusion `protobuf:"varint,1,opt,name=conclusion,proto3,enum=ermite.CiConclusion" json:"conclusion,omitempty"`
1201+ // Error is the error text stored with the job, as RunJob.error. Empty when
1202+ // there is none.
1203+ Error string `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"`
1204+ unknownFields protoimpl.UnknownFields
1205+ sizeCache protoimpl.SizeCache
1206+}
1207+
1208+func (x *JobEnded) Reset() {
1209+ *x = JobEnded{}
1210+ mi := &file_ermite_runner_proto_msgTypes[10]
1211+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1212+ ms.StoreMessageInfo(mi)
1213+}
1214+
1215+func (x *JobEnded) String() string {
1216+ return protoimpl.X.MessageStringOf(x)
1217+}
1218+
1219+func (*JobEnded) ProtoMessage() {}
1220+
1221+func (x *JobEnded) ProtoReflect() protoreflect.Message {
1222+ mi := &file_ermite_runner_proto_msgTypes[10]
1223+ if x != nil {
1224+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1225+ if ms.LoadMessageInfo() == nil {
1226+ ms.StoreMessageInfo(mi)
1227+ }
1228+ return ms
1229+ }
1230+ return mi.MessageOf(x)
1231+}
1232+
1233+// Deprecated: Use JobEnded.ProtoReflect.Descriptor instead.
1234+func (*JobEnded) Descriptor() ([]byte, []int) {
1235+ return file_ermite_runner_proto_rawDescGZIP(), []int{10}
1236+}
1237+
1238+func (x *JobEnded) GetConclusion() CiConclusion {
1239+ if x != nil {
1240+ return x.Conclusion
1241+ }
1242+ return CiConclusion_CI_CONCLUSION_UNSPECIFIED
1243+}
1244+
1245+func (x *JobEnded) GetError() string {
1246+ if x != nil {
1247+ return x.Error
1248+ }
1249+ return ""
1250+}
1251+
10261252 // AppendLogRequest carries log lines of one step.
10271253 type AppendLogRequest struct {
10281254 state protoimpl.MessageState `protogen:"open.v1"`
10291255 // JobId is the Job.job_id. It must be the job of the token.
10301256 JobId string `protobuf:"bytes,1,opt,name=job_id,json=jobId,proto3" json:"job_id,omitempty"`
10311257 // Step is the number of the step the lines belong to (see
1032- // UpdateJobRequest.steps), at least 1. A step that UpdateJob has not
1033- // announced yet is created by the first lines.
1258+ // UpdateJobRequest.steps), at least 1, and at least the step of the last
1259+ // stored line. A step that UpdateJob has not announced yet is created by the
1260+ // first lines. See the rules on AppendLog.
10341261 Step int32 `protobuf:"varint,2,opt,name=step,proto3" json:"step,omitempty"`
10351262 // Offset is the number of the first line of the request in the log of the
10361263 // job, counted from 0 across all steps. See the rules on AppendLog.
@@ -1044,7 +1271,7 @@ type AppendLogRequest struct {
10441271
10451272 func (x *AppendLogRequest) Reset() {
10461273 *x = AppendLogRequest{}
1047- mi := &file_ermite_runner_proto_msgTypes[8]
1274+ mi := &file_ermite_runner_proto_msgTypes[11]
10481275 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
10491276 ms.StoreMessageInfo(mi)
10501277 }
@@ -1056,7 +1283,7 @@ func (x *AppendLogRequest) String() string {
10561283 func (*AppendLogRequest) ProtoMessage() {}
10571284
10581285 func (x *AppendLogRequest) ProtoReflect() protoreflect.Message {
1059- mi := &file_ermite_runner_proto_msgTypes[8]
1286+ mi := &file_ermite_runner_proto_msgTypes[11]
10601287 if x != nil {
10611288 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
10621289 if ms.LoadMessageInfo() == nil {
@@ -1069,7 +1296,7 @@ func (x *AppendLogRequest) ProtoReflect() protoreflect.Message {
10691296
10701297 // Deprecated: Use AppendLogRequest.ProtoReflect.Descriptor instead.
10711298 func (*AppendLogRequest) Descriptor() ([]byte, []int) {
1072- return file_ermite_runner_proto_rawDescGZIP(), []int{8}
1299+ return file_ermite_runner_proto_rawDescGZIP(), []int{11}
10731300 }
10741301
10751302 func (x *AppendLogRequest) GetJobId() string {
@@ -1110,14 +1337,23 @@ type LogLine struct {
11101337 // replaces invalid bytes with U+FFFD). It must not contain a line feed; a
11111338 // carriage return is kept. It is the line as GitHub's raw log has it,
11121339 // including "##[group]" and "##[endgroup]" markers and "##[error]" lines.
1113- Text string `protobuf:"bytes,2,opt,name=text,proto3" json:"text,omitempty"`
1340+ Text string `protobuf:"bytes,2,opt,name=text,proto3" json:"text,omitempty"`
1341+ // Continued is true when this line is the continuation of the line before
1342+ // it, which was too long to send whole. A runner cuts a line of more than
1343+ // 64 KiB into pieces at UTF-8 character boundaries, each piece of at most
1344+ // 64 KiB, and sets continued on every piece but the first. Each piece is one
1345+ // line for numbering and offsets; the server and the raw log join the
1346+ // pieces back, so the original line is rebuilt exactly. The first line of a
1347+ // job cannot be continued (invalid_argument) and a continued line is in
1348+ // the same step as the line before it (failed_precondition otherwise).
1349+ Continued bool `protobuf:"varint,3,opt,name=continued,proto3" json:"continued,omitempty"`
11141350 unknownFields protoimpl.UnknownFields
11151351 sizeCache protoimpl.SizeCache
11161352 }
11171353
11181354 func (x *LogLine) Reset() {
11191355 *x = LogLine{}
1120- mi := &file_ermite_runner_proto_msgTypes[9]
1356+ mi := &file_ermite_runner_proto_msgTypes[12]
11211357 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
11221358 ms.StoreMessageInfo(mi)
11231359 }
@@ -1129,7 +1365,7 @@ func (x *LogLine) String() string {
11291365 func (*LogLine) ProtoMessage() {}
11301366
11311367 func (x *LogLine) ProtoReflect() protoreflect.Message {
1132- mi := &file_ermite_runner_proto_msgTypes[9]
1368+ mi := &file_ermite_runner_proto_msgTypes[12]
11331369 if x != nil {
11341370 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
11351371 if ms.LoadMessageInfo() == nil {
@@ -1142,7 +1378,7 @@ func (x *LogLine) ProtoReflect() protoreflect.Message {
11421378
11431379 // Deprecated: Use LogLine.ProtoReflect.Descriptor instead.
11441380 func (*LogLine) Descriptor() ([]byte, []int) {
1145- return file_ermite_runner_proto_rawDescGZIP(), []int{9}
1381+ return file_ermite_runner_proto_rawDescGZIP(), []int{12}
11461382 }
11471383
11481384 func (x *LogLine) GetTime() *timestamppb.Timestamp {
@@ -1159,6 +1395,13 @@ func (x *LogLine) GetText() string {
11591395 return ""
11601396 }
11611397
1398+func (x *LogLine) GetContinued() bool {
1399+ if x != nil {
1400+ return x.Continued
1401+ }
1402+ return false
1403+}
1404+
11621405 // AppendLogResponse tells how much of the log the server holds.
11631406 type AppendLogResponse struct {
11641407 state protoimpl.MessageState `protogen:"open.v1"`
@@ -1171,7 +1414,7 @@ type AppendLogResponse struct {
11711414
11721415 func (x *AppendLogResponse) Reset() {
11731416 *x = AppendLogResponse{}
1174- mi := &file_ermite_runner_proto_msgTypes[10]
1417+ mi := &file_ermite_runner_proto_msgTypes[13]
11751418 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
11761419 ms.StoreMessageInfo(mi)
11771420 }
@@ -1183,7 +1426,7 @@ func (x *AppendLogResponse) String() string {
11831426 func (*AppendLogResponse) ProtoMessage() {}
11841427
11851428 func (x *AppendLogResponse) ProtoReflect() protoreflect.Message {
1186- mi := &file_ermite_runner_proto_msgTypes[10]
1429+ mi := &file_ermite_runner_proto_msgTypes[13]
11871430 if x != nil {
11881431 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
11891432 if ms.LoadMessageInfo() == nil {
@@ -1196,7 +1439,7 @@ func (x *AppendLogResponse) ProtoReflect() protoreflect.Message {
11961439
11971440 // Deprecated: Use AppendLogResponse.ProtoReflect.Descriptor instead.
11981441 func (*AppendLogResponse) Descriptor() ([]byte, []int) {
1199- return file_ermite_runner_proto_rawDescGZIP(), []int{10}
1442+ return file_ermite_runner_proto_rawDescGZIP(), []int{13}
12001443 }
12011444
12021445 func (x *AppendLogResponse) GetNextOffset() int64 {
@@ -1217,7 +1460,7 @@ type HeartbeatRequest struct {
12171460
12181461 func (x *HeartbeatRequest) Reset() {
12191462 *x = HeartbeatRequest{}
1220- mi := &file_ermite_runner_proto_msgTypes[11]
1463+ mi := &file_ermite_runner_proto_msgTypes[14]
12211464 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
12221465 ms.StoreMessageInfo(mi)
12231466 }
@@ -1229,7 +1472,7 @@ func (x *HeartbeatRequest) String() string {
12291472 func (*HeartbeatRequest) ProtoMessage() {}
12301473
12311474 func (x *HeartbeatRequest) ProtoReflect() protoreflect.Message {
1232- mi := &file_ermite_runner_proto_msgTypes[11]
1475+ mi := &file_ermite_runner_proto_msgTypes[14]
12331476 if x != nil {
12341477 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
12351478 if ms.LoadMessageInfo() == nil {
@@ -1242,7 +1485,7 @@ func (x *HeartbeatRequest) ProtoReflect() protoreflect.Message {
12421485
12431486 // Deprecated: Use HeartbeatRequest.ProtoReflect.Descriptor instead.
12441487 func (*HeartbeatRequest) Descriptor() ([]byte, []int) {
1245- return file_ermite_runner_proto_rawDescGZIP(), []int{11}
1488+ return file_ermite_runner_proto_rawDescGZIP(), []int{14}
12461489 }
12471490
12481491 func (x *HeartbeatRequest) GetJobId() string {
@@ -1261,7 +1504,7 @@ type HeartbeatResponse struct {
12611504
12621505 func (x *HeartbeatResponse) Reset() {
12631506 *x = HeartbeatResponse{}
1264- mi := &file_ermite_runner_proto_msgTypes[12]
1507+ mi := &file_ermite_runner_proto_msgTypes[15]
12651508 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
12661509 ms.StoreMessageInfo(mi)
12671510 }
@@ -1273,7 +1516,7 @@ func (x *HeartbeatResponse) String() string {
12731516 func (*HeartbeatResponse) ProtoMessage() {}
12741517
12751518 func (x *HeartbeatResponse) ProtoReflect() protoreflect.Message {
1276- mi := &file_ermite_runner_proto_msgTypes[12]
1519+ mi := &file_ermite_runner_proto_msgTypes[15]
12771520 if x != nil {
12781521 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
12791522 if ms.LoadMessageInfo() == nil {
@@ -1286,7 +1529,7 @@ func (x *HeartbeatResponse) ProtoReflect() protoreflect.Message {
12861529
12871530 // Deprecated: Use HeartbeatResponse.ProtoReflect.Descriptor instead.
12881531 func (*HeartbeatResponse) Descriptor() ([]byte, []int) {
1289- return file_ermite_runner_proto_rawDescGZIP(), []int{12}
1532+ return file_ermite_runner_proto_rawDescGZIP(), []int{15}
12901533 }
12911534
12921535 // WatchJobRequest names the job to watch.
@@ -1300,7 +1543,7 @@ type WatchJobRequest struct {
13001543
13011544 func (x *WatchJobRequest) Reset() {
13021545 *x = WatchJobRequest{}
1303- mi := &file_ermite_runner_proto_msgTypes[13]
1546+ mi := &file_ermite_runner_proto_msgTypes[16]
13041547 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
13051548 ms.StoreMessageInfo(mi)
13061549 }
@@ -1312,7 +1555,7 @@ func (x *WatchJobRequest) String() string {
13121555 func (*WatchJobRequest) ProtoMessage() {}
13131556
13141557 func (x *WatchJobRequest) ProtoReflect() protoreflect.Message {
1315- mi := &file_ermite_runner_proto_msgTypes[13]
1558+ mi := &file_ermite_runner_proto_msgTypes[16]
13161559 if x != nil {
13171560 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
13181561 if ms.LoadMessageInfo() == nil {
@@ -1325,7 +1568,7 @@ func (x *WatchJobRequest) ProtoReflect() protoreflect.Message {
13251568
13261569 // Deprecated: Use WatchJobRequest.ProtoReflect.Descriptor instead.
13271570 func (*WatchJobRequest) Descriptor() ([]byte, []int) {
1328- return file_ermite_runner_proto_rawDescGZIP(), []int{13}
1571+ return file_ermite_runner_proto_rawDescGZIP(), []int{16}
13291572 }
13301573
13311574 func (x *WatchJobRequest) GetJobId() string {
@@ -1335,28 +1578,25 @@ func (x *WatchJobRequest) GetJobId() string {
13351578 return ""
13361579 }
13371580
1338-// WatchJobResponse is one message of the stream: a request to cancel the job,
1339-// or a keep-alive.
1581+// WatchJobResponse is one message of the stream.
13401582 type WatchJobResponse struct {
13411583 state protoimpl.MessageState `protogen:"open.v1"`
1342- // Reason is why the job must stop. CANCEL_REASON_UNSPECIFIED marks a
1343- // keep-alive: the other fields are empty and the client ignores it. New
1344- // reasons may be added; a runner that does not know one cancels the job and
1345- // reports CI_CONCLUSION_CANCELLED.
1346- Reason CancelReason `protobuf:"varint,1,opt,name=reason,proto3,enum=ermite.CancelReason" json:"reason,omitempty"`
1347- // Message is a short human-readable explanation, for the job log, for
1348- // example "Canceled by the owner". At most 1,024 characters.
1349- Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"`
1350- // RequestedAt is when the cancellation was requested, by the clock of the
1351- // server.
1352- RequestedAt *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=requested_at,json=requestedAt,proto3" json:"requested_at,omitempty"`
1584+ // Msg is what the message carries. New kinds may be added: a client ignores
1585+ // the ones it does not know.
1586+ //
1587+ // Types that are valid to be assigned to Msg:
1588+ //
1589+ // *WatchJobResponse_Keepalive
1590+ // *WatchJobResponse_Cancel
1591+ // *WatchJobResponse_Ended
1592+ Msg isWatchJobResponse_Msg `protobuf_oneof:"msg"`
13531593 unknownFields protoimpl.UnknownFields
13541594 sizeCache protoimpl.SizeCache
13551595 }
13561596
13571597 func (x *WatchJobResponse) Reset() {
13581598 *x = WatchJobResponse{}
1359- mi := &file_ermite_runner_proto_msgTypes[14]
1599+ mi := &file_ermite_runner_proto_msgTypes[17]
13601600 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
13611601 ms.StoreMessageInfo(mi)
13621602 }
@@ -1368,7 +1608,7 @@ func (x *WatchJobResponse) String() string {
13681608 func (*WatchJobResponse) ProtoMessage() {}
13691609
13701610 func (x *WatchJobResponse) ProtoReflect() protoreflect.Message {
1371- mi := &file_ermite_runner_proto_msgTypes[14]
1611+ mi := &file_ermite_runner_proto_msgTypes[17]
13721612 if x != nil {
13731613 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
13741614 if ms.LoadMessageInfo() == nil {
@@ -1381,24 +1621,167 @@ func (x *WatchJobResponse) ProtoReflect() protoreflect.Message {
13811621
13821622 // Deprecated: Use WatchJobResponse.ProtoReflect.Descriptor instead.
13831623 func (*WatchJobResponse) Descriptor() ([]byte, []int) {
1384- return file_ermite_runner_proto_rawDescGZIP(), []int{14}
1624+ return file_ermite_runner_proto_rawDescGZIP(), []int{17}
13851625 }
13861626
1387-func (x *WatchJobResponse) GetReason() CancelReason {
1627+func (x *WatchJobResponse) GetMsg() isWatchJobResponse_Msg {
1628+ if x != nil {
1629+ return x.Msg
1630+ }
1631+ return nil
1632+}
1633+
1634+func (x *WatchJobResponse) GetKeepalive() *Keepalive {
1635+ if x != nil {
1636+ if x, ok := x.Msg.(*WatchJobResponse_Keepalive); ok {
1637+ return x.Keepalive
1638+ }
1639+ }
1640+ return nil
1641+}
1642+
1643+func (x *WatchJobResponse) GetCancel() *CancelRequest {
1644+ if x != nil {
1645+ if x, ok := x.Msg.(*WatchJobResponse_Cancel); ok {
1646+ return x.Cancel
1647+ }
1648+ }
1649+ return nil
1650+}
1651+
1652+func (x *WatchJobResponse) GetEnded() *JobEnded {
1653+ if x != nil {
1654+ if x, ok := x.Msg.(*WatchJobResponse_Ended); ok {
1655+ return x.Ended
1656+ }
1657+ }
1658+ return nil
1659+}
1660+
1661+type isWatchJobResponse_Msg interface {
1662+ isWatchJobResponse_Msg()
1663+}
1664+
1665+type WatchJobResponse_Keepalive struct {
1666+ // Keepalive: nothing to do, the stream is alive.
1667+ Keepalive *Keepalive `protobuf:"bytes,1,opt,name=keepalive,proto3,oneof"`
1668+}
1669+
1670+type WatchJobResponse_Cancel struct {
1671+ // Cancel: the job must stop.
1672+ Cancel *CancelRequest `protobuf:"bytes,2,opt,name=cancel,proto3,oneof"`
1673+}
1674+
1675+type WatchJobResponse_Ended struct {
1676+ // Ended: the job has ended. The last message of a stream: a close without
1677+ // it means the client reconnects.
1678+ Ended *JobEnded `protobuf:"bytes,3,opt,name=ended,proto3,oneof"`
1679+}
1680+
1681+func (*WatchJobResponse_Keepalive) isWatchJobResponse_Msg() {}
1682+
1683+func (*WatchJobResponse_Cancel) isWatchJobResponse_Msg() {}
1684+
1685+func (*WatchJobResponse_Ended) isWatchJobResponse_Msg() {}
1686+
1687+// Keepalive has no fields.
1688+type Keepalive struct {
1689+ state protoimpl.MessageState `protogen:"open.v1"`
1690+ unknownFields protoimpl.UnknownFields
1691+ sizeCache protoimpl.SizeCache
1692+}
1693+
1694+func (x *Keepalive) Reset() {
1695+ *x = Keepalive{}
1696+ mi := &file_ermite_runner_proto_msgTypes[18]
1697+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1698+ ms.StoreMessageInfo(mi)
1699+}
1700+
1701+func (x *Keepalive) String() string {
1702+ return protoimpl.X.MessageStringOf(x)
1703+}
1704+
1705+func (*Keepalive) ProtoMessage() {}
1706+
1707+func (x *Keepalive) ProtoReflect() protoreflect.Message {
1708+ mi := &file_ermite_runner_proto_msgTypes[18]
1709+ if x != nil {
1710+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1711+ if ms.LoadMessageInfo() == nil {
1712+ ms.StoreMessageInfo(mi)
1713+ }
1714+ return ms
1715+ }
1716+ return mi.MessageOf(x)
1717+}
1718+
1719+// Deprecated: Use Keepalive.ProtoReflect.Descriptor instead.
1720+func (*Keepalive) Descriptor() ([]byte, []int) {
1721+ return file_ermite_runner_proto_rawDescGZIP(), []int{18}
1722+}
1723+
1724+// CancelRequest asks the runner to stop a job.
1725+type CancelRequest struct {
1726+ state protoimpl.MessageState `protogen:"open.v1"`
1727+ // Reason is why the job must stop. New reasons may be added; a runner that
1728+ // does not know one cancels the job and reports CI_CONCLUSION_CANCELLED.
1729+ Reason CancelReason `protobuf:"varint,1,opt,name=reason,proto3,enum=ermite.CancelReason" json:"reason,omitempty"`
1730+ // Message is a short human-readable explanation, for the job log, for
1731+ // example "Canceled by the owner". At most 1,024 characters.
1732+ Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"`
1733+ // RequestedAt is when the cancellation was requested, by the clock of the
1734+ // server.
1735+ RequestedAt *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=requested_at,json=requestedAt,proto3" json:"requested_at,omitempty"`
1736+ unknownFields protoimpl.UnknownFields
1737+ sizeCache protoimpl.SizeCache
1738+}
1739+
1740+func (x *CancelRequest) Reset() {
1741+ *x = CancelRequest{}
1742+ mi := &file_ermite_runner_proto_msgTypes[19]
1743+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1744+ ms.StoreMessageInfo(mi)
1745+}
1746+
1747+func (x *CancelRequest) String() string {
1748+ return protoimpl.X.MessageStringOf(x)
1749+}
1750+
1751+func (*CancelRequest) ProtoMessage() {}
1752+
1753+func (x *CancelRequest) ProtoReflect() protoreflect.Message {
1754+ mi := &file_ermite_runner_proto_msgTypes[19]
1755+ if x != nil {
1756+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1757+ if ms.LoadMessageInfo() == nil {
1758+ ms.StoreMessageInfo(mi)
1759+ }
1760+ return ms
1761+ }
1762+ return mi.MessageOf(x)
1763+}
1764+
1765+// Deprecated: Use CancelRequest.ProtoReflect.Descriptor instead.
1766+func (*CancelRequest) Descriptor() ([]byte, []int) {
1767+ return file_ermite_runner_proto_rawDescGZIP(), []int{19}
1768+}
1769+
1770+func (x *CancelRequest) GetReason() CancelReason {
13881771 if x != nil {
13891772 return x.Reason
13901773 }
13911774 return CancelReason_CANCEL_REASON_UNSPECIFIED
13921775 }
13931776
1394-func (x *WatchJobResponse) GetMessage() string {
1777+func (x *CancelRequest) GetMessage() string {
13951778 if x != nil {
13961779 return x.Message
13971780 }
13981781 return ""
13991782 }
14001783
1401-func (x *WatchJobResponse) GetRequestedAt() *timestamppb.Timestamp {
1784+func (x *CancelRequest) GetRequestedAt() *timestamppb.Timestamp {
14021785 if x != nil {
14031786 return x.RequestedAt
14041787 }
@@ -1409,21 +1792,25 @@ var File_ermite_runner_proto protoreflect.FileDescriptor
14091792
14101793 const file_ermite_runner_proto_rawDesc = "" +
14111794 "\n" +
1412- "\x13ermite/runner.proto\x12\x06ermite\x1a\x1fgoogle/protobuf/timestamp.proto\"\xb8\x01\n" +
1795+ "\x13ermite/runner.proto\x12\x06ermite\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xd2\x01\n" +
14131796 "\x15RegisterRunnerRequest\x12\x12\n" +
14141797 "\x04name\x18\x01 \x01(\tR\x04name\x12\x16\n" +
14151798 "\x06labels\x18\x02 \x03(\tR\x06labels\x12\x18\n" +
14161799 "\aversion\x18\x03 \x01(\tR\aversion\x125\n" +
14171800 "\tisolation\x18\x04 \x01(\x0e2\x17.ermite.RunnerIsolationR\tisolation\x12\x12\n" +
14181801 "\x04arch\x18\x05 \x01(\tR\x04arch\x12\x0e\n" +
1419- "\x02os\x18\x06 \x01(\tR\x02os\"X\n" +
1802+ "\x02os\x18\x06 \x01(\tR\x02os\x12\x18\n" +
1803+ "\areplace\x18\a \x01(\bR\areplace\"X\n" +
14201804 "\x16RegisterRunnerResponse\x12\x1b\n" +
14211805 "\trunner_id\x18\x01 \x01(\tR\brunnerId\x12!\n" +
1422- "\frunner_token\x18\x02 \x01(\tR\vrunnerToken\")\n" +
1806+ "\frunner_token\x18\x02 \x01(\tR\vrunnerToken\"B\n" +
14231807 "\x0fFetchJobRequest\x12\x16\n" +
1424- "\x06labels\x18\x01 \x03(\tR\x06labels\"1\n" +
1808+ "\x06labels\x18\x01 \x03(\tR\x06labels\x12\x17\n" +
1809+ "\apoll_id\x18\x02 \x01(\tR\x06pollId\"m\n" +
14251810 "\x10FetchJobResponse\x12\x1d\n" +
1426- "\x03job\x18\x01 \x01(\v2\v.ermite.JobR\x03job\"\xfa\x02\n" +
1811+ "\x03job\x18\x01 \x01(\v2\v.ermite.JobR\x03job\x12:\n" +
1812+ "\vretry_after\x18\x02 \x01(\v2\x19.google.protobuf.DurationR\n" +
1813+ "retryAfter\"\xfa\x02\n" +
14271814 "\x03Job\x12\x15\n" +
14281815 "\x06job_id\x18\x01 \x01(\tR\x05jobId\x12\x15\n" +
14291816 "\x06run_id\x18\x02 \x01(\x03R\x05runId\x12\x18\n" +
@@ -1439,18 +1826,10 @@ const file_ermite_runner_proto_rawDesc = "" +
14391826 "\x0ftimeout_minutes\x18\v \x01(\x05R\x0etimeoutMinutes\x12\x12\n" +
14401827 "\x04repo\x18\f \x01(\tR\x04repo\x12\x10\n" +
14411828 "\x03sha\x18\r \x01(\tR\x03sha\x12\x10\n" +
1442- "\x03ref\x18\x0e \x01(\tR\x03ref\"\xaf\x02\n" +
1829+ "\x03ref\x18\x0e \x01(\tR\x03ref\"S\n" +
14431830 "\x10UpdateJobRequest\x12\x15\n" +
14441831 "\x06job_id\x18\x01 \x01(\tR\x05jobId\x12(\n" +
1445- "\x05steps\x18\x02 \x03(\v2\x12.ermite.StepUpdateR\x05steps\x12?\n" +
1446- "\aoutputs\x18\x03 \x03(\v2%.ermite.UpdateJobRequest.OutputsEntryR\aoutputs\x124\n" +
1447- "\n" +
1448- "conclusion\x18\x04 \x01(\x0e2\x14.ermite.CiConclusionR\n" +
1449- "conclusion\x12'\n" +
1450- "\x0frunner_shutdown\x18\x05 \x01(\bR\x0erunnerShutdown\x1a:\n" +
1451- "\fOutputsEntry\x12\x10\n" +
1452- "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
1453- "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\x91\x02\n" +
1832+ "\x05steps\x18\x02 \x03(\v2\x12.ermite.StepUpdateR\x05steps\"\x91\x02\n" +
14541833 "\n" +
14551834 "StepUpdate\x12\x16\n" +
14561835 "\x06number\x18\x01 \x01(\x05R\x06number\x12\x12\n" +
@@ -1462,15 +1841,38 @@ const file_ermite_runner_proto_rawDesc = "" +
14621841 "\n" +
14631842 "started_at\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampR\tstartedAt\x12=\n" +
14641843 "\fcompleted_at\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampR\vcompletedAt\"\x13\n" +
1465- "\x11UpdateJobResponse\"|\n" +
1844+ "\x11UpdateJobResponse\"\xb3\x02\n" +
1845+ "\x12CompleteJobRequest\x12\x15\n" +
1846+ "\x06job_id\x18\x01 \x01(\tR\x05jobId\x124\n" +
1847+ "\n" +
1848+ "conclusion\x18\x02 \x01(\x0e2\x14.ermite.CiConclusionR\n" +
1849+ "conclusion\x12A\n" +
1850+ "\aoutputs\x18\x03 \x03(\v2'.ermite.CompleteJobRequest.OutputsEntryR\aoutputs\x12'\n" +
1851+ "\x0frunner_shutdown\x18\x04 \x01(\bR\x0erunnerShutdown\x12(\n" +
1852+ "\x05steps\x18\x05 \x03(\v2\x12.ermite.StepUpdateR\x05steps\x1a:\n" +
1853+ "\fOutputsEntry\x12\x10\n" +
1854+ "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
1855+ "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"{\n" +
1856+ "\x13CompleteJobResponse\x124\n" +
1857+ "\n" +
1858+ "conclusion\x18\x01 \x01(\x0e2\x14.ermite.CiConclusionR\n" +
1859+ "conclusion\x12\x14\n" +
1860+ "\x05error\x18\x02 \x01(\tR\x05error\x12\x18\n" +
1861+ "\aapplied\x18\x03 \x01(\bR\aapplied\"V\n" +
1862+ "\bJobEnded\x124\n" +
1863+ "\n" +
1864+ "conclusion\x18\x01 \x01(\x0e2\x14.ermite.CiConclusionR\n" +
1865+ "conclusion\x12\x14\n" +
1866+ "\x05error\x18\x02 \x01(\tR\x05error\"|\n" +
14661867 "\x10AppendLogRequest\x12\x15\n" +
14671868 "\x06job_id\x18\x01 \x01(\tR\x05jobId\x12\x12\n" +
14681869 "\x04step\x18\x02 \x01(\x05R\x04step\x12\x16\n" +
14691870 "\x06offset\x18\x03 \x01(\x03R\x06offset\x12%\n" +
1470- "\x05lines\x18\x04 \x03(\v2\x0f.ermite.LogLineR\x05lines\"M\n" +
1871+ "\x05lines\x18\x04 \x03(\v2\x0f.ermite.LogLineR\x05lines\"k\n" +
14711872 "\aLogLine\x12.\n" +
14721873 "\x04time\x18\x01 \x01(\v2\x1a.google.protobuf.TimestampR\x04time\x12\x12\n" +
1473- "\x04text\x18\x02 \x01(\tR\x04text\"4\n" +
1874+ "\x04text\x18\x02 \x01(\tR\x04text\x12\x1c\n" +
1875+ "\tcontinued\x18\x03 \x01(\bR\tcontinued\"4\n" +
14741876 "\x11AppendLogResponse\x12\x1f\n" +
14751877 "\vnext_offset\x18\x01 \x01(\x03R\n" +
14761878 "nextOffset\")\n" +
@@ -1478,8 +1880,14 @@ const file_ermite_runner_proto_rawDesc = "" +
14781880 "\x06job_id\x18\x01 \x01(\tR\x05jobId\"\x13\n" +
14791881 "\x11HeartbeatResponse\"(\n" +
14801882 "\x0fWatchJobRequest\x12\x15\n" +
1481- "\x06job_id\x18\x01 \x01(\tR\x05jobId\"\x99\x01\n" +
1482- "\x10WatchJobResponse\x12,\n" +
1883+ "\x06job_id\x18\x01 \x01(\tR\x05jobId\"\xa7\x01\n" +
1884+ "\x10WatchJobResponse\x121\n" +
1885+ "\tkeepalive\x18\x01 \x01(\v2\x11.ermite.KeepaliveH\x00R\tkeepalive\x12/\n" +
1886+ "\x06cancel\x18\x02 \x01(\v2\x15.ermite.CancelRequestH\x00R\x06cancel\x12(\n" +
1887+ "\x05ended\x18\x03 \x01(\v2\x10.ermite.JobEndedH\x00R\x05endedB\x05\n" +
1888+ "\x03msg\"\v\n" +
1889+ "\tKeepalive\"\x96\x01\n" +
1890+ "\rCancelRequest\x12,\n" +
14831891 "\x06reason\x18\x01 \x01(\x0e2\x14.ermite.CancelReasonR\x06reason\x12\x18\n" +
14841892 "\amessage\x18\x02 \x01(\tR\amessage\x12=\n" +
14851893 "\frequested_at\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\vrequestedAt*o\n" +
@@ -1511,14 +1919,15 @@ const file_ermite_runner_proto_rawDesc = "" +
15111919 "\x19CANCEL_REASON_UNSPECIFIED\x10\x00\x12\x16\n" +
15121920 "\x12CANCEL_REASON_USER\x10\x01\x12\x1b\n" +
15131921 "\x17CANCEL_REASON_FAIL_FAST\x10\x02\x12\x19\n" +
1514- "\x15CANCEL_REASON_TIMEOUT\x10\x032\xa6\x03\n" +
1922+ "\x15CANCEL_REASON_TIMEOUT\x10\x032\xee\x03\n" +
15151923 "\rRunnerService\x12O\n" +
15161924 "\x0eRegisterRunner\x12\x1d.ermite.RegisterRunnerRequest\x1a\x1e.ermite.RegisterRunnerResponse\x12=\n" +
15171925 "\bFetchJob\x12\x17.ermite.FetchJobRequest\x1a\x18.ermite.FetchJobResponse\x12@\n" +
15181926 "\tUpdateJob\x12\x18.ermite.UpdateJobRequest\x1a\x19.ermite.UpdateJobResponse\x12@\n" +
15191927 "\tAppendLog\x12\x18.ermite.AppendLogRequest\x1a\x19.ermite.AppendLogResponse\x12@\n" +
15201928 "\tHeartbeat\x12\x18.ermite.HeartbeatRequest\x1a\x19.ermite.HeartbeatResponse\x12?\n" +
1521- "\bWatchJob\x12\x17.ermite.WatchJobRequest\x1a\x18.ermite.WatchJobResponse0\x01Br\n" +
1929+ "\bWatchJob\x12\x17.ermite.WatchJobRequest\x1a\x18.ermite.WatchJobResponse0\x01\x12F\n" +
1930+ "\vCompleteJob\x12\x1a.ermite.CompleteJobRequest\x1a\x1b.ermite.CompleteJobResponseBr\n" +
15221931 "\n" +
15231932 "com.ermiteB\vRunnerProtoP\x01Z\x1fbnema.dev/ermite-api/gen/ermite\xa2\x02\x03EXX\xaa\x02\x06Ermite\xca\x02\x06Ermite\xe2\x02\x12Ermite\\GPBMetadata\xea\x02\x06Ermiteb\x06proto3"
15241933
@@ -1535,7 +1944,7 @@ func file_ermite_runner_proto_rawDescGZIP() []byte {
15351944 }
15361945
15371946 var file_ermite_runner_proto_enumTypes = make([]protoimpl.EnumInfo, 5)
1538-var file_ermite_runner_proto_msgTypes = make([]protoimpl.MessageInfo, 16)
1947+var file_ermite_runner_proto_msgTypes = make([]protoimpl.MessageInfo, 21)
15391948 var file_ermite_runner_proto_goTypes = []any{
15401949 (CiStatus)(0), // 0: ermite.CiStatus
15411950 (CiConclusion)(0), // 1: ermite.CiConclusion
@@ -1550,47 +1959,62 @@ var file_ermite_runner_proto_goTypes = []any{
15501959 (*UpdateJobRequest)(nil), // 10: ermite.UpdateJobRequest
15511960 (*StepUpdate)(nil), // 11: ermite.StepUpdate
15521961 (*UpdateJobResponse)(nil), // 12: ermite.UpdateJobResponse
1553- (*AppendLogRequest)(nil), // 13: ermite.AppendLogRequest
1554- (*LogLine)(nil), // 14: ermite.LogLine
1555- (*AppendLogResponse)(nil), // 15: ermite.AppendLogResponse
1556- (*HeartbeatRequest)(nil), // 16: ermite.HeartbeatRequest
1557- (*HeartbeatResponse)(nil), // 17: ermite.HeartbeatResponse
1558- (*WatchJobRequest)(nil), // 18: ermite.WatchJobRequest
1559- (*WatchJobResponse)(nil), // 19: ermite.WatchJobResponse
1560- nil, // 20: ermite.UpdateJobRequest.OutputsEntry
1561- (*timestamppb.Timestamp)(nil), // 21: google.protobuf.Timestamp
1962+ (*CompleteJobRequest)(nil), // 13: ermite.CompleteJobRequest
1963+ (*CompleteJobResponse)(nil), // 14: ermite.CompleteJobResponse
1964+ (*JobEnded)(nil), // 15: ermite.JobEnded
1965+ (*AppendLogRequest)(nil), // 16: ermite.AppendLogRequest
1966+ (*LogLine)(nil), // 17: ermite.LogLine
1967+ (*AppendLogResponse)(nil), // 18: ermite.AppendLogResponse
1968+ (*HeartbeatRequest)(nil), // 19: ermite.HeartbeatRequest
1969+ (*HeartbeatResponse)(nil), // 20: ermite.HeartbeatResponse
1970+ (*WatchJobRequest)(nil), // 21: ermite.WatchJobRequest
1971+ (*WatchJobResponse)(nil), // 22: ermite.WatchJobResponse
1972+ (*Keepalive)(nil), // 23: ermite.Keepalive
1973+ (*CancelRequest)(nil), // 24: ermite.CancelRequest
1974+ nil, // 25: ermite.CompleteJobRequest.OutputsEntry
1975+ (*durationpb.Duration)(nil), // 26: google.protobuf.Duration
1976+ (*timestamppb.Timestamp)(nil), // 27: google.protobuf.Timestamp
15621977 }
15631978 var file_ermite_runner_proto_depIdxs = []int32{
15641979 3, // 0: ermite.RegisterRunnerRequest.isolation:type_name -> ermite.RunnerIsolation
15651980 9, // 1: ermite.FetchJobResponse.job:type_name -> ermite.Job
1566- 11, // 2: ermite.UpdateJobRequest.steps:type_name -> ermite.StepUpdate
1567- 20, // 3: ermite.UpdateJobRequest.outputs:type_name -> ermite.UpdateJobRequest.OutputsEntry
1568- 1, // 4: ermite.UpdateJobRequest.conclusion:type_name -> ermite.CiConclusion
1569- 2, // 5: ermite.StepUpdate.state:type_name -> ermite.StepState
1570- 1, // 6: ermite.StepUpdate.conclusion:type_name -> ermite.CiConclusion
1571- 21, // 7: ermite.StepUpdate.started_at:type_name -> google.protobuf.Timestamp
1572- 21, // 8: ermite.StepUpdate.completed_at:type_name -> google.protobuf.Timestamp
1573- 14, // 9: ermite.AppendLogRequest.lines:type_name -> ermite.LogLine
1574- 21, // 10: ermite.LogLine.time:type_name -> google.protobuf.Timestamp
1575- 4, // 11: ermite.WatchJobResponse.reason:type_name -> ermite.CancelReason
1576- 21, // 12: ermite.WatchJobResponse.requested_at:type_name -> google.protobuf.Timestamp
1577- 5, // 13: ermite.RunnerService.RegisterRunner:input_type -> ermite.RegisterRunnerRequest
1578- 7, // 14: ermite.RunnerService.FetchJob:input_type -> ermite.FetchJobRequest
1579- 10, // 15: ermite.RunnerService.UpdateJob:input_type -> ermite.UpdateJobRequest
1580- 13, // 16: ermite.RunnerService.AppendLog:input_type -> ermite.AppendLogRequest
1581- 16, // 17: ermite.RunnerService.Heartbeat:input_type -> ermite.HeartbeatRequest
1582- 18, // 18: ermite.RunnerService.WatchJob:input_type -> ermite.WatchJobRequest
1583- 6, // 19: ermite.RunnerService.RegisterRunner:output_type -> ermite.RegisterRunnerResponse
1584- 8, // 20: ermite.RunnerService.FetchJob:output_type -> ermite.FetchJobResponse
1585- 12, // 21: ermite.RunnerService.UpdateJob:output_type -> ermite.UpdateJobResponse
1586- 15, // 22: ermite.RunnerService.AppendLog:output_type -> ermite.AppendLogResponse
1587- 17, // 23: ermite.RunnerService.Heartbeat:output_type -> ermite.HeartbeatResponse
1588- 19, // 24: ermite.RunnerService.WatchJob:output_type -> ermite.WatchJobResponse
1589- 19, // [19:25] is the sub-list for method output_type
1590- 13, // [13:19] is the sub-list for method input_type
1591- 13, // [13:13] is the sub-list for extension type_name
1592- 13, // [13:13] is the sub-list for extension extendee
1593- 0, // [0:13] is the sub-list for field type_name
1981+ 26, // 2: ermite.FetchJobResponse.retry_after:type_name -> google.protobuf.Duration
1982+ 11, // 3: ermite.UpdateJobRequest.steps:type_name -> ermite.StepUpdate
1983+ 2, // 4: ermite.StepUpdate.state:type_name -> ermite.StepState
1984+ 1, // 5: ermite.StepUpdate.conclusion:type_name -> ermite.CiConclusion
1985+ 27, // 6: ermite.StepUpdate.started_at:type_name -> google.protobuf.Timestamp
1986+ 27, // 7: ermite.StepUpdate.completed_at:type_name -> google.protobuf.Timestamp
1987+ 1, // 8: ermite.CompleteJobRequest.conclusion:type_name -> ermite.CiConclusion
1988+ 25, // 9: ermite.CompleteJobRequest.outputs:type_name -> ermite.CompleteJobRequest.OutputsEntry
1989+ 11, // 10: ermite.CompleteJobRequest.steps:type_name -> ermite.StepUpdate
1990+ 1, // 11: ermite.CompleteJobResponse.conclusion:type_name -> ermite.CiConclusion
1991+ 1, // 12: ermite.JobEnded.conclusion:type_name -> ermite.CiConclusion
1992+ 17, // 13: ermite.AppendLogRequest.lines:type_name -> ermite.LogLine
1993+ 27, // 14: ermite.LogLine.time:type_name -> google.protobuf.Timestamp
1994+ 23, // 15: ermite.WatchJobResponse.keepalive:type_name -> ermite.Keepalive
1995+ 24, // 16: ermite.WatchJobResponse.cancel:type_name -> ermite.CancelRequest
1996+ 15, // 17: ermite.WatchJobResponse.ended:type_name -> ermite.JobEnded
1997+ 4, // 18: ermite.CancelRequest.reason:type_name -> ermite.CancelReason
1998+ 27, // 19: ermite.CancelRequest.requested_at:type_name -> google.protobuf.Timestamp
1999+ 5, // 20: ermite.RunnerService.RegisterRunner:input_type -> ermite.RegisterRunnerRequest
2000+ 7, // 21: ermite.RunnerService.FetchJob:input_type -> ermite.FetchJobRequest
2001+ 10, // 22: ermite.RunnerService.UpdateJob:input_type -> ermite.UpdateJobRequest
2002+ 16, // 23: ermite.RunnerService.AppendLog:input_type -> ermite.AppendLogRequest
2003+ 19, // 24: ermite.RunnerService.Heartbeat:input_type -> ermite.HeartbeatRequest
2004+ 21, // 25: ermite.RunnerService.WatchJob:input_type -> ermite.WatchJobRequest
2005+ 13, // 26: ermite.RunnerService.CompleteJob:input_type -> ermite.CompleteJobRequest
2006+ 6, // 27: ermite.RunnerService.RegisterRunner:output_type -> ermite.RegisterRunnerResponse
2007+ 8, // 28: ermite.RunnerService.FetchJob:output_type -> ermite.FetchJobResponse
2008+ 12, // 29: ermite.RunnerService.UpdateJob:output_type -> ermite.UpdateJobResponse
2009+ 18, // 30: ermite.RunnerService.AppendLog:output_type -> ermite.AppendLogResponse
2010+ 20, // 31: ermite.RunnerService.Heartbeat:output_type -> ermite.HeartbeatResponse
2011+ 22, // 32: ermite.RunnerService.WatchJob:output_type -> ermite.WatchJobResponse
2012+ 14, // 33: ermite.RunnerService.CompleteJob:output_type -> ermite.CompleteJobResponse
2013+ 27, // [27:34] is the sub-list for method output_type
2014+ 20, // [20:27] is the sub-list for method input_type
2015+ 20, // [20:20] is the sub-list for extension type_name
2016+ 20, // [20:20] is the sub-list for extension extendee
2017+ 0, // [0:20] is the sub-list for field type_name
15942018 }
15952019
15962020 func init() { file_ermite_runner_proto_init() }
@@ -1598,13 +2022,18 @@ func file_ermite_runner_proto_init() {
15982022 if File_ermite_runner_proto != nil {
15992023 return
16002024 }
2025+ file_ermite_runner_proto_msgTypes[17].OneofWrappers = []any{
2026+ (*WatchJobResponse_Keepalive)(nil),
2027+ (*WatchJobResponse_Cancel)(nil),
2028+ (*WatchJobResponse_Ended)(nil),
2029+ }
16012030 type x struct{}
16022031 out := protoimpl.TypeBuilder{
16032032 File: protoimpl.DescBuilder{
16042033 GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
16052034 RawDescriptor: unsafe.Slice(unsafe.StringData(file_ermite_runner_proto_rawDesc), len(file_ermite_runner_proto_rawDesc)),
16062035 NumEnums: 5,
1607- NumMessages: 16,
2036+ NumMessages: 21,
16082037 NumExtensions: 0,
16092038 NumServices: 1,
16102039 },
@@ -11,6 +11,7 @@ package ermite
11 import (11 import (
12 protoreflect "google.golang.org/protobuf/reflect/protoreflect"12 protoreflect "google.golang.org/protobuf/reflect/protoreflect"
13 protoimpl "google.golang.org/protobuf/runtime/protoimpl"13 protoimpl "google.golang.org/protobuf/runtime/protoimpl"
14+ durationpb "google.golang.org/protobuf/types/known/durationpb"
14 timestamppb "google.golang.org/protobuf/types/known/timestamppb"15 timestamppb "google.golang.org/protobuf/types/known/timestamppb"
15 reflect "reflect"16 reflect "reflect"
16 sync "sync"17 sync "sync"
@@ -287,8 +288,7 @@ func (RunnerIsolation) EnumDescriptor() ([]byte, []int) {
287 type CancelReason int32288 type CancelReason int32
288 289
289 const (290 const (
290- // Not set. A WatchJobResponse with this reason is a keep-alive and carries291+ // Not set. Never sent in a CancelRequest.
291- // no request.
292 CancelReason_CANCEL_REASON_UNSPECIFIED CancelReason = 0292 CancelReason_CANCEL_REASON_UNSPECIFIED CancelReason = 0
293 // The owner cancelled the run (RunService.CancelRun, web or CLI). The job293 // The owner cancelled the run (RunService.CancelRun, web or CLI). The job
294 // ends CI_CONCLUSION_CANCELLED.294 // ends CI_CONCLUSION_CANCELLED.
@@ -296,8 +296,8 @@ const (
296 // Another job of the same matrix failed and the strategy has fail-fast. The296 // Another job of the same matrix failed and the strategy has fail-fast. The
297 // job ends CI_CONCLUSION_CANCELLED.297 // job ends CI_CONCLUSION_CANCELLED.
298 CancelReason_CANCEL_REASON_FAIL_FAST CancelReason = 2298 CancelReason_CANCEL_REASON_FAIL_FAST CancelReason = 2
299- // The job exceeded its timeout_minutes without the runner ending it. The299+ // The job exceeded its time budget (see Job.timeout_minutes). The job ends
300- // job ends CI_CONCLUSION_TIMED_OUT.300+ // CI_CONCLUSION_TIMED_OUT.
301 CancelReason_CANCEL_REASON_TIMEOUT CancelReason = 3301 CancelReason_CANCEL_REASON_TIMEOUT CancelReason = 3
302 )302 )
303 303
@@ -367,7 +367,14 @@ type RegisterRunnerRequest struct {
367 Arch string `protobuf:"bytes,5,opt,name=arch,proto3" json:"arch,omitempty"`367 Arch string `protobuf:"bytes,5,opt,name=arch,proto3" json:"arch,omitempty"`
368 // Os is the operating system: "linux". Other values are invalid_argument368 // Os is the operating system: "linux". Other values are invalid_argument
369 // (the set may grow with compatible additions).369 // (the set may grow with compatible additions).
370- Os string `protobuf:"bytes,6,opt,name=os,proto3" json:"os,omitempty"`370+ Os string `protobuf:"bytes,6,opt,name=os,proto3" json:"os,omitempty"`
371+ // Replace removes the runner that already has this name, if any, before
372+ // registering this one: its runner token is revoked at once, as in
373+ // RunService.RemoveRunner, and a job it is running is not affected. The new
374+ // runner gets a new runner_id. Needs the same scope as registering. Without
375+ // replace, a name in use is already_exists. It is how a runner re-registers
376+ // after losing its state.
377+ Replace bool `protobuf:"varint,7,opt,name=replace,proto3" json:"replace,omitempty"`
371 unknownFields protoimpl.UnknownFields378 unknownFields protoimpl.UnknownFields
372 sizeCache protoimpl.SizeCache379 sizeCache protoimpl.SizeCache
373 }380 }
@@ -444,6 +451,13 @@ func (x *RegisterRunnerRequest) GetOs() string {
444 return ""451 return ""
445 }452 }
446 453
454+func (x *RegisterRunnerRequest) GetReplace() bool {
455+ if x != nil {
456+ return x.Replace
457+ }
458+ return false
459+}
460+
447 // RegisterRunnerResponse holds the new runner and its secret.461 // RegisterRunnerResponse holds the new runner and its secret.
448 type RegisterRunnerResponse struct {462 type RegisterRunnerResponse struct {
449 state protoimpl.MessageState `protogen:"open.v1"`463 state protoimpl.MessageState `protogen:"open.v1"`
@@ -451,9 +465,10 @@ type RegisterRunnerResponse struct {
451 // a secret.465 // a secret.
452 RunnerId string `protobuf:"bytes,1,opt,name=runner_id,json=runnerId,proto3" json:"runner_id,omitempty"`466 RunnerId string `protobuf:"bytes,1,opt,name=runner_id,json=runnerId,proto3" json:"runner_id,omitempty"`
453 // RunnerToken is the secret of the runner, to present as "Authorization:467 // RunnerToken is the secret of the runner, to present as "Authorization:
454- // Bearer <runner_token>" to FetchJob. It starts with "ermiterun_" and is468+ // Bearer <runner_token>" to FetchJob: "ermiterun_" and 43 base64url
455- // returned once: the server keeps only its SHA-256, the runner must store469+ // characters (32 random bytes, no padding). It is returned once: the server
456- // it (mode 0600). It stops working when the runner is removed.470+ // keeps only its SHA-256, the runner must store it (mode 0600). It stops
471+ // working when the runner is removed or replaced.
457 RunnerToken string `protobuf:"bytes,2,opt,name=runner_token,json=runnerToken,proto3" json:"runner_token,omitempty"`472 RunnerToken string `protobuf:"bytes,2,opt,name=runner_token,json=runnerToken,proto3" json:"runner_token,omitempty"`
458 unknownFields protoimpl.UnknownFields473 unknownFields protoimpl.UnknownFields
459 sizeCache protoimpl.SizeCache474 sizeCache protoimpl.SizeCache
@@ -510,7 +525,14 @@ type FetchJobRequest struct {
510 // labels of the runner, which is the normal case. Otherwise each one must be525 // labels of the runner, which is the normal case. Otherwise each one must be
511 // a label of the runner (invalid_argument if not) and only jobs whose runs-on526 // a label of the runner (invalid_argument if not) and only jobs whose runs-on
512 // labels are all among them are offered.527 // labels are all among them are offered.
513- Labels []string `protobuf:"bytes,1,rep,name=labels,proto3" json:"labels,omitempty"`528+ Labels []string `protobuf:"bytes,1,rep,name=labels,proto3" json:"labels,omitempty"`
529+ // PollId identifies this poll, chosen by the client (for example 16 random
530+ // bytes in base64url), 1 to 64 characters among letters, digits, "_" and
531+ // "-". A client that retries a call whose response it did not get sends the
532+ // same poll_id, and gets the assignment the first call made, if it made one
533+ // (see FetchJob). A new poll uses a new id. Empty means no retry protection:
534+ // a lost assignment is then recovered by the acknowledgement timeout alone.
535+ PollId string `protobuf:"bytes,2,opt,name=poll_id,json=pollId,proto3" json:"poll_id,omitempty"`
514 unknownFields protoimpl.UnknownFields536 unknownFields protoimpl.UnknownFields
515 sizeCache protoimpl.SizeCache537 sizeCache protoimpl.SizeCache
516 }538 }
@@ -552,12 +574,24 @@ func (x *FetchJobRequest) GetLabels() []string {
552 return nil574 return nil
553 }575 }
554 576
577+func (x *FetchJobRequest) GetPollId() string {
578+ if x != nil {
579+ return x.PollId
580+ }
581+ return ""
582+}
583+
555 // FetchJobResponse holds the job given to the runner, if any.584 // FetchJobResponse holds the job given to the runner, if any.
556 type FetchJobResponse struct {585 type FetchJobResponse struct {
557 state protoimpl.MessageState `protogen:"open.v1"`586 state protoimpl.MessageState `protogen:"open.v1"`
558 // Job is the job to run. Unset when no job was available before the end of587 // Job is the job to run. Unset when no job was available before the end of
559 // the poll.588 // the poll.
560- Job *Job `protobuf:"bytes,1,opt,name=job,proto3" json:"job,omitempty"`589+ Job *Job `protobuf:"bytes,1,opt,name=job,proto3" json:"job,omitempty"`
590+ // RetryAfter is how long the client waits before its next FetchJob. Unset or
591+ // zero means call again at once. The server sets it when it has no job for
592+ // the runner and wants to slow polling down (load, shutdown in progress);
593+ // clients obey it and may wait longer, never less. Unset when job is set.
594+ RetryAfter *durationpb.Duration `protobuf:"bytes,2,opt,name=retry_after,json=retryAfter,proto3" json:"retry_after,omitempty"`
561 unknownFields protoimpl.UnknownFields595 unknownFields protoimpl.UnknownFields
562 sizeCache protoimpl.SizeCache596 sizeCache protoimpl.SizeCache
563 }597 }
@@ -599,6 +633,13 @@ func (x *FetchJobResponse) GetJob() *Job {
599 return nil633 return nil
600 }634 }
601 635
636+func (x *FetchJobResponse) GetRetryAfter() *durationpb.Duration {
637+ if x != nil {
638+ return x.RetryAfter
639+ }
640+ return nil
641+}
642+
602 // Job is a job given to a runner: everything needed to run it, nothing more.643 // Job is a job given to a runner: everything needed to run it, nothing more.
603 // The runner parses workflow_yaml itself; the protocol does not carry the644 // The runner parses workflow_yaml itself; the protocol does not carry the
604 // workflow model, so it does not change when the model grows.645 // workflow model, so it does not change when the model grows.
@@ -615,10 +656,13 @@ type Job struct {
615 // one more after each RunService.RerunRun. It is the "github.run_attempt".656 // one more after each RunService.RerunRun. It is the "github.run_attempt".
616 Attempt int32 `protobuf:"varint,3,opt,name=attempt,proto3" json:"attempt,omitempty"`657 Attempt int32 `protobuf:"varint,3,opt,name=attempt,proto3" json:"attempt,omitempty"`
617 // JobToken is the secret that authenticates this job's UpdateJob,658 // JobToken is the secret that authenticates this job's UpdateJob,
618- // AppendLog, Heartbeat and WatchJob calls ("Authorization: Bearer659+ // AppendLog, Heartbeat, WatchJob and CompleteJob calls ("Authorization:
619- // <job_token>"). It is random, valid until the job ends, survives a server660+ // Bearer <job_token>"): "ermitejob_" and 43 base64url characters (32 random
620- // restart and is shown only in this response. It must stay in the runner661+ // bytes, no padding). It is random, valid from the assignment until the job
621- // host process and never reach the job's code.662+ // ends (and for CompleteJob 24 hours more), survives a server restart and
663+ // is shown only in this response, or in a repeat of the same poll_id. It
664+ // must stay in the runner host process and never reach the job's code. A new
665+ // assignment of the same job has a new token.
622 JobToken string `protobuf:"bytes,4,opt,name=job_token,json=jobToken,proto3" json:"job_token,omitempty"`666 JobToken string `protobuf:"bytes,4,opt,name=job_token,json=jobToken,proto3" json:"job_token,omitempty"`
623 // WorkflowPath is the path of the workflow file in the repository, for667 // WorkflowPath is the path of the workflow file in the repository, for
624 // example ".github/workflows/ci.yml".668 // example ".github/workflows/ci.yml".
@@ -643,8 +687,11 @@ type Job struct {
643 Labels []string `protobuf:"bytes,10,rep,name=labels,proto3" json:"labels,omitempty"`687 Labels []string `protobuf:"bytes,10,rep,name=labels,proto3" json:"labels,omitempty"`
644 // TimeoutMinutes is the timeout of the job in minutes: the value of the688 // TimeoutMinutes is the timeout of the job in minutes: the value of the
645 // workflow's timeout-minutes, or 360 when it has none. At least 1. The689 // workflow's timeout-minutes, or 360 when it has none. At least 1. The
646- // runner enforces it from the start of the job; the server ends a job that690+ // runner enforces it from the moment it receives the job. The server counts
647- // exceeds it by announcing CANCEL_REASON_TIMEOUT.691+ // from the moment of the assignment and adds 60 seconds of slack, so the
692+ // runner's own timeout always comes first: when the server's deadline
693+ // passes it announces CANCEL_REASON_TIMEOUT, and ends the job itself after
694+ // 60 more seconds without a conclusion (see WatchJob).
648 TimeoutMinutes int32 `protobuf:"varint,11,opt,name=timeout_minutes,json=timeoutMinutes,proto3" json:"timeout_minutes,omitempty"`695 TimeoutMinutes int32 `protobuf:"varint,11,opt,name=timeout_minutes,json=timeoutMinutes,proto3" json:"timeout_minutes,omitempty"`
649 // Repo is the name of the repository.696 // Repo is the name of the repository.
650 Repo string `protobuf:"bytes,12,opt,name=repo,proto3" json:"repo,omitempty"`697 Repo string `protobuf:"bytes,12,opt,name=repo,proto3" json:"repo,omitempty"`
@@ -786,7 +833,7 @@ func (x *Job) GetRef() string {
786 return ""833 return ""
787 }834 }
788 835
789-// UpdateJobRequest reports the steps and, in the last call, the end of a job.836+// UpdateJobRequest reports the steps of a running job.
790 type UpdateJobRequest struct {837 type UpdateJobRequest struct {
791 state protoimpl.MessageState `protogen:"open.v1"`838 state protoimpl.MessageState `protogen:"open.v1"`
792 // JobId is the Job.job_id. It must be the job of the token.839 // JobId is the Job.job_id. It must be the job of the token.
@@ -796,31 +843,9 @@ type UpdateJobRequest struct {
796 // (invalid_argument beyond). Steps are numbered like GitHub's log: the843 // (invalid_argument beyond). Steps are numbered like GitHub's log: the
797 // implicit "Set up job" is 1, the steps of the workflow follow in order, and844 // implicit "Set up job" is 1, the steps of the workflow follow in order, and
798 // the implicit "Complete job" is last.845 // the implicit "Complete job" is last.
799- Steps []*StepUpdate `protobuf:"bytes,2,rep,name=steps,proto3" json:"steps,omitempty"`846+ Steps []*StepUpdate `protobuf:"bytes,2,rep,name=steps,proto3" json:"steps,omitempty"`
800- // Outputs are the outputs of the job, evaluated by the runner. Only allowed847+ unknownFields protoimpl.UnknownFields
801- // in the call that carries the conclusion (invalid_argument otherwise).848+ sizeCache protoimpl.SizeCache
802- // Names are 1 to 255 characters among letters, digits, "_" and "-" and
803- // begin with a letter or "_" and are case-insensitive: two names that
804- // differ only by case are invalid_argument. Each value is at most 1 MiB, and
805- // all names and values together at most 1 MiB (1,048,576 bytes); beyond that
806- // the whole call is rejected with invalid_argument and the runner reports
807- // the failure of the job without outputs, with a line in the log.
808- Outputs map[string]string `protobuf:"bytes,3,rep,name=outputs,proto3" json:"outputs,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
809- // Conclusion ends the job when set. A runner reports CI_CONCLUSION_SUCCESS,
810- // CI_CONCLUSION_FAILURE, CI_CONCLUSION_CANCELLED or CI_CONCLUSION_TIMED_OUT; any
811- // other value is invalid_argument. CI_CONCLUSION_UNSPECIFIED means the job goes
812- // on. Steps that were not reported COMPLETED when the job ends are
813- // completed by the server as CI_CONCLUSION_CANCELLED (or SKIPPED when they
814- // never started and the job did not fail by cancellation).
815- Conclusion CiConclusion `protobuf:"varint,4,opt,name=conclusion,proto3,enum=ermite.CiConclusion" json:"conclusion,omitempty"`
816- // RunnerShutdown is true when the runner ends the job because it was told to
817- // stop (service stopped, signal). Only allowed with CI_CONCLUSION_CANCELLED.
818- // The server then annotates the job with GitHub's message "The runner has
819- // received a shutdown signal. This can happen when the runner service is
820- // stopped, or a manually started runner is canceled."
821- RunnerShutdown bool `protobuf:"varint,5,opt,name=runner_shutdown,json=runnerShutdown,proto3" json:"runner_shutdown,omitempty"`
822- unknownFields protoimpl.UnknownFields
823- sizeCache protoimpl.SizeCache
824 }849 }
825 850
826 func (x *UpdateJobRequest) Reset() {851 func (x *UpdateJobRequest) Reset() {
@@ -867,27 +892,6 @@ func (x *UpdateJobRequest) GetSteps() []*StepUpdate {
867 return nil892 return nil
868 }893 }
869 894
870-func (x *UpdateJobRequest) GetOutputs() map[string]string {
871- if x != nil {
872- return x.Outputs
873- }
874- return nil
875-}
876-
877-func (x *UpdateJobRequest) GetConclusion() CiConclusion {
878- if x != nil {
879- return x.Conclusion
880- }
881- return CiConclusion_CI_CONCLUSION_UNSPECIFIED
882-}
883-
884-func (x *UpdateJobRequest) GetRunnerShutdown() bool {
885- if x != nil {
886- return x.RunnerShutdown
887- }
888- return false
889-}
890-
891 // StepUpdate is the state of one step as the runner knows it.895 // StepUpdate is the state of one step as the runner knows it.
892 type StepUpdate struct {896 type StepUpdate struct {
893 state protoimpl.MessageState `protogen:"open.v1"`897 state protoimpl.MessageState `protogen:"open.v1"`
@@ -1023,14 +1027,237 @@ func (*UpdateJobResponse) Descriptor() ([]byte, []int) {
1023 return file_ermite_runner_proto_rawDescGZIP(), []int{7}1027 return file_ermite_runner_proto_rawDescGZIP(), []int{7}
1024 }1028 }
1025 1029
1030+// CompleteJobRequest ends a job.
1031+type CompleteJobRequest struct {
1032+ state protoimpl.MessageState `protogen:"open.v1"`
1033+ // JobId is the Job.job_id. It must be the job of the token.
1034+ JobId string `protobuf:"bytes,1,opt,name=job_id,json=jobId,proto3" json:"job_id,omitempty"`
1035+ // Conclusion is how the job ended. Required: a runner reports
1036+ // CI_CONCLUSION_SUCCESS, CI_CONCLUSION_FAILURE, CI_CONCLUSION_CANCELLED or
1037+ // CI_CONCLUSION_TIMED_OUT; any other value is invalid_argument. Skipped is
1038+ // decided by the server and never reported by a runner.
1039+ Conclusion CiConclusion `protobuf:"varint,2,opt,name=conclusion,proto3,enum=ermite.CiConclusion" json:"conclusion,omitempty"`
1040+ // Outputs are the outputs of the job, evaluated by the runner. Names are 1
1041+ // to 255 characters among letters, digits, "_" and "-" and begin with a
1042+ // letter or "_" and are case-insensitive: two names that differ only by case
1043+ // are invalid_argument. Each value is at most 1 MiB, and all names and
1044+ // values together at most 1 MiB (1,048,576 bytes); beyond that the whole call
1045+ // is rejected with invalid_argument and the runner reports the failure of
1046+ // the job without outputs, with a line in the log.
1047+ Outputs map[string]string `protobuf:"bytes,3,rep,name=outputs,proto3" json:"outputs,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
1048+ // RunnerShutdown is true when the runner ends the job because it was told to
1049+ // stop (service stopped, signal). Only allowed with CI_CONCLUSION_CANCELLED.
1050+ // The server then annotates the job with GitHub's message "The runner has
1051+ // received a shutdown signal. This can happen when the runner service is
1052+ // stopped, or a manually started runner is canceled." The conclusion
1053+ // CANCELLED for this case is to be verified against a recording of GitHub.
1054+ RunnerShutdown bool `protobuf:"varint,4,opt,name=runner_shutdown,json=runnerShutdown,proto3" json:"runner_shutdown,omitempty"`
1055+ // Steps are the final steps, with the same meaning as UpdateJobRequest.steps,
1056+ // applied in the same atomic step as the conclusion.
1057+ Steps []*StepUpdate `protobuf:"bytes,5,rep,name=steps,proto3" json:"steps,omitempty"`
1058+ unknownFields protoimpl.UnknownFields
1059+ sizeCache protoimpl.SizeCache
1060+}
1061+
1062+func (x *CompleteJobRequest) Reset() {
1063+ *x = CompleteJobRequest{}
1064+ mi := &file_ermite_runner_proto_msgTypes[8]
1065+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1066+ ms.StoreMessageInfo(mi)
1067+}
1068+
1069+func (x *CompleteJobRequest) String() string {
1070+ return protoimpl.X.MessageStringOf(x)
1071+}
1072+
1073+func (*CompleteJobRequest) ProtoMessage() {}
1074+
1075+func (x *CompleteJobRequest) ProtoReflect() protoreflect.Message {
1076+ mi := &file_ermite_runner_proto_msgTypes[8]
1077+ if x != nil {
1078+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1079+ if ms.LoadMessageInfo() == nil {
1080+ ms.StoreMessageInfo(mi)
1081+ }
1082+ return ms
1083+ }
1084+ return mi.MessageOf(x)
1085+}
1086+
1087+// Deprecated: Use CompleteJobRequest.ProtoReflect.Descriptor instead.
1088+func (*CompleteJobRequest) Descriptor() ([]byte, []int) {
1089+ return file_ermite_runner_proto_rawDescGZIP(), []int{8}
1090+}
1091+
1092+func (x *CompleteJobRequest) GetJobId() string {
1093+ if x != nil {
1094+ return x.JobId
1095+ }
1096+ return ""
1097+}
1098+
1099+func (x *CompleteJobRequest) GetConclusion() CiConclusion {
1100+ if x != nil {
1101+ return x.Conclusion
1102+ }
1103+ return CiConclusion_CI_CONCLUSION_UNSPECIFIED
1104+}
1105+
1106+func (x *CompleteJobRequest) GetOutputs() map[string]string {
1107+ if x != nil {
1108+ return x.Outputs
1109+ }
1110+ return nil
1111+}
1112+
1113+func (x *CompleteJobRequest) GetRunnerShutdown() bool {
1114+ if x != nil {
1115+ return x.RunnerShutdown
1116+ }
1117+ return false
1118+}
1119+
1120+func (x *CompleteJobRequest) GetSteps() []*StepUpdate {
1121+ if x != nil {
1122+ return x.Steps
1123+ }
1124+ return nil
1125+}
1126+
1127+// CompleteJobResponse is the stored result of a job.
1128+type CompleteJobResponse struct {
1129+ state protoimpl.MessageState `protogen:"open.v1"`
1130+ // Conclusion is the conclusion of the job as the server stored it. It is the
1131+ // one of the request when applied is true.
1132+ Conclusion CiConclusion `protobuf:"varint,1,opt,name=conclusion,proto3,enum=ermite.CiConclusion" json:"conclusion,omitempty"`
1133+ // Error is the error text stored with the job, as RunJob.error. Empty when
1134+ // there is none.
1135+ Error string `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"`
1136+ // Applied is true when this call ended the job, false when the job had
1137+ // already ended (a retry, or the server ended it first) and nothing changed.
1138+ Applied bool `protobuf:"varint,3,opt,name=applied,proto3" json:"applied,omitempty"`
1139+ unknownFields protoimpl.UnknownFields
1140+ sizeCache protoimpl.SizeCache
1141+}
1142+
1143+func (x *CompleteJobResponse) Reset() {
1144+ *x = CompleteJobResponse{}
1145+ mi := &file_ermite_runner_proto_msgTypes[9]
1146+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1147+ ms.StoreMessageInfo(mi)
1148+}
1149+
1150+func (x *CompleteJobResponse) String() string {
1151+ return protoimpl.X.MessageStringOf(x)
1152+}
1153+
1154+func (*CompleteJobResponse) ProtoMessage() {}
1155+
1156+func (x *CompleteJobResponse) ProtoReflect() protoreflect.Message {
1157+ mi := &file_ermite_runner_proto_msgTypes[9]
1158+ if x != nil {
1159+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1160+ if ms.LoadMessageInfo() == nil {
1161+ ms.StoreMessageInfo(mi)
1162+ }
1163+ return ms
1164+ }
1165+ return mi.MessageOf(x)
1166+}
1167+
1168+// Deprecated: Use CompleteJobResponse.ProtoReflect.Descriptor instead.
1169+func (*CompleteJobResponse) Descriptor() ([]byte, []int) {
1170+ return file_ermite_runner_proto_rawDescGZIP(), []int{9}
1171+}
1172+
1173+func (x *CompleteJobResponse) GetConclusion() CiConclusion {
1174+ if x != nil {
1175+ return x.Conclusion
1176+ }
1177+ return CiConclusion_CI_CONCLUSION_UNSPECIFIED
1178+}
1179+
1180+func (x *CompleteJobResponse) GetError() string {
1181+ if x != nil {
1182+ return x.Error
1183+ }
1184+ return ""
1185+}
1186+
1187+func (x *CompleteJobResponse) GetApplied() bool {
1188+ if x != nil {
1189+ return x.Applied
1190+ }
1191+ return false
1192+}
1193+
1194+// JobEnded says that a job has ended. It is sent in WatchJob and attached, as
1195+// an error detail, to the failed_precondition errors that RunnerService returns
1196+// for a job that has ended.
1197+type JobEnded struct {
1198+ state protoimpl.MessageState `protogen:"open.v1"`
1199+ // Conclusion is how the job ended.
1200+ Conclusion CiConclusion `protobuf:"varint,1,opt,name=conclusion,proto3,enum=ermite.CiConclusion" json:"conclusion,omitempty"`
1201+ // Error is the error text stored with the job, as RunJob.error. Empty when
1202+ // there is none.
1203+ Error string `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"`
1204+ unknownFields protoimpl.UnknownFields
1205+ sizeCache protoimpl.SizeCache
1206+}
1207+
1208+func (x *JobEnded) Reset() {
1209+ *x = JobEnded{}
1210+ mi := &file_ermite_runner_proto_msgTypes[10]
1211+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1212+ ms.StoreMessageInfo(mi)
1213+}
1214+
1215+func (x *JobEnded) String() string {
1216+ return protoimpl.X.MessageStringOf(x)
1217+}
1218+
1219+func (*JobEnded) ProtoMessage() {}
1220+
1221+func (x *JobEnded) ProtoReflect() protoreflect.Message {
1222+ mi := &file_ermite_runner_proto_msgTypes[10]
1223+ if x != nil {
1224+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1225+ if ms.LoadMessageInfo() == nil {
1226+ ms.StoreMessageInfo(mi)
1227+ }
1228+ return ms
1229+ }
1230+ return mi.MessageOf(x)
1231+}
1232+
1233+// Deprecated: Use JobEnded.ProtoReflect.Descriptor instead.
1234+func (*JobEnded) Descriptor() ([]byte, []int) {
1235+ return file_ermite_runner_proto_rawDescGZIP(), []int{10}
1236+}
1237+
1238+func (x *JobEnded) GetConclusion() CiConclusion {
1239+ if x != nil {
1240+ return x.Conclusion
1241+ }
1242+ return CiConclusion_CI_CONCLUSION_UNSPECIFIED
1243+}
1244+
1245+func (x *JobEnded) GetError() string {
1246+ if x != nil {
1247+ return x.Error
1248+ }
1249+ return ""
1250+}
1251+
1026 // AppendLogRequest carries log lines of one step.1252 // AppendLogRequest carries log lines of one step.
1027 type AppendLogRequest struct {1253 type AppendLogRequest struct {
1028 state protoimpl.MessageState `protogen:"open.v1"`1254 state protoimpl.MessageState `protogen:"open.v1"`
1029 // JobId is the Job.job_id. It must be the job of the token.1255 // JobId is the Job.job_id. It must be the job of the token.
1030 JobId string `protobuf:"bytes,1,opt,name=job_id,json=jobId,proto3" json:"job_id,omitempty"`1256 JobId string `protobuf:"bytes,1,opt,name=job_id,json=jobId,proto3" json:"job_id,omitempty"`
1031 // Step is the number of the step the lines belong to (see1257 // Step is the number of the step the lines belong to (see
1032- // UpdateJobRequest.steps), at least 1. A step that UpdateJob has not1258+ // UpdateJobRequest.steps), at least 1, and at least the step of the last
1033- // announced yet is created by the first lines.1259+ // stored line. A step that UpdateJob has not announced yet is created by the
1260+ // first lines. See the rules on AppendLog.
1034 Step int32 `protobuf:"varint,2,opt,name=step,proto3" json:"step,omitempty"`1261 Step int32 `protobuf:"varint,2,opt,name=step,proto3" json:"step,omitempty"`
1035 // Offset is the number of the first line of the request in the log of the1262 // Offset is the number of the first line of the request in the log of the
1036 // job, counted from 0 across all steps. See the rules on AppendLog.1263 // job, counted from 0 across all steps. See the rules on AppendLog.
@@ -1044,7 +1271,7 @@ type AppendLogRequest struct {
1044 1271
1045 func (x *AppendLogRequest) Reset() {1272 func (x *AppendLogRequest) Reset() {
1046 *x = AppendLogRequest{}1273 *x = AppendLogRequest{}
1047- mi := &file_ermite_runner_proto_msgTypes[8]1274+ mi := &file_ermite_runner_proto_msgTypes[11]
1048 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1275 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1049 ms.StoreMessageInfo(mi)1276 ms.StoreMessageInfo(mi)
1050 }1277 }
@@ -1056,7 +1283,7 @@ func (x *AppendLogRequest) String() string {
1056 func (*AppendLogRequest) ProtoMessage() {}1283 func (*AppendLogRequest) ProtoMessage() {}
1057 1284
1058 func (x *AppendLogRequest) ProtoReflect() protoreflect.Message {1285 func (x *AppendLogRequest) ProtoReflect() protoreflect.Message {
1059- mi := &file_ermite_runner_proto_msgTypes[8]1286+ mi := &file_ermite_runner_proto_msgTypes[11]
1060 if x != nil {1287 if x != nil {
1061 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1288 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1062 if ms.LoadMessageInfo() == nil {1289 if ms.LoadMessageInfo() == nil {
@@ -1069,7 +1296,7 @@ func (x *AppendLogRequest) ProtoReflect() protoreflect.Message {
1069 1296
1070 // Deprecated: Use AppendLogRequest.ProtoReflect.Descriptor instead.1297 // Deprecated: Use AppendLogRequest.ProtoReflect.Descriptor instead.
1071 func (*AppendLogRequest) Descriptor() ([]byte, []int) {1298 func (*AppendLogRequest) Descriptor() ([]byte, []int) {
1072- return file_ermite_runner_proto_rawDescGZIP(), []int{8}1299+ return file_ermite_runner_proto_rawDescGZIP(), []int{11}
1073 }1300 }
1074 1301
1075 func (x *AppendLogRequest) GetJobId() string {1302 func (x *AppendLogRequest) GetJobId() string {
@@ -1110,14 +1337,23 @@ type LogLine struct {
1110 // replaces invalid bytes with U+FFFD). It must not contain a line feed; a1337 // replaces invalid bytes with U+FFFD). It must not contain a line feed; a
1111 // carriage return is kept. It is the line as GitHub's raw log has it,1338 // carriage return is kept. It is the line as GitHub's raw log has it,
1112 // including "##[group]" and "##[endgroup]" markers and "##[error]" lines.1339 // including "##[group]" and "##[endgroup]" markers and "##[error]" lines.
1113- Text string `protobuf:"bytes,2,opt,name=text,proto3" json:"text,omitempty"`1340+ Text string `protobuf:"bytes,2,opt,name=text,proto3" json:"text,omitempty"`
1341+ // Continued is true when this line is the continuation of the line before
1342+ // it, which was too long to send whole. A runner cuts a line of more than
1343+ // 64 KiB into pieces at UTF-8 character boundaries, each piece of at most
1344+ // 64 KiB, and sets continued on every piece but the first. Each piece is one
1345+ // line for numbering and offsets; the server and the raw log join the
1346+ // pieces back, so the original line is rebuilt exactly. The first line of a
1347+ // job cannot be continued (invalid_argument) and a continued line is in
1348+ // the same step as the line before it (failed_precondition otherwise).
1349+ Continued bool `protobuf:"varint,3,opt,name=continued,proto3" json:"continued,omitempty"`
1114 unknownFields protoimpl.UnknownFields1350 unknownFields protoimpl.UnknownFields
1115 sizeCache protoimpl.SizeCache1351 sizeCache protoimpl.SizeCache
1116 }1352 }
1117 1353
1118 func (x *LogLine) Reset() {1354 func (x *LogLine) Reset() {
1119 *x = LogLine{}1355 *x = LogLine{}
1120- mi := &file_ermite_runner_proto_msgTypes[9]1356+ mi := &file_ermite_runner_proto_msgTypes[12]
1121 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1357 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1122 ms.StoreMessageInfo(mi)1358 ms.StoreMessageInfo(mi)
1123 }1359 }
@@ -1129,7 +1365,7 @@ func (x *LogLine) String() string {
1129 func (*LogLine) ProtoMessage() {}1365 func (*LogLine) ProtoMessage() {}
1130 1366
1131 func (x *LogLine) ProtoReflect() protoreflect.Message {1367 func (x *LogLine) ProtoReflect() protoreflect.Message {
1132- mi := &file_ermite_runner_proto_msgTypes[9]1368+ mi := &file_ermite_runner_proto_msgTypes[12]
1133 if x != nil {1369 if x != nil {
1134 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1370 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1135 if ms.LoadMessageInfo() == nil {1371 if ms.LoadMessageInfo() == nil {
@@ -1142,7 +1378,7 @@ func (x *LogLine) ProtoReflect() protoreflect.Message {
1142 1378
1143 // Deprecated: Use LogLine.ProtoReflect.Descriptor instead.1379 // Deprecated: Use LogLine.ProtoReflect.Descriptor instead.
1144 func (*LogLine) Descriptor() ([]byte, []int) {1380 func (*LogLine) Descriptor() ([]byte, []int) {
1145- return file_ermite_runner_proto_rawDescGZIP(), []int{9}1381+ return file_ermite_runner_proto_rawDescGZIP(), []int{12}
1146 }1382 }
1147 1383
1148 func (x *LogLine) GetTime() *timestamppb.Timestamp {1384 func (x *LogLine) GetTime() *timestamppb.Timestamp {
@@ -1159,6 +1395,13 @@ func (x *LogLine) GetText() string {
1159 return ""1395 return ""
1160 }1396 }
1161 1397
1398+func (x *LogLine) GetContinued() bool {
1399+ if x != nil {
1400+ return x.Continued
1401+ }
1402+ return false
1403+}
1404+
1162 // AppendLogResponse tells how much of the log the server holds.1405 // AppendLogResponse tells how much of the log the server holds.
1163 type AppendLogResponse struct {1406 type AppendLogResponse struct {
1164 state protoimpl.MessageState `protogen:"open.v1"`1407 state protoimpl.MessageState `protogen:"open.v1"`
@@ -1171,7 +1414,7 @@ type AppendLogResponse struct {
1171 1414
1172 func (x *AppendLogResponse) Reset() {1415 func (x *AppendLogResponse) Reset() {
1173 *x = AppendLogResponse{}1416 *x = AppendLogResponse{}
1174- mi := &file_ermite_runner_proto_msgTypes[10]1417+ mi := &file_ermite_runner_proto_msgTypes[13]
1175 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1418 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1176 ms.StoreMessageInfo(mi)1419 ms.StoreMessageInfo(mi)
1177 }1420 }
@@ -1183,7 +1426,7 @@ func (x *AppendLogResponse) String() string {
1183 func (*AppendLogResponse) ProtoMessage() {}1426 func (*AppendLogResponse) ProtoMessage() {}
1184 1427
1185 func (x *AppendLogResponse) ProtoReflect() protoreflect.Message {1428 func (x *AppendLogResponse) ProtoReflect() protoreflect.Message {
1186- mi := &file_ermite_runner_proto_msgTypes[10]1429+ mi := &file_ermite_runner_proto_msgTypes[13]
1187 if x != nil {1430 if x != nil {
1188 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1431 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1189 if ms.LoadMessageInfo() == nil {1432 if ms.LoadMessageInfo() == nil {
@@ -1196,7 +1439,7 @@ func (x *AppendLogResponse) ProtoReflect() protoreflect.Message {
1196 1439
1197 // Deprecated: Use AppendLogResponse.ProtoReflect.Descriptor instead.1440 // Deprecated: Use AppendLogResponse.ProtoReflect.Descriptor instead.
1198 func (*AppendLogResponse) Descriptor() ([]byte, []int) {1441 func (*AppendLogResponse) Descriptor() ([]byte, []int) {
1199- return file_ermite_runner_proto_rawDescGZIP(), []int{10}1442+ return file_ermite_runner_proto_rawDescGZIP(), []int{13}
1200 }1443 }
1201 1444
1202 func (x *AppendLogResponse) GetNextOffset() int64 {1445 func (x *AppendLogResponse) GetNextOffset() int64 {
@@ -1217,7 +1460,7 @@ type HeartbeatRequest struct {
1217 1460
1218 func (x *HeartbeatRequest) Reset() {1461 func (x *HeartbeatRequest) Reset() {
1219 *x = HeartbeatRequest{}1462 *x = HeartbeatRequest{}
1220- mi := &file_ermite_runner_proto_msgTypes[11]1463+ mi := &file_ermite_runner_proto_msgTypes[14]
1221 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1464 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1222 ms.StoreMessageInfo(mi)1465 ms.StoreMessageInfo(mi)
1223 }1466 }
@@ -1229,7 +1472,7 @@ func (x *HeartbeatRequest) String() string {
1229 func (*HeartbeatRequest) ProtoMessage() {}1472 func (*HeartbeatRequest) ProtoMessage() {}
1230 1473
1231 func (x *HeartbeatRequest) ProtoReflect() protoreflect.Message {1474 func (x *HeartbeatRequest) ProtoReflect() protoreflect.Message {
1232- mi := &file_ermite_runner_proto_msgTypes[11]1475+ mi := &file_ermite_runner_proto_msgTypes[14]
1233 if x != nil {1476 if x != nil {
1234 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1477 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1235 if ms.LoadMessageInfo() == nil {1478 if ms.LoadMessageInfo() == nil {
@@ -1242,7 +1485,7 @@ func (x *HeartbeatRequest) ProtoReflect() protoreflect.Message {
1242 1485
1243 // Deprecated: Use HeartbeatRequest.ProtoReflect.Descriptor instead.1486 // Deprecated: Use HeartbeatRequest.ProtoReflect.Descriptor instead.
1244 func (*HeartbeatRequest) Descriptor() ([]byte, []int) {1487 func (*HeartbeatRequest) Descriptor() ([]byte, []int) {
1245- return file_ermite_runner_proto_rawDescGZIP(), []int{11}1488+ return file_ermite_runner_proto_rawDescGZIP(), []int{14}
1246 }1489 }
1247 1490
1248 func (x *HeartbeatRequest) GetJobId() string {1491 func (x *HeartbeatRequest) GetJobId() string {
@@ -1261,7 +1504,7 @@ type HeartbeatResponse struct {
1261 1504
1262 func (x *HeartbeatResponse) Reset() {1505 func (x *HeartbeatResponse) Reset() {
1263 *x = HeartbeatResponse{}1506 *x = HeartbeatResponse{}
1264- mi := &file_ermite_runner_proto_msgTypes[12]1507+ mi := &file_ermite_runner_proto_msgTypes[15]
1265 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1508 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1266 ms.StoreMessageInfo(mi)1509 ms.StoreMessageInfo(mi)
1267 }1510 }
@@ -1273,7 +1516,7 @@ func (x *HeartbeatResponse) String() string {
1273 func (*HeartbeatResponse) ProtoMessage() {}1516 func (*HeartbeatResponse) ProtoMessage() {}
1274 1517
1275 func (x *HeartbeatResponse) ProtoReflect() protoreflect.Message {1518 func (x *HeartbeatResponse) ProtoReflect() protoreflect.Message {
1276- mi := &file_ermite_runner_proto_msgTypes[12]1519+ mi := &file_ermite_runner_proto_msgTypes[15]
1277 if x != nil {1520 if x != nil {
1278 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1521 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1279 if ms.LoadMessageInfo() == nil {1522 if ms.LoadMessageInfo() == nil {
@@ -1286,7 +1529,7 @@ func (x *HeartbeatResponse) ProtoReflect() protoreflect.Message {
1286 1529
1287 // Deprecated: Use HeartbeatResponse.ProtoReflect.Descriptor instead.1530 // Deprecated: Use HeartbeatResponse.ProtoReflect.Descriptor instead.
1288 func (*HeartbeatResponse) Descriptor() ([]byte, []int) {1531 func (*HeartbeatResponse) Descriptor() ([]byte, []int) {
1289- return file_ermite_runner_proto_rawDescGZIP(), []int{12}1532+ return file_ermite_runner_proto_rawDescGZIP(), []int{15}
1290 }1533 }
1291 1534
1292 // WatchJobRequest names the job to watch.1535 // WatchJobRequest names the job to watch.
@@ -1300,7 +1543,7 @@ type WatchJobRequest struct {
1300 1543
1301 func (x *WatchJobRequest) Reset() {1544 func (x *WatchJobRequest) Reset() {
1302 *x = WatchJobRequest{}1545 *x = WatchJobRequest{}
1303- mi := &file_ermite_runner_proto_msgTypes[13]1546+ mi := &file_ermite_runner_proto_msgTypes[16]
1304 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1547 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1305 ms.StoreMessageInfo(mi)1548 ms.StoreMessageInfo(mi)
1306 }1549 }
@@ -1312,7 +1555,7 @@ func (x *WatchJobRequest) String() string {
1312 func (*WatchJobRequest) ProtoMessage() {}1555 func (*WatchJobRequest) ProtoMessage() {}
1313 1556
1314 func (x *WatchJobRequest) ProtoReflect() protoreflect.Message {1557 func (x *WatchJobRequest) ProtoReflect() protoreflect.Message {
1315- mi := &file_ermite_runner_proto_msgTypes[13]1558+ mi := &file_ermite_runner_proto_msgTypes[16]
1316 if x != nil {1559 if x != nil {
1317 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1560 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1318 if ms.LoadMessageInfo() == nil {1561 if ms.LoadMessageInfo() == nil {
@@ -1325,7 +1568,7 @@ func (x *WatchJobRequest) ProtoReflect() protoreflect.Message {
1325 1568
1326 // Deprecated: Use WatchJobRequest.ProtoReflect.Descriptor instead.1569 // Deprecated: Use WatchJobRequest.ProtoReflect.Descriptor instead.
1327 func (*WatchJobRequest) Descriptor() ([]byte, []int) {1570 func (*WatchJobRequest) Descriptor() ([]byte, []int) {
1328- return file_ermite_runner_proto_rawDescGZIP(), []int{13}1571+ return file_ermite_runner_proto_rawDescGZIP(), []int{16}
1329 }1572 }
1330 1573
1331 func (x *WatchJobRequest) GetJobId() string {1574 func (x *WatchJobRequest) GetJobId() string {
@@ -1335,28 +1578,25 @@ func (x *WatchJobRequest) GetJobId() string {
1335 return ""1578 return ""
1336 }1579 }
1337 1580
1338-// WatchJobResponse is one message of the stream: a request to cancel the job,1581+// WatchJobResponse is one message of the stream.
1339-// or a keep-alive.
1340 type WatchJobResponse struct {1582 type WatchJobResponse struct {
1341 state protoimpl.MessageState `protogen:"open.v1"`1583 state protoimpl.MessageState `protogen:"open.v1"`
1342- // Reason is why the job must stop. CANCEL_REASON_UNSPECIFIED marks a1584+ // Msg is what the message carries. New kinds may be added: a client ignores
1343- // keep-alive: the other fields are empty and the client ignores it. New1585+ // the ones it does not know.
1344- // reasons may be added; a runner that does not know one cancels the job and1586+ //
1345- // reports CI_CONCLUSION_CANCELLED.1587+ // Types that are valid to be assigned to Msg:
1346- Reason CancelReason `protobuf:"varint,1,opt,name=reason,proto3,enum=ermite.CancelReason" json:"reason,omitempty"`1588+ //
1347- // Message is a short human-readable explanation, for the job log, for1589+ // *WatchJobResponse_Keepalive
1348- // example "Canceled by the owner". At most 1,024 characters.1590+ // *WatchJobResponse_Cancel
1349- Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"`1591+ // *WatchJobResponse_Ended
1350- // RequestedAt is when the cancellation was requested, by the clock of the1592+ Msg isWatchJobResponse_Msg `protobuf_oneof:"msg"`
1351- // server.
1352- RequestedAt *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=requested_at,json=requestedAt,proto3" json:"requested_at,omitempty"`
1353 unknownFields protoimpl.UnknownFields1593 unknownFields protoimpl.UnknownFields
1354 sizeCache protoimpl.SizeCache1594 sizeCache protoimpl.SizeCache
1355 }1595 }
1356 1596
1357 func (x *WatchJobResponse) Reset() {1597 func (x *WatchJobResponse) Reset() {
1358 *x = WatchJobResponse{}1598 *x = WatchJobResponse{}
1359- mi := &file_ermite_runner_proto_msgTypes[14]1599+ mi := &file_ermite_runner_proto_msgTypes[17]
1360 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1600 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1361 ms.StoreMessageInfo(mi)1601 ms.StoreMessageInfo(mi)
1362 }1602 }
@@ -1368,7 +1608,7 @@ func (x *WatchJobResponse) String() string {
1368 func (*WatchJobResponse) ProtoMessage() {}1608 func (*WatchJobResponse) ProtoMessage() {}
1369 1609
1370 func (x *WatchJobResponse) ProtoReflect() protoreflect.Message {1610 func (x *WatchJobResponse) ProtoReflect() protoreflect.Message {
1371- mi := &file_ermite_runner_proto_msgTypes[14]1611+ mi := &file_ermite_runner_proto_msgTypes[17]
1372 if x != nil {1612 if x != nil {
1373 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))1613 ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1374 if ms.LoadMessageInfo() == nil {1614 if ms.LoadMessageInfo() == nil {
@@ -1381,24 +1621,167 @@ func (x *WatchJobResponse) ProtoReflect() protoreflect.Message {
1381 1621
1382 // Deprecated: Use WatchJobResponse.ProtoReflect.Descriptor instead.1622 // Deprecated: Use WatchJobResponse.ProtoReflect.Descriptor instead.
1383 func (*WatchJobResponse) Descriptor() ([]byte, []int) {1623 func (*WatchJobResponse) Descriptor() ([]byte, []int) {
1384- return file_ermite_runner_proto_rawDescGZIP(), []int{14}1624+ return file_ermite_runner_proto_rawDescGZIP(), []int{17}
1385 }1625 }
1386 1626
1387-func (x *WatchJobResponse) GetReason() CancelReason {1627+func (x *WatchJobResponse) GetMsg() isWatchJobResponse_Msg {
1628+ if x != nil {
1629+ return x.Msg
1630+ }
1631+ return nil
1632+}
1633+
1634+func (x *WatchJobResponse) GetKeepalive() *Keepalive {
1635+ if x != nil {
1636+ if x, ok := x.Msg.(*WatchJobResponse_Keepalive); ok {
1637+ return x.Keepalive
1638+ }
1639+ }
1640+ return nil
1641+}
1642+
1643+func (x *WatchJobResponse) GetCancel() *CancelRequest {
1644+ if x != nil {
1645+ if x, ok := x.Msg.(*WatchJobResponse_Cancel); ok {
1646+ return x.Cancel
1647+ }
1648+ }
1649+ return nil
1650+}
1651+
1652+func (x *WatchJobResponse) GetEnded() *JobEnded {
1653+ if x != nil {
1654+ if x, ok := x.Msg.(*WatchJobResponse_Ended); ok {
1655+ return x.Ended
1656+ }
1657+ }
1658+ return nil
1659+}
1660+
1661+type isWatchJobResponse_Msg interface {
1662+ isWatchJobResponse_Msg()
1663+}
1664+
1665+type WatchJobResponse_Keepalive struct {
1666+ // Keepalive: nothing to do, the stream is alive.
1667+ Keepalive *Keepalive `protobuf:"bytes,1,opt,name=keepalive,proto3,oneof"`
1668+}
1669+
1670+type WatchJobResponse_Cancel struct {
1671+ // Cancel: the job must stop.
1672+ Cancel *CancelRequest `protobuf:"bytes,2,opt,name=cancel,proto3,oneof"`
1673+}
1674+
1675+type WatchJobResponse_Ended struct {
1676+ // Ended: the job has ended. The last message of a stream: a close without
1677+ // it means the client reconnects.
1678+ Ended *JobEnded `protobuf:"bytes,3,opt,name=ended,proto3,oneof"`
1679+}
1680+
1681+func (*WatchJobResponse_Keepalive) isWatchJobResponse_Msg() {}
1682+
1683+func (*WatchJobResponse_Cancel) isWatchJobResponse_Msg() {}
1684+
1685+func (*WatchJobResponse_Ended) isWatchJobResponse_Msg() {}
1686+
1687+// Keepalive has no fields.
1688+type Keepalive struct {
1689+ state protoimpl.MessageState `protogen:"open.v1"`
1690+ unknownFields protoimpl.UnknownFields
1691+ sizeCache protoimpl.SizeCache
1692+}
1693+
1694+func (x *Keepalive) Reset() {
1695+ *x = Keepalive{}
1696+ mi := &file_ermite_runner_proto_msgTypes[18]
1697+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1698+ ms.StoreMessageInfo(mi)
1699+}
1700+
1701+func (x *Keepalive) String() string {
1702+ return protoimpl.X.MessageStringOf(x)
1703+}
1704+
1705+func (*Keepalive) ProtoMessage() {}
1706+
1707+func (x *Keepalive) ProtoReflect() protoreflect.Message {
1708+ mi := &file_ermite_runner_proto_msgTypes[18]
1709+ if x != nil {
1710+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1711+ if ms.LoadMessageInfo() == nil {
1712+ ms.StoreMessageInfo(mi)
1713+ }
1714+ return ms
1715+ }
1716+ return mi.MessageOf(x)
1717+}
1718+
1719+// Deprecated: Use Keepalive.ProtoReflect.Descriptor instead.
1720+func (*Keepalive) Descriptor() ([]byte, []int) {
1721+ return file_ermite_runner_proto_rawDescGZIP(), []int{18}
1722+}
1723+
1724+// CancelRequest asks the runner to stop a job.
1725+type CancelRequest struct {
1726+ state protoimpl.MessageState `protogen:"open.v1"`
1727+ // Reason is why the job must stop. New reasons may be added; a runner that
1728+ // does not know one cancels the job and reports CI_CONCLUSION_CANCELLED.
1729+ Reason CancelReason `protobuf:"varint,1,opt,name=reason,proto3,enum=ermite.CancelReason" json:"reason,omitempty"`
1730+ // Message is a short human-readable explanation, for the job log, for
1731+ // example "Canceled by the owner". At most 1,024 characters.
1732+ Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"`
1733+ // RequestedAt is when the cancellation was requested, by the clock of the
1734+ // server.
1735+ RequestedAt *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=requested_at,json=requestedAt,proto3" json:"requested_at,omitempty"`
1736+ unknownFields protoimpl.UnknownFields
1737+ sizeCache protoimpl.SizeCache
1738+}
1739+
1740+func (x *CancelRequest) Reset() {
1741+ *x = CancelRequest{}
1742+ mi := &file_ermite_runner_proto_msgTypes[19]
1743+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1744+ ms.StoreMessageInfo(mi)
1745+}
1746+
1747+func (x *CancelRequest) String() string {
1748+ return protoimpl.X.MessageStringOf(x)
1749+}
1750+
1751+func (*CancelRequest) ProtoMessage() {}
1752+
1753+func (x *CancelRequest) ProtoReflect() protoreflect.Message {
1754+ mi := &file_ermite_runner_proto_msgTypes[19]
1755+ if x != nil {
1756+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
1757+ if ms.LoadMessageInfo() == nil {
1758+ ms.StoreMessageInfo(mi)
1759+ }
1760+ return ms
1761+ }
1762+ return mi.MessageOf(x)
1763+}
1764+
1765+// Deprecated: Use CancelRequest.ProtoReflect.Descriptor instead.
1766+func (*CancelRequest) Descriptor() ([]byte, []int) {
1767+ return file_ermite_runner_proto_rawDescGZIP(), []int{19}
1768+}
1769+
1770+func (x *CancelRequest) GetReason() CancelReason {
1388 if x != nil {1771 if x != nil {
1389 return x.Reason1772 return x.Reason
1390 }1773 }
1391 return CancelReason_CANCEL_REASON_UNSPECIFIED1774 return CancelReason_CANCEL_REASON_UNSPECIFIED
1392 }1775 }
1393 1776
1394-func (x *WatchJobResponse) GetMessage() string {1777+func (x *CancelRequest) GetMessage() string {
1395 if x != nil {1778 if x != nil {
1396 return x.Message1779 return x.Message
1397 }1780 }
1398 return ""1781 return ""
1399 }1782 }
1400 1783
1401-func (x *WatchJobResponse) GetRequestedAt() *timestamppb.Timestamp {1784+func (x *CancelRequest) GetRequestedAt() *timestamppb.Timestamp {
1402 if x != nil {1785 if x != nil {
1403 return x.RequestedAt1786 return x.RequestedAt
1404 }1787 }
@@ -1409,21 +1792,25 @@ var File_ermite_runner_proto protoreflect.FileDescriptor
1409 1792
1410 const file_ermite_runner_proto_rawDesc = "" +1793 const file_ermite_runner_proto_rawDesc = "" +
1411 "\n" +1794 "\n" +
1412- "\x13ermite/runner.proto\x12\x06ermite\x1a\x1fgoogle/protobuf/timestamp.proto\"\xb8\x01\n" +1795+ "\x13ermite/runner.proto\x12\x06ermite\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xd2\x01\n" +
1413 "\x15RegisterRunnerRequest\x12\x12\n" +1796 "\x15RegisterRunnerRequest\x12\x12\n" +
1414 "\x04name\x18\x01 \x01(\tR\x04name\x12\x16\n" +1797 "\x04name\x18\x01 \x01(\tR\x04name\x12\x16\n" +
1415 "\x06labels\x18\x02 \x03(\tR\x06labels\x12\x18\n" +1798 "\x06labels\x18\x02 \x03(\tR\x06labels\x12\x18\n" +
1416 "\aversion\x18\x03 \x01(\tR\aversion\x125\n" +1799 "\aversion\x18\x03 \x01(\tR\aversion\x125\n" +
1417 "\tisolation\x18\x04 \x01(\x0e2\x17.ermite.RunnerIsolationR\tisolation\x12\x12\n" +1800 "\tisolation\x18\x04 \x01(\x0e2\x17.ermite.RunnerIsolationR\tisolation\x12\x12\n" +
1418 "\x04arch\x18\x05 \x01(\tR\x04arch\x12\x0e\n" +1801 "\x04arch\x18\x05 \x01(\tR\x04arch\x12\x0e\n" +
1419- "\x02os\x18\x06 \x01(\tR\x02os\"X\n" +1802+ "\x02os\x18\x06 \x01(\tR\x02os\x12\x18\n" +
1803+ "\areplace\x18\a \x01(\bR\areplace\"X\n" +
1420 "\x16RegisterRunnerResponse\x12\x1b\n" +1804 "\x16RegisterRunnerResponse\x12\x1b\n" +
1421 "\trunner_id\x18\x01 \x01(\tR\brunnerId\x12!\n" +1805 "\trunner_id\x18\x01 \x01(\tR\brunnerId\x12!\n" +
1422- "\frunner_token\x18\x02 \x01(\tR\vrunnerToken\")\n" +1806+ "\frunner_token\x18\x02 \x01(\tR\vrunnerToken\"B\n" +
1423 "\x0fFetchJobRequest\x12\x16\n" +1807 "\x0fFetchJobRequest\x12\x16\n" +
1424- "\x06labels\x18\x01 \x03(\tR\x06labels\"1\n" +1808+ "\x06labels\x18\x01 \x03(\tR\x06labels\x12\x17\n" +
1809+ "\apoll_id\x18\x02 \x01(\tR\x06pollId\"m\n" +
1425 "\x10FetchJobResponse\x12\x1d\n" +1810 "\x10FetchJobResponse\x12\x1d\n" +
1426- "\x03job\x18\x01 \x01(\v2\v.ermite.JobR\x03job\"\xfa\x02\n" +1811+ "\x03job\x18\x01 \x01(\v2\v.ermite.JobR\x03job\x12:\n" +
1812+ "\vretry_after\x18\x02 \x01(\v2\x19.google.protobuf.DurationR\n" +
1813+ "retryAfter\"\xfa\x02\n" +
1427 "\x03Job\x12\x15\n" +1814 "\x03Job\x12\x15\n" +
1428 "\x06job_id\x18\x01 \x01(\tR\x05jobId\x12\x15\n" +1815 "\x06job_id\x18\x01 \x01(\tR\x05jobId\x12\x15\n" +
1429 "\x06run_id\x18\x02 \x01(\x03R\x05runId\x12\x18\n" +1816 "\x06run_id\x18\x02 \x01(\x03R\x05runId\x12\x18\n" +
@@ -1439,18 +1826,10 @@ const file_ermite_runner_proto_rawDesc = "" +
1439 "\x0ftimeout_minutes\x18\v \x01(\x05R\x0etimeoutMinutes\x12\x12\n" +1826 "\x0ftimeout_minutes\x18\v \x01(\x05R\x0etimeoutMinutes\x12\x12\n" +
1440 "\x04repo\x18\f \x01(\tR\x04repo\x12\x10\n" +1827 "\x04repo\x18\f \x01(\tR\x04repo\x12\x10\n" +
1441 "\x03sha\x18\r \x01(\tR\x03sha\x12\x10\n" +1828 "\x03sha\x18\r \x01(\tR\x03sha\x12\x10\n" +
1442- "\x03ref\x18\x0e \x01(\tR\x03ref\"\xaf\x02\n" +1829+ "\x03ref\x18\x0e \x01(\tR\x03ref\"S\n" +
1443 "\x10UpdateJobRequest\x12\x15\n" +1830 "\x10UpdateJobRequest\x12\x15\n" +
1444 "\x06job_id\x18\x01 \x01(\tR\x05jobId\x12(\n" +1831 "\x06job_id\x18\x01 \x01(\tR\x05jobId\x12(\n" +
1445- "\x05steps\x18\x02 \x03(\v2\x12.ermite.StepUpdateR\x05steps\x12?\n" +1832+ "\x05steps\x18\x02 \x03(\v2\x12.ermite.StepUpdateR\x05steps\"\x91\x02\n" +
1446- "\aoutputs\x18\x03 \x03(\v2%.ermite.UpdateJobRequest.OutputsEntryR\aoutputs\x124\n" +
1447- "\n" +
1448- "conclusion\x18\x04 \x01(\x0e2\x14.ermite.CiConclusionR\n" +
1449- "conclusion\x12'\n" +
1450- "\x0frunner_shutdown\x18\x05 \x01(\bR\x0erunnerShutdown\x1a:\n" +
1451- "\fOutputsEntry\x12\x10\n" +
1452- "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
1453- "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\x91\x02\n" +
1454 "\n" +1833 "\n" +
1455 "StepUpdate\x12\x16\n" +1834 "StepUpdate\x12\x16\n" +
1456 "\x06number\x18\x01 \x01(\x05R\x06number\x12\x12\n" +1835 "\x06number\x18\x01 \x01(\x05R\x06number\x12\x12\n" +
@@ -1462,15 +1841,38 @@ const file_ermite_runner_proto_rawDesc = "" +
1462 "\n" +1841 "\n" +
1463 "started_at\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampR\tstartedAt\x12=\n" +1842 "started_at\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampR\tstartedAt\x12=\n" +
1464 "\fcompleted_at\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampR\vcompletedAt\"\x13\n" +1843 "\fcompleted_at\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampR\vcompletedAt\"\x13\n" +
1465- "\x11UpdateJobResponse\"|\n" +1844+ "\x11UpdateJobResponse\"\xb3\x02\n" +
1845+ "\x12CompleteJobRequest\x12\x15\n" +
1846+ "\x06job_id\x18\x01 \x01(\tR\x05jobId\x124\n" +
1847+ "\n" +
1848+ "conclusion\x18\x02 \x01(\x0e2\x14.ermite.CiConclusionR\n" +
1849+ "conclusion\x12A\n" +
1850+ "\aoutputs\x18\x03 \x03(\v2'.ermite.CompleteJobRequest.OutputsEntryR\aoutputs\x12'\n" +
1851+ "\x0frunner_shutdown\x18\x04 \x01(\bR\x0erunnerShutdown\x12(\n" +
1852+ "\x05steps\x18\x05 \x03(\v2\x12.ermite.StepUpdateR\x05steps\x1a:\n" +
1853+ "\fOutputsEntry\x12\x10\n" +
1854+ "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
1855+ "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"{\n" +
1856+ "\x13CompleteJobResponse\x124\n" +
1857+ "\n" +
1858+ "conclusion\x18\x01 \x01(\x0e2\x14.ermite.CiConclusionR\n" +
1859+ "conclusion\x12\x14\n" +
1860+ "\x05error\x18\x02 \x01(\tR\x05error\x12\x18\n" +
1861+ "\aapplied\x18\x03 \x01(\bR\aapplied\"V\n" +
1862+ "\bJobEnded\x124\n" +
1863+ "\n" +
1864+ "conclusion\x18\x01 \x01(\x0e2\x14.ermite.CiConclusionR\n" +
1865+ "conclusion\x12\x14\n" +
1866+ "\x05error\x18\x02 \x01(\tR\x05error\"|\n" +
1466 "\x10AppendLogRequest\x12\x15\n" +1867 "\x10AppendLogRequest\x12\x15\n" +
1467 "\x06job_id\x18\x01 \x01(\tR\x05jobId\x12\x12\n" +1868 "\x06job_id\x18\x01 \x01(\tR\x05jobId\x12\x12\n" +
1468 "\x04step\x18\x02 \x01(\x05R\x04step\x12\x16\n" +1869 "\x04step\x18\x02 \x01(\x05R\x04step\x12\x16\n" +
1469 "\x06offset\x18\x03 \x01(\x03R\x06offset\x12%\n" +1870 "\x06offset\x18\x03 \x01(\x03R\x06offset\x12%\n" +
1470- "\x05lines\x18\x04 \x03(\v2\x0f.ermite.LogLineR\x05lines\"M\n" +1871+ "\x05lines\x18\x04 \x03(\v2\x0f.ermite.LogLineR\x05lines\"k\n" +
1471 "\aLogLine\x12.\n" +1872 "\aLogLine\x12.\n" +
1472 "\x04time\x18\x01 \x01(\v2\x1a.google.protobuf.TimestampR\x04time\x12\x12\n" +1873 "\x04time\x18\x01 \x01(\v2\x1a.google.protobuf.TimestampR\x04time\x12\x12\n" +
1473- "\x04text\x18\x02 \x01(\tR\x04text\"4\n" +1874+ "\x04text\x18\x02 \x01(\tR\x04text\x12\x1c\n" +
1875+ "\tcontinued\x18\x03 \x01(\bR\tcontinued\"4\n" +
1474 "\x11AppendLogResponse\x12\x1f\n" +1876 "\x11AppendLogResponse\x12\x1f\n" +
1475 "\vnext_offset\x18\x01 \x01(\x03R\n" +1877 "\vnext_offset\x18\x01 \x01(\x03R\n" +
1476 "nextOffset\")\n" +1878 "nextOffset\")\n" +
@@ -1478,8 +1880,14 @@ const file_ermite_runner_proto_rawDesc = "" +
1478 "\x06job_id\x18\x01 \x01(\tR\x05jobId\"\x13\n" +1880 "\x06job_id\x18\x01 \x01(\tR\x05jobId\"\x13\n" +
1479 "\x11HeartbeatResponse\"(\n" +1881 "\x11HeartbeatResponse\"(\n" +
1480 "\x0fWatchJobRequest\x12\x15\n" +1882 "\x0fWatchJobRequest\x12\x15\n" +
1481- "\x06job_id\x18\x01 \x01(\tR\x05jobId\"\x99\x01\n" +1883+ "\x06job_id\x18\x01 \x01(\tR\x05jobId\"\xa7\x01\n" +
1482- "\x10WatchJobResponse\x12,\n" +1884+ "\x10WatchJobResponse\x121\n" +
1885+ "\tkeepalive\x18\x01 \x01(\v2\x11.ermite.KeepaliveH\x00R\tkeepalive\x12/\n" +
1886+ "\x06cancel\x18\x02 \x01(\v2\x15.ermite.CancelRequestH\x00R\x06cancel\x12(\n" +
1887+ "\x05ended\x18\x03 \x01(\v2\x10.ermite.JobEndedH\x00R\x05endedB\x05\n" +
1888+ "\x03msg\"\v\n" +
1889+ "\tKeepalive\"\x96\x01\n" +
1890+ "\rCancelRequest\x12,\n" +
1483 "\x06reason\x18\x01 \x01(\x0e2\x14.ermite.CancelReasonR\x06reason\x12\x18\n" +1891 "\x06reason\x18\x01 \x01(\x0e2\x14.ermite.CancelReasonR\x06reason\x12\x18\n" +
1484 "\amessage\x18\x02 \x01(\tR\amessage\x12=\n" +1892 "\amessage\x18\x02 \x01(\tR\amessage\x12=\n" +
1485 "\frequested_at\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\vrequestedAt*o\n" +1893 "\frequested_at\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\vrequestedAt*o\n" +
@@ -1511,14 +1919,15 @@ const file_ermite_runner_proto_rawDesc = "" +
1511 "\x19CANCEL_REASON_UNSPECIFIED\x10\x00\x12\x16\n" +1919 "\x19CANCEL_REASON_UNSPECIFIED\x10\x00\x12\x16\n" +
1512 "\x12CANCEL_REASON_USER\x10\x01\x12\x1b\n" +1920 "\x12CANCEL_REASON_USER\x10\x01\x12\x1b\n" +
1513 "\x17CANCEL_REASON_FAIL_FAST\x10\x02\x12\x19\n" +1921 "\x17CANCEL_REASON_FAIL_FAST\x10\x02\x12\x19\n" +
1514- "\x15CANCEL_REASON_TIMEOUT\x10\x032\xa6\x03\n" +1922+ "\x15CANCEL_REASON_TIMEOUT\x10\x032\xee\x03\n" +
1515 "\rRunnerService\x12O\n" +1923 "\rRunnerService\x12O\n" +
1516 "\x0eRegisterRunner\x12\x1d.ermite.RegisterRunnerRequest\x1a\x1e.ermite.RegisterRunnerResponse\x12=\n" +1924 "\x0eRegisterRunner\x12\x1d.ermite.RegisterRunnerRequest\x1a\x1e.ermite.RegisterRunnerResponse\x12=\n" +
1517 "\bFetchJob\x12\x17.ermite.FetchJobRequest\x1a\x18.ermite.FetchJobResponse\x12@\n" +1925 "\bFetchJob\x12\x17.ermite.FetchJobRequest\x1a\x18.ermite.FetchJobResponse\x12@\n" +
1518 "\tUpdateJob\x12\x18.ermite.UpdateJobRequest\x1a\x19.ermite.UpdateJobResponse\x12@\n" +1926 "\tUpdateJob\x12\x18.ermite.UpdateJobRequest\x1a\x19.ermite.UpdateJobResponse\x12@\n" +
1519 "\tAppendLog\x12\x18.ermite.AppendLogRequest\x1a\x19.ermite.AppendLogResponse\x12@\n" +1927 "\tAppendLog\x12\x18.ermite.AppendLogRequest\x1a\x19.ermite.AppendLogResponse\x12@\n" +
1520 "\tHeartbeat\x12\x18.ermite.HeartbeatRequest\x1a\x19.ermite.HeartbeatResponse\x12?\n" +1928 "\tHeartbeat\x12\x18.ermite.HeartbeatRequest\x1a\x19.ermite.HeartbeatResponse\x12?\n" +
1521- "\bWatchJob\x12\x17.ermite.WatchJobRequest\x1a\x18.ermite.WatchJobResponse0\x01Br\n" +1929+ "\bWatchJob\x12\x17.ermite.WatchJobRequest\x1a\x18.ermite.WatchJobResponse0\x01\x12F\n" +
1930+ "\vCompleteJob\x12\x1a.ermite.CompleteJobRequest\x1a\x1b.ermite.CompleteJobResponseBr\n" +
1522 "\n" +1931 "\n" +
1523 "com.ermiteB\vRunnerProtoP\x01Z\x1fbnema.dev/ermite-api/gen/ermite\xa2\x02\x03EXX\xaa\x02\x06Ermite\xca\x02\x06Ermite\xe2\x02\x12Ermite\\GPBMetadata\xea\x02\x06Ermiteb\x06proto3"1932 "com.ermiteB\vRunnerProtoP\x01Z\x1fbnema.dev/ermite-api/gen/ermite\xa2\x02\x03EXX\xaa\x02\x06Ermite\xca\x02\x06Ermite\xe2\x02\x12Ermite\\GPBMetadata\xea\x02\x06Ermiteb\x06proto3"
1524 1933
@@ -1535,7 +1944,7 @@ func file_ermite_runner_proto_rawDescGZIP() []byte {
1535 }1944 }
1536 1945
1537 var file_ermite_runner_proto_enumTypes = make([]protoimpl.EnumInfo, 5)1946 var file_ermite_runner_proto_enumTypes = make([]protoimpl.EnumInfo, 5)
1538-var file_ermite_runner_proto_msgTypes = make([]protoimpl.MessageInfo, 16)1947+var file_ermite_runner_proto_msgTypes = make([]protoimpl.MessageInfo, 21)
1539 var file_ermite_runner_proto_goTypes = []any{1948 var file_ermite_runner_proto_goTypes = []any{
1540 (CiStatus)(0), // 0: ermite.CiStatus1949 (CiStatus)(0), // 0: ermite.CiStatus
1541 (CiConclusion)(0), // 1: ermite.CiConclusion1950 (CiConclusion)(0), // 1: ermite.CiConclusion
@@ -1550,47 +1959,62 @@ var file_ermite_runner_proto_goTypes = []any{
1550 (*UpdateJobRequest)(nil), // 10: ermite.UpdateJobRequest1959 (*UpdateJobRequest)(nil), // 10: ermite.UpdateJobRequest
1551 (*StepUpdate)(nil), // 11: ermite.StepUpdate1960 (*StepUpdate)(nil), // 11: ermite.StepUpdate
1552 (*UpdateJobResponse)(nil), // 12: ermite.UpdateJobResponse1961 (*UpdateJobResponse)(nil), // 12: ermite.UpdateJobResponse
1553- (*AppendLogRequest)(nil), // 13: ermite.AppendLogRequest1962+ (*CompleteJobRequest)(nil), // 13: ermite.CompleteJobRequest
1554- (*LogLine)(nil), // 14: ermite.LogLine1963+ (*CompleteJobResponse)(nil), // 14: ermite.CompleteJobResponse
1555- (*AppendLogResponse)(nil), // 15: ermite.AppendLogResponse1964+ (*JobEnded)(nil), // 15: ermite.JobEnded
1556- (*HeartbeatRequest)(nil), // 16: ermite.HeartbeatRequest1965+ (*AppendLogRequest)(nil), // 16: ermite.AppendLogRequest
1557- (*HeartbeatResponse)(nil), // 17: ermite.HeartbeatResponse1966+ (*LogLine)(nil), // 17: ermite.LogLine
1558- (*WatchJobRequest)(nil), // 18: ermite.WatchJobRequest1967+ (*AppendLogResponse)(nil), // 18: ermite.AppendLogResponse
1559- (*WatchJobResponse)(nil), // 19: ermite.WatchJobResponse1968+ (*HeartbeatRequest)(nil), // 19: ermite.HeartbeatRequest
1560- nil, // 20: ermite.UpdateJobRequest.OutputsEntry1969+ (*HeartbeatResponse)(nil), // 20: ermite.HeartbeatResponse
1561- (*timestamppb.Timestamp)(nil), // 21: google.protobuf.Timestamp1970+ (*WatchJobRequest)(nil), // 21: ermite.WatchJobRequest
1971+ (*WatchJobResponse)(nil), // 22: ermite.WatchJobResponse
1972+ (*Keepalive)(nil), // 23: ermite.Keepalive
1973+ (*CancelRequest)(nil), // 24: ermite.CancelRequest
1974+ nil, // 25: ermite.CompleteJobRequest.OutputsEntry
1975+ (*durationpb.Duration)(nil), // 26: google.protobuf.Duration
1976+ (*timestamppb.Timestamp)(nil), // 27: google.protobuf.Timestamp
1562 }1977 }
1563 var file_ermite_runner_proto_depIdxs = []int32{1978 var file_ermite_runner_proto_depIdxs = []int32{
1564 3, // 0: ermite.RegisterRunnerRequest.isolation:type_name -> ermite.RunnerIsolation1979 3, // 0: ermite.RegisterRunnerRequest.isolation:type_name -> ermite.RunnerIsolation
1565 9, // 1: ermite.FetchJobResponse.job:type_name -> ermite.Job1980 9, // 1: ermite.FetchJobResponse.job:type_name -> ermite.Job
1566- 11, // 2: ermite.UpdateJobRequest.steps:type_name -> ermite.StepUpdate1981+ 26, // 2: ermite.FetchJobResponse.retry_after:type_name -> google.protobuf.Duration
1567- 20, // 3: ermite.UpdateJobRequest.outputs:type_name -> ermite.UpdateJobRequest.OutputsEntry1982+ 11, // 3: ermite.UpdateJobRequest.steps:type_name -> ermite.StepUpdate
1568- 1, // 4: ermite.UpdateJobRequest.conclusion:type_name -> ermite.CiConclusion1983+ 2, // 4: ermite.StepUpdate.state:type_name -> ermite.StepState
1569- 2, // 5: ermite.StepUpdate.state:type_name -> ermite.StepState1984+ 1, // 5: ermite.StepUpdate.conclusion:type_name -> ermite.CiConclusion
1570- 1, // 6: ermite.StepUpdate.conclusion:type_name -> ermite.CiConclusion1985+ 27, // 6: ermite.StepUpdate.started_at:type_name -> google.protobuf.Timestamp
1571- 21, // 7: ermite.StepUpdate.started_at:type_name -> google.protobuf.Timestamp1986+ 27, // 7: ermite.StepUpdate.completed_at:type_name -> google.protobuf.Timestamp
1572- 21, // 8: ermite.StepUpdate.completed_at:type_name -> google.protobuf.Timestamp1987+ 1, // 8: ermite.CompleteJobRequest.conclusion:type_name -> ermite.CiConclusion
1573- 14, // 9: ermite.AppendLogRequest.lines:type_name -> ermite.LogLine1988+ 25, // 9: ermite.CompleteJobRequest.outputs:type_name -> ermite.CompleteJobRequest.OutputsEntry
1574- 21, // 10: ermite.LogLine.time:type_name -> google.protobuf.Timestamp1989+ 11, // 10: ermite.CompleteJobRequest.steps:type_name -> ermite.StepUpdate
1575- 4, // 11: ermite.WatchJobResponse.reason:type_name -> ermite.CancelReason1990+ 1, // 11: ermite.CompleteJobResponse.conclusion:type_name -> ermite.CiConclusion
1576- 21, // 12: ermite.WatchJobResponse.requested_at:type_name -> google.protobuf.Timestamp1991+ 1, // 12: ermite.JobEnded.conclusion:type_name -> ermite.CiConclusion
1577- 5, // 13: ermite.RunnerService.RegisterRunner:input_type -> ermite.RegisterRunnerRequest1992+ 17, // 13: ermite.AppendLogRequest.lines:type_name -> ermite.LogLine
1578- 7, // 14: ermite.RunnerService.FetchJob:input_type -> ermite.FetchJobRequest1993+ 27, // 14: ermite.LogLine.time:type_name -> google.protobuf.Timestamp
1579- 10, // 15: ermite.RunnerService.UpdateJob:input_type -> ermite.UpdateJobRequest1994+ 23, // 15: ermite.WatchJobResponse.keepalive:type_name -> ermite.Keepalive
1580- 13, // 16: ermite.RunnerService.AppendLog:input_type -> ermite.AppendLogRequest1995+ 24, // 16: ermite.WatchJobResponse.cancel:type_name -> ermite.CancelRequest
1581- 16, // 17: ermite.RunnerService.Heartbeat:input_type -> ermite.HeartbeatRequest1996+ 15, // 17: ermite.WatchJobResponse.ended:type_name -> ermite.JobEnded
1582- 18, // 18: ermite.RunnerService.WatchJob:input_type -> ermite.WatchJobRequest1997+ 4, // 18: ermite.CancelRequest.reason:type_name -> ermite.CancelReason
1583- 6, // 19: ermite.RunnerService.RegisterRunner:output_type -> ermite.RegisterRunnerResponse1998+ 27, // 19: ermite.CancelRequest.requested_at:type_name -> google.protobuf.Timestamp
1584- 8, // 20: ermite.RunnerService.FetchJob:output_type -> ermite.FetchJobResponse1999+ 5, // 20: ermite.RunnerService.RegisterRunner:input_type -> ermite.RegisterRunnerRequest
1585- 12, // 21: ermite.RunnerService.UpdateJob:output_type -> ermite.UpdateJobResponse2000+ 7, // 21: ermite.RunnerService.FetchJob:input_type -> ermite.FetchJobRequest
1586- 15, // 22: ermite.RunnerService.AppendLog:output_type -> ermite.AppendLogResponse2001+ 10, // 22: ermite.RunnerService.UpdateJob:input_type -> ermite.UpdateJobRequest
1587- 17, // 23: ermite.RunnerService.Heartbeat:output_type -> ermite.HeartbeatResponse2002+ 16, // 23: ermite.RunnerService.AppendLog:input_type -> ermite.AppendLogRequest
1588- 19, // 24: ermite.RunnerService.WatchJob:output_type -> ermite.WatchJobResponse2003+ 19, // 24: ermite.RunnerService.Heartbeat:input_type -> ermite.HeartbeatRequest
1589- 19, // [19:25] is the sub-list for method output_type2004+ 21, // 25: ermite.RunnerService.WatchJob:input_type -> ermite.WatchJobRequest
1590- 13, // [13:19] is the sub-list for method input_type2005+ 13, // 26: ermite.RunnerService.CompleteJob:input_type -> ermite.CompleteJobRequest
1591- 13, // [13:13] is the sub-list for extension type_name2006+ 6, // 27: ermite.RunnerService.RegisterRunner:output_type -> ermite.RegisterRunnerResponse
1592- 13, // [13:13] is the sub-list for extension extendee2007+ 8, // 28: ermite.RunnerService.FetchJob:output_type -> ermite.FetchJobResponse
1593- 0, // [0:13] is the sub-list for field type_name2008+ 12, // 29: ermite.RunnerService.UpdateJob:output_type -> ermite.UpdateJobResponse
2009+ 18, // 30: ermite.RunnerService.AppendLog:output_type -> ermite.AppendLogResponse
2010+ 20, // 31: ermite.RunnerService.Heartbeat:output_type -> ermite.HeartbeatResponse
2011+ 22, // 32: ermite.RunnerService.WatchJob:output_type -> ermite.WatchJobResponse
2012+ 14, // 33: ermite.RunnerService.CompleteJob:output_type -> ermite.CompleteJobResponse
2013+ 27, // [27:34] is the sub-list for method output_type
2014+ 20, // [20:27] is the sub-list for method input_type
2015+ 20, // [20:20] is the sub-list for extension type_name
2016+ 20, // [20:20] is the sub-list for extension extendee
2017+ 0, // [0:20] is the sub-list for field type_name
1594 }2018 }
1595 2019
1596 func init() { file_ermite_runner_proto_init() }2020 func init() { file_ermite_runner_proto_init() }
@@ -1598,13 +2022,18 @@ func file_ermite_runner_proto_init() {
1598 if File_ermite_runner_proto != nil {2022 if File_ermite_runner_proto != nil {
1599 return2023 return
1600 }2024 }
2025+ file_ermite_runner_proto_msgTypes[17].OneofWrappers = []any{
2026+ (*WatchJobResponse_Keepalive)(nil),
2027+ (*WatchJobResponse_Cancel)(nil),
2028+ (*WatchJobResponse_Ended)(nil),
2029+ }
1601 type x struct{}2030 type x struct{}
1602 out := protoimpl.TypeBuilder{2031 out := protoimpl.TypeBuilder{
1603 File: protoimpl.DescBuilder{2032 File: protoimpl.DescBuilder{
1604 GoPackagePath: reflect.TypeOf(x{}).PkgPath(),2033 GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
1605 RawDescriptor: unsafe.Slice(unsafe.StringData(file_ermite_runner_proto_rawDesc), len(file_ermite_runner_proto_rawDesc)),2034 RawDescriptor: unsafe.Slice(unsafe.StringData(file_ermite_runner_proto_rawDesc), len(file_ermite_runner_proto_rawDesc)),
1606 NumEnums: 5,2035 NumEnums: 5,
1607- NumMessages: 16,2036+ NumMessages: 21,
1608 NumExtensions: 0,2037 NumExtensions: 0,
1609 NumServices: 1,2038 NumServices: 1,
1610 },2039 },
M gen_test.go +105 -14
@@ -3,8 +3,11 @@
33 package api_test
44
55 import (
6+ "errors"
67 "testing"
78
9+ "connectrpc.com/connect"
10+
811 "google.golang.org/protobuf/proto"
912 "google.golang.org/protobuf/reflect/protoreflect"
1013 "google.golang.org/protobuf/types/known/fieldmaskpb"
@@ -50,6 +53,7 @@ func TestProcedures(t *testing.T) {
5053 {ermiteconnect.RunnerServiceAppendLogProcedure, "/ermite.RunnerService/AppendLog"},
5154 {ermiteconnect.RunnerServiceHeartbeatProcedure, "/ermite.RunnerService/Heartbeat"},
5255 {ermiteconnect.RunnerServiceWatchJobProcedure, "/ermite.RunnerService/WatchJob"},
56+ {ermiteconnect.RunnerServiceCompleteJobProcedure, "/ermite.RunnerService/CompleteJob"},
5357 {ermiteconnect.RunServiceListRunsProcedure, "/ermite.RunService/ListRuns"},
5458 {ermiteconnect.RunServiceGetRunProcedure, "/ermite.RunService/GetRun"},
5559 {ermiteconnect.RunServiceGetJobProcedure, "/ermite.RunService/GetJob"},
@@ -87,20 +91,40 @@ func TestEnumNumbers(t *testing.T) {
8791 if got := ermite.DiffSide_DIFF_SIDE_NEW; got != 2 {
8892 t.Errorf("DIFF_SIDE_NEW = %d, want 2", got)
8993 }
90- if got := ermite.CiStatus_CI_STATUS_COMPLETED; got != 3 {
91- t.Errorf("RUN_STATUS_COMPLETED = %d, want 3", got)
92- }
93- if got := ermite.CiConclusion_CI_CONCLUSION_ACTION_REQUIRED; got != 7 {
94- t.Errorf("CONCLUSION_ACTION_REQUIRED = %d, want 7", got)
95- }
96- if got := ermite.StepState_STEP_STATE_COMPLETED; got != 3 {
97- t.Errorf("STEP_STATE_COMPLETED = %d, want 3", got)
98- }
99- if got := ermite.RunnerIsolation_RUNNER_ISOLATION_CONTAINER_PRIVILEGED; got != 3 {
100- t.Errorf("RUNNER_ISOLATION_CONTAINER_PRIVILEGED = %d, want 3", got)
101- }
102- if got := ermite.CancelReason_CANCEL_REASON_TIMEOUT; got != 3 {
103- t.Errorf("CANCEL_REASON_TIMEOUT = %d, want 3", got)
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+ }
104128 }
105129 }
106130
@@ -238,3 +262,70 @@ func TestJobRoundTrip(t *testing.T) {
238262 t.Errorf("round trip mismatch: %v != %v", &out, in)
239263 }
240264 }
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,8 +3,11 @@
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"
9 "google.golang.org/protobuf/reflect/protoreflect"12 "google.golang.org/protobuf/reflect/protoreflect"
10 "google.golang.org/protobuf/types/known/fieldmaskpb"13 "google.golang.org/protobuf/types/known/fieldmaskpb"
@@ -50,6 +53,7 @@ func TestProcedures(t *testing.T) {
50 {ermiteconnect.RunnerServiceAppendLogProcedure, "/ermite.RunnerService/AppendLog"},53 {ermiteconnect.RunnerServiceAppendLogProcedure, "/ermite.RunnerService/AppendLog"},
51 {ermiteconnect.RunnerServiceHeartbeatProcedure, "/ermite.RunnerService/Heartbeat"},54 {ermiteconnect.RunnerServiceHeartbeatProcedure, "/ermite.RunnerService/Heartbeat"},
52 {ermiteconnect.RunnerServiceWatchJobProcedure, "/ermite.RunnerService/WatchJob"},55 {ermiteconnect.RunnerServiceWatchJobProcedure, "/ermite.RunnerService/WatchJob"},
56+ {ermiteconnect.RunnerServiceCompleteJobProcedure, "/ermite.RunnerService/CompleteJob"},
53 {ermiteconnect.RunServiceListRunsProcedure, "/ermite.RunService/ListRuns"},57 {ermiteconnect.RunServiceListRunsProcedure, "/ermite.RunService/ListRuns"},
54 {ermiteconnect.RunServiceGetRunProcedure, "/ermite.RunService/GetRun"},58 {ermiteconnect.RunServiceGetRunProcedure, "/ermite.RunService/GetRun"},
55 {ermiteconnect.RunServiceGetJobProcedure, "/ermite.RunService/GetJob"},59 {ermiteconnect.RunServiceGetJobProcedure, "/ermite.RunService/GetJob"},
@@ -87,20 +91,40 @@ func TestEnumNumbers(t *testing.T) {
87 if got := ermite.DiffSide_DIFF_SIDE_NEW; got != 2 {91 if got := ermite.DiffSide_DIFF_SIDE_NEW; got != 2 {
88 t.Errorf("DIFF_SIDE_NEW = %d, want 2", got)92 t.Errorf("DIFF_SIDE_NEW = %d, want 2", got)
89 }93 }
90- if got := ermite.CiStatus_CI_STATUS_COMPLETED; got != 3 {94+ enums := []struct {
91- t.Errorf("RUN_STATUS_COMPLETED = %d, want 3", got)95+ name string
92- }96+ got int32
93- if got := ermite.CiConclusion_CI_CONCLUSION_ACTION_REQUIRED; got != 7 {97+ want int32
94- t.Errorf("CONCLUSION_ACTION_REQUIRED = %d, want 7", got)98+ }{
95- }99+ {"CI_STATUS_UNSPECIFIED", int32(ermite.CiStatus_CI_STATUS_UNSPECIFIED), 0},
96- if got := ermite.StepState_STEP_STATE_COMPLETED; got != 3 {100+ {"CI_STATUS_QUEUED", int32(ermite.CiStatus_CI_STATUS_QUEUED), 1},
97- t.Errorf("STEP_STATE_COMPLETED = %d, want 3", got)101+ {"CI_STATUS_IN_PROGRESS", int32(ermite.CiStatus_CI_STATUS_IN_PROGRESS), 2},
98- }102+ {"CI_STATUS_COMPLETED", int32(ermite.CiStatus_CI_STATUS_COMPLETED), 3},
99- if got := ermite.RunnerIsolation_RUNNER_ISOLATION_CONTAINER_PRIVILEGED; got != 3 {103+ {"CI_CONCLUSION_UNSPECIFIED", int32(ermite.CiConclusion_CI_CONCLUSION_UNSPECIFIED), 0},
100- t.Errorf("RUNNER_ISOLATION_CONTAINER_PRIVILEGED = %d, want 3", got)104+ {"CI_CONCLUSION_SUCCESS", int32(ermite.CiConclusion_CI_CONCLUSION_SUCCESS), 1},
101- }105+ {"CI_CONCLUSION_FAILURE", int32(ermite.CiConclusion_CI_CONCLUSION_FAILURE), 2},
102- if got := ermite.CancelReason_CANCEL_REASON_TIMEOUT; got != 3 {106+ {"CI_CONCLUSION_CANCELLED", int32(ermite.CiConclusion_CI_CONCLUSION_CANCELLED), 3},
103- t.Errorf("CANCEL_REASON_TIMEOUT = %d, want 3", got)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+ }
104 }128 }
105 }129 }
106 130
@@ -238,3 +262,70 @@ func TestJobRoundTrip(t *testing.T) {
238 t.Errorf("round trip mismatch: %v != %v", &out, in)262 t.Errorf("round trip mismatch: %v != %v", &out, in)
239 }263 }
240 }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 +5 -3
@@ -26,8 +26,10 @@ service EventService {
2626 // "home"), subject to the same visibility. They carry the state after the
2727 // change and are published when a run or a job is created, changes status
2828 // or conclusion, or when one of its steps starts or ends (never per log
29- // line: follow a log with RunService.GetJobLog). A job update is published
30- // before the update of its run. There is no event for a change of title or
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
3133 // description, nor for a resolved comment: after a gap, or to see such
3234 // changes, clients read the merge request again. A merge publishes
3335 // merge_request_merged, then pushed for the target branch.
@@ -171,7 +173,7 @@ message CommentAdded {
171173 // Gap tells the client that events were lost: the requested resume point is
172174 // older than the server's buffer. It has no fields. The client should reload
173175 // the state it displays (repositories, refs, merge requests and their
174-// comments) and carry on with the next events.
176+// comments, CI runs and their jobs) and carry on with the next events.
175177 message Gap {}
176178
177179 // RunUpdated reports a CI run that was created or whose status, conclusion or
@@ -26,8 +26,10 @@ service EventService {
26 // "home"), subject to the same visibility. They carry the state after the26 // "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 status27 // 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 log28 // or conclusion, or when one of its steps starts or ends (never per log
29- // line: follow a log with RunService.GetJobLog). A job update is published29+ // line: follow a log with RunService.GetJobLog). The order of these events
30- // before the update of its run. There is no event for a change of title or30+ // 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
31 // 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
32 // changes, clients read the merge request again. A merge publishes34 // changes, clients read the merge request again. A merge publishes
33 // merge_request_merged, then pushed for the target branch.35 // merge_request_merged, then pushed for the target branch.
@@ -171,7 +173,7 @@ message CommentAdded {
171 // 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
172 // 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
173 // the state it displays (repositories, refs, merge requests and their175 // the state it displays (repositories, refs, merge requests and their
174-// comments) and carry on with the next events.176+// comments, CI runs and their jobs) and carry on with the next events.
175 message Gap {}177 message Gap {}
176 178
177 // RunUpdated reports a CI run that was created or whose status, conclusion or179 // RunUpdated reports a CI run that was created or whose status, conclusion or
M proto/ermite/run.proto +16 -0
@@ -68,6 +68,11 @@ message Run {
6868 google.protobuf.Timestamp started_at = 16;
6969 // CompletedAt is when this attempt completed. Unset until then.
7070 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;
7176 }
7277
7378 // RunJob is a job of a run as people see it: the state of one job instance
@@ -127,6 +132,11 @@ message RunJob {
127132 google.protobuf.Timestamp started_at = 17;
128133 // CompletedAt is when the job completed. Unset until then.
129134 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;
130140 }
131141
132142 // Step is one step of a job, with where its lines are in the job log.
@@ -405,6 +415,12 @@ message JobLogLine {
405415 // Text is the line without its terminator, as in LogLine.text, including the
406416 // "##[group]" and "##[endgroup]" markers. Clients show it escaped.
407417 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;
408424 }
409425
410426 // CancelRunRequest names the run to cancel.
@@ -68,6 +68,11 @@ message Run {
68 google.protobuf.Timestamp started_at = 16;68 google.protobuf.Timestamp started_at = 16;
69 // CompletedAt is when this attempt completed. Unset until then.69 // CompletedAt is when this attempt completed. Unset until then.
70 google.protobuf.Timestamp completed_at = 17;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;
71 }76 }
72 77
73 // RunJob is a job of a run as people see it: the state of one job instance78 // RunJob is a job of a run as people see it: the state of one job instance
@@ -127,6 +132,11 @@ message RunJob {
127 google.protobuf.Timestamp started_at = 17;132 google.protobuf.Timestamp started_at = 17;
128 // CompletedAt is when the job completed. Unset until then.133 // CompletedAt is when the job completed. Unset until then.
129 google.protobuf.Timestamp completed_at = 18;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;
130 }140 }
131 141
132 // Step is one step of a job, with where its lines are in the job log.142 // Step is one step of a job, with where its lines are in the job log.
@@ -405,6 +415,12 @@ message JobLogLine {
405 // Text is the line without its terminator, as in LogLine.text, including the415 // Text is the line without its terminator, as in LogLine.text, including the
406 // "##[group]" and "##[endgroup]" markers. Clients show it escaped.416 // "##[group]" and "##[endgroup]" markers. Clients show it escaped.
407 string text = 3;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;
408 }424 }
409 425
410 // CancelRunRequest names the run to cancel.426 // CancelRunRequest names the run to cancel.
M proto/ermite/runner.proto +273 -101
@@ -4,6 +4,7 @@ syntax = "proto3";
44
55 package ermite;
66
7+import "google/protobuf/duration.proto";
78 import "google/protobuf/timestamp.proto";
89
910 // CiStatus is where a run, a job or a check is in its life. It is the same
@@ -79,8 +80,7 @@ enum RunnerIsolation {
7980
8081 // CancelReason says why the server asks a runner to stop a job.
8182 enum CancelReason {
82- // Not set. A WatchJobResponse with this reason is a keep-alive and carries
83- // no request.
83+ // Not set. Never sent in a CancelRequest.
8484 CANCEL_REASON_UNSPECIFIED = 0;
8585 // The owner cancelled the run (RunService.CancelRun, web or CLI). The job
8686 // ends CI_CONCLUSION_CANCELLED.
@@ -88,38 +88,55 @@ enum CancelReason {
8888 // Another job of the same matrix failed and the strategy has fail-fast. The
8989 // job ends CI_CONCLUSION_CANCELLED.
9090 CANCEL_REASON_FAIL_FAST = 2;
91- // The job exceeded its timeout_minutes without the runner ending it. The
92- // job ends CI_CONCLUSION_TIMED_OUT.
91+ // The job exceeded its time budget (see Job.timeout_minutes). The job ends
92+ // CI_CONCLUSION_TIMED_OUT.
9393 CANCEL_REASON_TIMEOUT = 3;
9494 }
9595
9696 // RunnerService is what a CI runner (ermite-runner) calls to get jobs and
9797 // report on them. It is not for people: owners use RunService.
9898 //
99-// Three credentials are used, each by different RPCs:
99+// Three credentials are used, each by different RPCs. All are sent as
100+// "Authorization: Bearer <secret>":
100101 //
101102 // - RegisterRunner: a stored token with the runner:register scope, or the
102-// root token, as "Authorization: Bearer <secret>".
103-// - FetchJob: the runner token returned by RegisterRunner, as
104-// "Authorization: Bearer <runner_token>". It identifies the runner. It
105-// only works for FetchJob.
106-// - UpdateJob, AppendLog, Heartbeat, WatchJob: the job token of the Job that
107-// FetchJob returned, as "Authorization: Bearer <job_token>". It is valid
108-// for that one job and attempt, from its assignment until it ends. The
109-// job_id of the request must be the one of the token.
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.
110112 //
111-// A request without a credential, with an unknown, expired or revoked one, or
112-// with a credential of the wrong kind for the RPC, gets unauthenticated. A
113-// job token that is valid but belongs to another job than the job_id of the
114-// request gets permission_denied. A job that has ended (concluded, cancelled,
115-// failed by the server, or whose runner was lost) no longer has a valid job
116-// token: every later call gets unauthenticated, which a runner treats as "the
117-// job is over" and stops sending anything for it. Repeated failed
118-// authentications are rate limited per client address.
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.
119120 //
120-// Runner tokens and job tokens are secrets: the server never logs them and
121-// clients must not log them. The job token stays in the runner host process; it
122-// is never given to the sandbox that runs the job's code.
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.
123140 //
124141 // Servers that implement this service list "ci.runners" in
125142 // GetVersionResponse.features.
@@ -130,37 +147,54 @@ service RunnerService {
130147 // (a runner serves every repository), or the root token. Fails with
131148 // unauthenticated without a valid token, permission_denied without the
132149 // scope or with a repository-restricted token, invalid_argument if a field
133- // breaks a rule, and already_exists if name is taken by another runner.
150+ // breaks a rule, and already_exists if name is taken by another runner and
151+ // replace is not set.
134152 rpc RegisterRunner(RegisterRunnerRequest) returns (RegisterRunnerResponse);
135153 // FetchJob asks for a job to run (long poll). The server holds the call
136154 // until a queued job matches the runner, or about 50 seconds (never more
137155 // than 50) pass, then answers; without a job the response has no job and the
138- // runner calls again at once. Clients set a call timeout above 50 seconds
139- // (60 is suggested) and must not poll when they have no free capacity.
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.
140159 //
141160 // A job matches when every label of its runs-on is among the labels of the
142161 // runner (case-insensitive). A job is given to exactly one runner, even when
143162 // many runners and many polls compete; the assignment is recorded before
144- // the response is sent, and the job is then in progress. If the response is
145- // lost the job is not offered again: with no Heartbeat for 2 minutes the
146- // server fails it as a lost runner.
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).
147184 //
148185 // Needs the runner token. Fails with unauthenticated when the runner was
149186 // removed (also ending a call in progress) and invalid_argument if labels
150- // breaks a rule.
187+ // or poll_id break a rule.
151188 rpc FetchJob(FetchJobRequest) returns (FetchJobResponse);
152- // UpdateJob reports the state of the steps of a job and, once, how the job
153- // ended. Needs the job token. Each call is applied whole or not at all, and
154- // repeating an identical call changes nothing, so a runner may retry after a
155- // network error.
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.
156193 //
157- // Fails with invalid_argument if a rule is broken (including the output
158- // limits), and with failed_precondition if a step goes back (a COMPLETED
159- // step cannot change, a step cannot go from IN_PROGRESS to PENDING).
160- // The call that carries a conclusion ends the job: its job token stops
161- // working, the server completes the job and no further call is accepted,
162- // so a retry of that last call after a lost response gets unauthenticated,
163- // which means it was applied.
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.
164198 rpc UpdateJob(UpdateJobRequest) returns (UpdateJobResponse);
165199 // AppendLog appends lines to the log of a job. Needs the job token.
166200 //
@@ -172,46 +206,102 @@ service RunnerService {
172206 // - offset equal to the server's line count: the lines are appended;
173207 // - offset below it: the first (count - offset) lines were already stored
174208 // (a retry after a lost response) and are skipped, and the rest is
175- // appended; a request entirely stored changes nothing;
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;
176213 // - offset above it: out_of_range (a gap is a bug of the client; the
177214 // message gives the line count the server holds).
178215 //
179216 // The response gives the line count after the call, so a client can check
180- // itself. A request has at most 1,000 lines whose text adds up to at most
181- // 64 KiB (65,536 bytes), all in the same step; more is invalid_argument. A
182- // line of more than 64 KiB is cut by the client into several lines.
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.
183230 //
184231 // The log of a job is limited to 512 MiB (536,870,912 bytes) as stored by
185232 // the server (text, timestamps and separators). The call that would exceed
186233 // it appends nothing; the server adds a last line saying that the limit was
187234 // reached, ends the job with CI_CONCLUSION_FAILURE and RunJob.error set, and
188- // answers resource_exhausted. The runner then stops the job and sends
189- // nothing more: its calls get unauthenticated.
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.
190240 //
191241 // Fails with invalid_argument if a rule is broken (step number, text with a
192- // line feed or invalid UTF-8, missing time).
242+ // line feed or invalid UTF-8, missing time, a continued first line).
193243 rpc AppendLog(AppendLogRequest) returns (AppendLogResponse);
194244 // Heartbeat tells the server that the runner is alive and still working on
195- // the job. Needs the job token. The runner calls it every 30 seconds for as
196- // long as the job runs, from assignment to the end. Without a heartbeat for
197- // 2 minutes the server ends the job with CI_CONCLUSION_FAILURE and the message
198- // "The self-hosted runner: <name> lost communication with the server...".
199- // Only Heartbeat counts: other calls do not keep a job alive.
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.
200257 rpc Heartbeat(HeartbeatRequest) returns (HeartbeatResponse);
201- // WatchJob streams the requests to stop a job. Needs the job token. The
258+ // WatchJob streams what the runner must react to. Needs the job token. The
202259 // runner opens it when it starts the job and keeps it open; it reopens it
203- // after any error other than unauthenticated.
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.
204263 //
205264 // When the stream opens and a cancellation has already been requested, the
206265 // first message is that request, so a reconnect never misses one. Only the
207266 // first reason is sent: a job already cancelled by the owner does not get a
208- // second message when its timeout expires. The server also sends a
209- // keep-alive message (reason CANCEL_REASON_UNSPECIFIED) at least every 30
210- // seconds, which the client ignores. The stream ends without error when
211- // the job ends. The runner stops the job as the official runner does
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
212274 // (interrupt, then terminate, then kill) and reports the conclusion given
213- // for the reason with UpdateJob.
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.
214283 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);
215305 }
216306
217307 // RegisterRunnerRequest describes the runner to register.
@@ -237,6 +327,13 @@ message RegisterRunnerRequest {
237327 // Os is the operating system: "linux". Other values are invalid_argument
238328 // (the set may grow with compatible additions).
239329 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;
240337 }
241338
242339 // RegisterRunnerResponse holds the new runner and its secret.
@@ -245,9 +342,10 @@ message RegisterRunnerResponse {
245342 // a secret.
246343 string runner_id = 1;
247344 // RunnerToken is the secret of the runner, to present as "Authorization:
248- // Bearer <runner_token>" to FetchJob. It starts with "ermiterun_" and is
249- // returned once: the server keeps only its SHA-256, the runner must store
250- // it (mode 0600). It stops working when the runner is removed.
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.
251349 string runner_token = 2;
252350 }
253351
@@ -258,6 +356,13 @@ message FetchJobRequest {
258356 // a label of the runner (invalid_argument if not) and only jobs whose runs-on
259357 // labels are all among them are offered.
260358 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;
261366 }
262367
263368 // FetchJobResponse holds the job given to the runner, if any.
@@ -265,6 +370,11 @@ message FetchJobResponse {
265370 // Job is the job to run. Unset when no job was available before the end of
266371 // the poll.
267372 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;
268378 }
269379
270380 // Job is a job given to a runner: everything needed to run it, nothing more.
@@ -282,10 +392,13 @@ message Job {
282392 // one more after each RunService.RerunRun. It is the "github.run_attempt".
283393 int32 attempt = 3;
284394 // JobToken is the secret that authenticates this job's UpdateJob,
285- // AppendLog, Heartbeat and WatchJob calls ("Authorization: Bearer
286- // <job_token>"). It is random, valid until the job ends, survives a server
287- // restart and is shown only in this response. It must stay in the runner
288- // host process and never reach the job's code.
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.
289402 string job_token = 4;
290403 // WorkflowPath is the path of the workflow file in the repository, for
291404 // example ".github/workflows/ci.yml".
@@ -310,8 +423,11 @@ message Job {
310423 repeated string labels = 10;
311424 // TimeoutMinutes is the timeout of the job in minutes: the value of the
312425 // workflow's timeout-minutes, or 360 when it has none. At least 1. The
313- // runner enforces it from the start of the job; the server ends a job that
314- // exceeds it by announcing CANCEL_REASON_TIMEOUT.
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).
315431 int32 timeout_minutes = 11;
316432 // Repo is the name of the repository.
317433 string repo = 12;
@@ -323,7 +439,7 @@ message Job {
323439 string ref = 14;
324440 }
325441
326-// UpdateJobRequest reports the steps and, in the last call, the end of a job.
442+// UpdateJobRequest reports the steps of a running job.
327443 message UpdateJobRequest {
328444 // JobId is the Job.job_id. It must be the job of the token.
329445 string job_id = 1;
@@ -333,28 +449,6 @@ message UpdateJobRequest {
333449 // implicit "Set up job" is 1, the steps of the workflow follow in order, and
334450 // the implicit "Complete job" is last.
335451 repeated StepUpdate steps = 2;
336- // Outputs are the outputs of the job, evaluated by the runner. Only allowed
337- // in the call that carries the conclusion (invalid_argument otherwise).
338- // Names are 1 to 255 characters among letters, digits, "_" and "-" and
339- // begin with a letter or "_" and are case-insensitive: two names that
340- // differ only by case are invalid_argument. Each value is at most 1 MiB, and
341- // all names and values together at most 1 MiB (1,048,576 bytes); beyond that
342- // the whole call is rejected with invalid_argument and the runner reports
343- // the failure of the job without outputs, with a line in the log.
344- map<string, string> outputs = 3;
345- // Conclusion ends the job when set. A runner reports CI_CONCLUSION_SUCCESS,
346- // CI_CONCLUSION_FAILURE, CI_CONCLUSION_CANCELLED or CI_CONCLUSION_TIMED_OUT; any
347- // other value is invalid_argument. CI_CONCLUSION_UNSPECIFIED means the job goes
348- // on. Steps that were not reported COMPLETED when the job ends are
349- // completed by the server as CI_CONCLUSION_CANCELLED (or SKIPPED when they
350- // never started and the job did not fail by cancellation).
351- CiConclusion conclusion = 4;
352- // RunnerShutdown is true when the runner ends the job because it was told to
353- // stop (service stopped, signal). Only allowed with CI_CONCLUSION_CANCELLED.
354- // The server then annotates the job with GitHub's message "The runner has
355- // received a shutdown signal. This can happen when the runner service is
356- // stopped, or a manually started runner is canceled."
357- bool runner_shutdown = 5;
358452 }
359453
360454 // StepUpdate is the state of one step as the runner knows it.
@@ -383,13 +477,67 @@ message StepUpdate {
383477 // UpdateJobResponse is empty.
384478 message UpdateJobResponse {}
385479
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+
386533 // AppendLogRequest carries log lines of one step.
387534 message AppendLogRequest {
388535 // JobId is the Job.job_id. It must be the job of the token.
389536 string job_id = 1;
390537 // Step is the number of the step the lines belong to (see
391- // UpdateJobRequest.steps), at least 1. A step that UpdateJob has not
392- // announced yet is created by the first lines.
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.
393541 int32 step = 2;
394542 // Offset is the number of the first line of the request in the log of the
395543 // job, counted from 0 across all steps. See the rules on AppendLog.
@@ -409,6 +557,15 @@ message LogLine {
409557 // carriage return is kept. It is the line as GitHub's raw log has it,
410558 // including "##[group]" and "##[endgroup]" markers and "##[error]" lines.
411559 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;
412569 }
413570
414571 // AppendLogResponse tells how much of the log the server holds.
@@ -433,13 +590,28 @@ message WatchJobRequest {
433590 string job_id = 1;
434591 }
435592
436-// WatchJobResponse is one message of the stream: a request to cancel the job,
437-// or a keep-alive.
593+// WatchJobResponse is one message of the stream.
438594 message WatchJobResponse {
439- // Reason is why the job must stop. CANCEL_REASON_UNSPECIFIED marks a
440- // keep-alive: the other fields are empty and the client ignores it. New
441- // reasons may be added; a runner that does not know one cancels the job and
442- // reports CI_CONCLUSION_CANCELLED.
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.
443615 CancelReason reason = 1;
444616 // Message is a short human-readable explanation, for the job log, for
445617 // example "Canceled by the owner". At most 1,024 characters.
@@ -4,6 +4,7 @@ syntax = "proto3";
4 4
5 package ermite;5 package ermite;
6 6
7+import "google/protobuf/duration.proto";
7 import "google/protobuf/timestamp.proto";8 import "google/protobuf/timestamp.proto";
8 9
9 // CiStatus is where a run, a job or a check is in its life. It is the same10 // CiStatus is where a run, a job or a check is in its life. It is the same
@@ -79,8 +80,7 @@ enum RunnerIsolation {
79 80
80 // CancelReason says why the server asks a runner to stop a job.81 // CancelReason says why the server asks a runner to stop a job.
81 enum CancelReason {82 enum CancelReason {
82- // Not set. A WatchJobResponse with this reason is a keep-alive and carries83+ // Not set. Never sent in a CancelRequest.
83- // no request.
84 CANCEL_REASON_UNSPECIFIED = 0;84 CANCEL_REASON_UNSPECIFIED = 0;
85 // The owner cancelled the run (RunService.CancelRun, web or CLI). The job85 // The owner cancelled the run (RunService.CancelRun, web or CLI). The job
86 // ends CI_CONCLUSION_CANCELLED.86 // ends CI_CONCLUSION_CANCELLED.
@@ -88,38 +88,55 @@ enum CancelReason {
88 // Another job of the same matrix failed and the strategy has fail-fast. The88 // Another job of the same matrix failed and the strategy has fail-fast. The
89 // job ends CI_CONCLUSION_CANCELLED.89 // job ends CI_CONCLUSION_CANCELLED.
90 CANCEL_REASON_FAIL_FAST = 2;90 CANCEL_REASON_FAIL_FAST = 2;
91- // The job exceeded its timeout_minutes without the runner ending it. The91+ // The job exceeded its time budget (see Job.timeout_minutes). The job ends
92- // job ends CI_CONCLUSION_TIMED_OUT.92+ // CI_CONCLUSION_TIMED_OUT.
93 CANCEL_REASON_TIMEOUT = 3;93 CANCEL_REASON_TIMEOUT = 3;
94 }94 }
95 95
96 // RunnerService is what a CI runner (ermite-runner) calls to get jobs and96 // 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.97 // report on them. It is not for people: owners use RunService.
98 //98 //
99-// Three credentials are used, each by different RPCs:99+// Three credentials are used, each by different RPCs. All are sent as
100+// "Authorization: Bearer <secret>":
100 //101 //
101 // - RegisterRunner: a stored token with the runner:register scope, or the102 // - RegisterRunner: a stored token with the runner:register scope, or the
102-// root token, as "Authorization: Bearer <secret>".103+// root token.
103-// - FetchJob: the runner token returned by RegisterRunner, as104+// - FetchJob: the runner token returned by RegisterRunner. It identifies the
104-// "Authorization: Bearer <runner_token>". It identifies the runner. It105+// runner and only works for FetchJob. It is "ermiterun_" followed by 43
105-// only works for FetchJob.106+// base64url characters (32 random bytes, no padding).
106-// - UpdateJob, AppendLog, Heartbeat, WatchJob: the job token of the Job that107+// - UpdateJob, AppendLog, Heartbeat, WatchJob, CompleteJob: the job token of
107-// FetchJob returned, as "Authorization: Bearer <job_token>". It is valid108+// the Job that FetchJob returned. It is valid for that one job and that
108-// for that one job and attempt, from its assignment until it ends. The109+// one assignment. It is "ermitejob_" followed by 43 base64url characters
109-// job_id of the request must be the one of the token.110+// (32 random bytes, no padding). The job_id of the request must be the
111+// one of the token.
110 //112 //
111-// A request without a credential, with an unknown, expired or revoked one, or113+// Errors. unauthenticated means only that the credential is bad: missing,
112-// with a credential of the wrong kind for the RPC, gets unauthenticated. A114+// unknown, expired, revoked, of the wrong kind for the RPC, or a job token
113-// job token that is valid but belongs to another job than the job_id of the115+// that was replaced (the job was given again, see FetchJob) or is older than
114-// request gets permission_denied. A job that has ended (concluded, cancelled,116+// the 24 hours below. A runner that gets it for a job abandons the job at
115-// failed by the server, or whose runner was lost) no longer has a valid job117+// once. A job token that is valid but belongs to another job than the job_id
116-// token: every later call gets unauthenticated, which a runner treats as "the118+// of the request gets permission_denied. Repeated failed authentications are
117-// job is over" and stops sending anything for it. Repeated failed119+// rate limited per client address.
118-// authentications are rate limited per client address.
119 //120 //
120-// Runner tokens and job tokens are secrets: the server never logs them and121+// The end of a job. A job ends when its conclusion is set, by CompleteJob or
121-// clients must not log them. The job token stays in the runner host process; it122+// by the server (cancellation not honoured, lost runner, log limit, queue
122-// is never given to the sandbox that runs the job's code.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.
123 //140 //
124 // Servers that implement this service list "ci.runners" in141 // Servers that implement this service list "ci.runners" in
125 // GetVersionResponse.features.142 // GetVersionResponse.features.
@@ -130,37 +147,54 @@ service RunnerService {
130 // (a runner serves every repository), or the root token. Fails with147 // (a runner serves every repository), or the root token. Fails with
131 // unauthenticated without a valid token, permission_denied without the148 // unauthenticated without a valid token, permission_denied without the
132 // scope or with a repository-restricted token, invalid_argument if a field149 // scope or with a repository-restricted token, invalid_argument if a field
133- // breaks a rule, and already_exists if name is taken by another runner.150+ // breaks a rule, and already_exists if name is taken by another runner and
151+ // replace is not set.
134 rpc RegisterRunner(RegisterRunnerRequest) returns (RegisterRunnerResponse);152 rpc RegisterRunner(RegisterRunnerRequest) returns (RegisterRunnerResponse);
135 // FetchJob asks for a job to run (long poll). The server holds the call153 // FetchJob asks for a job to run (long poll). The server holds the call
136 // until a queued job matches the runner, or about 50 seconds (never more154 // until a queued job matches the runner, or about 50 seconds (never more
137 // than 50) pass, then answers; without a job the response has no job and the155 // than 50) pass, then answers; without a job the response has no job and the
138- // runner calls again at once. Clients set a call timeout above 50 seconds156+ // runner calls again after retry_after. Clients set a call timeout above 50
139- // (60 is suggested) and must not poll when they have no free capacity.157+ // seconds (60 is suggested) and must not poll when they have no free
158+ // capacity.
140 //159 //
141 // A job matches when every label of its runs-on is among the labels of the160 // A job matches when every label of its runs-on is among the labels of the
142 // runner (case-insensitive). A job is given to exactly one runner, even when161 // runner (case-insensitive). A job is given to exactly one runner, even when
143 // many runners and many polls compete; the assignment is recorded before162 // many runners and many polls compete; the assignment is recorded before
144- // the response is sent, and the job is then in progress. If the response is163+ // the response is sent. The job stays QUEUED for the owner until it is
145- // lost the job is not offered again: with no Heartbeat for 2 minutes the164+ // acknowledged.
146- // server fails it as a lost runner.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).
147 //184 //
148 // Needs the runner token. Fails with unauthenticated when the runner was185 // Needs the runner token. Fails with unauthenticated when the runner was
149 // removed (also ending a call in progress) and invalid_argument if labels186 // removed (also ending a call in progress) and invalid_argument if labels
150- // breaks a rule.187+ // or poll_id break a rule.
151 rpc FetchJob(FetchJobRequest) returns (FetchJobResponse);188 rpc FetchJob(FetchJobRequest) returns (FetchJobResponse);
152- // UpdateJob reports the state of the steps of a job and, once, how the job189+ // UpdateJob reports the state of the steps of a job while it runs. Needs the
153- // ended. Needs the job token. Each call is applied whole or not at all, and190+ // job token. Each call is applied whole or not at all, and repeating an
154- // repeating an identical call changes nothing, so a runner may retry after a191+ // identical call changes nothing, so a runner may retry after a network
155- // network error.192+ // error. To end the job use CompleteJob.
156 //193 //
157- // Fails with invalid_argument if a rule is broken (including the output194+ // Fails with invalid_argument if a rule is broken, with failed_precondition
158- // limits), and with failed_precondition if a step goes back (a COMPLETED195+ // if a step goes back (a COMPLETED step cannot change, a step cannot go from
159- // step cannot change, a step cannot go from IN_PROGRESS to PENDING).196+ // IN_PROGRESS to PENDING) and with failed_precondition and a JobEnded detail
160- // The call that carries a conclusion ends the job: its job token stops197+ // if the job has ended.
161- // working, the server completes the job and no further call is accepted,
162- // so a retry of that last call after a lost response gets unauthenticated,
163- // which means it was applied.
164 rpc UpdateJob(UpdateJobRequest) returns (UpdateJobResponse);198 rpc UpdateJob(UpdateJobRequest) returns (UpdateJobResponse);
165 // AppendLog appends lines to the log of a job. Needs the job token.199 // AppendLog appends lines to the log of a job. Needs the job token.
166 //200 //
@@ -172,46 +206,102 @@ service RunnerService {
172 // - offset equal to the server's line count: the lines are appended;206 // - offset equal to the server's line count: the lines are appended;
173 // - offset below it: the first (count - offset) lines were already stored207 // - offset below it: the first (count - offset) lines were already stored
174 // (a retry after a lost response) and are skipped, and the rest is208 // (a retry after a lost response) and are skipped, and the rest is
175- // appended; a request entirely stored changes nothing;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;
176 // - offset above it: out_of_range (a gap is a bug of the client; the213 // - offset above it: out_of_range (a gap is a bug of the client; the
177 // message gives the line count the server holds).214 // message gives the line count the server holds).
178 //215 //
179 // The response gives the line count after the call, so a client can check216 // The response gives the line count after the call, so a client can check
180- // itself. A request has at most 1,000 lines whose text adds up to at most217+ // itself. Offsets resume only after a network error within the same runner
181- // 64 KiB (65,536 bytes), all in the same step; more is invalid_argument. A218+ // process; they are not a way to resume a job after a crash.
182- // line of more than 64 KiB is cut by the client into several lines.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.
183 //230 //
184 // The log of a job is limited to 512 MiB (536,870,912 bytes) as stored by231 // The log of a job is limited to 512 MiB (536,870,912 bytes) as stored by
185 // the server (text, timestamps and separators). The call that would exceed232 // the server (text, timestamps and separators). The call that would exceed
186 // it appends nothing; the server adds a last line saying that the limit was233 // it appends nothing; the server adds a last line saying that the limit was
187 // reached, ends the job with CI_CONCLUSION_FAILURE and RunJob.error set, and234 // reached, ends the job with CI_CONCLUSION_FAILURE and RunJob.error set, and
188- // answers resource_exhausted. The runner then stops the job and sends235+ // answers failed_precondition with a JobEnded detail like any call after the
189- // nothing more: its calls get unauthenticated.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.
190 //240 //
191 // Fails with invalid_argument if a rule is broken (step number, text with a241 // Fails with invalid_argument if a rule is broken (step number, text with a
192- // line feed or invalid UTF-8, missing time).242+ // line feed or invalid UTF-8, missing time, a continued first line).
193 rpc AppendLog(AppendLogRequest) returns (AppendLogResponse);243 rpc AppendLog(AppendLogRequest) returns (AppendLogResponse);
194 // Heartbeat tells the server that the runner is alive and still working on244 // Heartbeat tells the server that the runner is alive and still working on
195- // the job. Needs the job token. The runner calls it every 30 seconds for as245+ // the job. Needs the job token. The first one acknowledges the assignment
196- // long as the job runs, from assignment to the end. Without a heartbeat for246+ // (see FetchJob). The runner then calls it every 30 seconds for as long as
197- // 2 minutes the server ends the job with CI_CONCLUSION_FAILURE and the message247+ // the job runs. The deadline is 2 minutes after the later of the last
198- // "The self-hosted runner: <name> lost communication with the server...".248+ // heartbeat and the start of the server (so a server restart does not fail
199- // Only Heartbeat counts: other calls do not keep a job alive.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.
200 rpc Heartbeat(HeartbeatRequest) returns (HeartbeatResponse);257 rpc Heartbeat(HeartbeatRequest) returns (HeartbeatResponse);
201- // WatchJob streams the requests to stop a job. Needs the job token. The258+ // WatchJob streams what the runner must react to. Needs the job token. The
202 // runner opens it when it starts the job and keeps it open; it reopens it259 // runner opens it when it starts the job and keeps it open; it reopens it
203- // after any error other than unauthenticated.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.
204 //263 //
205 // When the stream opens and a cancellation has already been requested, the264 // When the stream opens and a cancellation has already been requested, the
206 // first message is that request, so a reconnect never misses one. Only the265 // first message is that request, so a reconnect never misses one. Only the
207 // first reason is sent: a job already cancelled by the owner does not get a266 // first reason is sent: a job already cancelled by the owner does not get a
208- // second message when its timeout expires. The server also sends a267+ // second message when its time budget expires. The server also sends a
209- // keep-alive message (reason CANCEL_REASON_UNSPECIFIED) at least every 30268+ // Keepalive at least every 30 seconds, which the client ignores. When the
210- // seconds, which the client ignores. The stream ends without error when269+ // job ends the server sends JobEnded, then closes the stream without error.
211- // the job ends. The runner stops the job as the official runner does270+ // 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
212 // (interrupt, then terminate, then kill) and reports the conclusion given274 // (interrupt, then terminate, then kill) and reports the conclusion given
213- // for the reason with UpdateJob.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.
214 rpc WatchJob(WatchJobRequest) returns (stream WatchJobResponse);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);
215 }305 }
216 306
217 // RegisterRunnerRequest describes the runner to register.307 // RegisterRunnerRequest describes the runner to register.
@@ -237,6 +327,13 @@ message RegisterRunnerRequest {
237 // Os is the operating system: "linux". Other values are invalid_argument327 // Os is the operating system: "linux". Other values are invalid_argument
238 // (the set may grow with compatible additions).328 // (the set may grow with compatible additions).
239 string os = 6;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;
240 }337 }
241 338
242 // RegisterRunnerResponse holds the new runner and its secret.339 // RegisterRunnerResponse holds the new runner and its secret.
@@ -245,9 +342,10 @@ message RegisterRunnerResponse {
245 // a secret.342 // a secret.
246 string runner_id = 1;343 string runner_id = 1;
247 // RunnerToken is the secret of the runner, to present as "Authorization:344 // RunnerToken is the secret of the runner, to present as "Authorization:
248- // Bearer <runner_token>" to FetchJob. It starts with "ermiterun_" and is345+ // Bearer <runner_token>" to FetchJob: "ermiterun_" and 43 base64url
249- // returned once: the server keeps only its SHA-256, the runner must store346+ // characters (32 random bytes, no padding). It is returned once: the server
250- // it (mode 0600). It stops working when the runner is removed.347+ // keeps only its SHA-256, the runner must store it (mode 0600). It stops
348+ // working when the runner is removed or replaced.
251 string runner_token = 2;349 string runner_token = 2;
252 }350 }
253 351
@@ -258,6 +356,13 @@ message FetchJobRequest {
258 // a label of the runner (invalid_argument if not) and only jobs whose runs-on356 // a label of the runner (invalid_argument if not) and only jobs whose runs-on
259 // labels are all among them are offered.357 // labels are all among them are offered.
260 repeated string labels = 1;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;
261 }366 }
262 367
263 // FetchJobResponse holds the job given to the runner, if any.368 // FetchJobResponse holds the job given to the runner, if any.
@@ -265,6 +370,11 @@ message FetchJobResponse {
265 // Job is the job to run. Unset when no job was available before the end of370 // Job is the job to run. Unset when no job was available before the end of
266 // the poll.371 // the poll.
267 Job job = 1;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;
268 }378 }
269 379
270 // Job is a job given to a runner: everything needed to run it, nothing more.380 // Job is a job given to a runner: everything needed to run it, nothing more.
@@ -282,10 +392,13 @@ message Job {
282 // one more after each RunService.RerunRun. It is the "github.run_attempt".392 // one more after each RunService.RerunRun. It is the "github.run_attempt".
283 int32 attempt = 3;393 int32 attempt = 3;
284 // JobToken is the secret that authenticates this job's UpdateJob,394 // JobToken is the secret that authenticates this job's UpdateJob,
285- // AppendLog, Heartbeat and WatchJob calls ("Authorization: Bearer395+ // AppendLog, Heartbeat, WatchJob and CompleteJob calls ("Authorization:
286- // <job_token>"). It is random, valid until the job ends, survives a server396+ // Bearer <job_token>"): "ermitejob_" and 43 base64url characters (32 random
287- // restart and is shown only in this response. It must stay in the runner397+ // bytes, no padding). It is random, valid from the assignment until the job
288- // host process and never reach the job's code.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.
289 string job_token = 4;402 string job_token = 4;
290 // WorkflowPath is the path of the workflow file in the repository, for403 // WorkflowPath is the path of the workflow file in the repository, for
291 // example ".github/workflows/ci.yml".404 // example ".github/workflows/ci.yml".
@@ -310,8 +423,11 @@ message Job {
310 repeated string labels = 10;423 repeated string labels = 10;
311 // TimeoutMinutes is the timeout of the job in minutes: the value of the424 // TimeoutMinutes is the timeout of the job in minutes: the value of the
312 // workflow's timeout-minutes, or 360 when it has none. At least 1. The425 // workflow's timeout-minutes, or 360 when it has none. At least 1. The
313- // runner enforces it from the start of the job; the server ends a job that426+ // runner enforces it from the moment it receives the job. The server counts
314- // exceeds it by announcing CANCEL_REASON_TIMEOUT.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).
315 int32 timeout_minutes = 11;431 int32 timeout_minutes = 11;
316 // Repo is the name of the repository.432 // Repo is the name of the repository.
317 string repo = 12;433 string repo = 12;
@@ -323,7 +439,7 @@ message Job {
323 string ref = 14;439 string ref = 14;
324 }440 }
325 441
326-// UpdateJobRequest reports the steps and, in the last call, the end of a job.442+// UpdateJobRequest reports the steps of a running job.
327 message UpdateJobRequest {443 message UpdateJobRequest {
328 // JobId is the Job.job_id. It must be the job of the token.444 // JobId is the Job.job_id. It must be the job of the token.
329 string job_id = 1;445 string job_id = 1;
@@ -333,28 +449,6 @@ message UpdateJobRequest {
333 // implicit "Set up job" is 1, the steps of the workflow follow in order, and449 // implicit "Set up job" is 1, the steps of the workflow follow in order, and
334 // the implicit "Complete job" is last.450 // the implicit "Complete job" is last.
335 repeated StepUpdate steps = 2;451 repeated StepUpdate steps = 2;
336- // Outputs are the outputs of the job, evaluated by the runner. Only allowed
337- // in the call that carries the conclusion (invalid_argument otherwise).
338- // Names are 1 to 255 characters among letters, digits, "_" and "-" and
339- // begin with a letter or "_" and are case-insensitive: two names that
340- // differ only by case are invalid_argument. Each value is at most 1 MiB, and
341- // all names and values together at most 1 MiB (1,048,576 bytes); beyond that
342- // the whole call is rejected with invalid_argument and the runner reports
343- // the failure of the job without outputs, with a line in the log.
344- map<string, string> outputs = 3;
345- // Conclusion ends the job when set. A runner reports CI_CONCLUSION_SUCCESS,
346- // CI_CONCLUSION_FAILURE, CI_CONCLUSION_CANCELLED or CI_CONCLUSION_TIMED_OUT; any
347- // other value is invalid_argument. CI_CONCLUSION_UNSPECIFIED means the job goes
348- // on. Steps that were not reported COMPLETED when the job ends are
349- // completed by the server as CI_CONCLUSION_CANCELLED (or SKIPPED when they
350- // never started and the job did not fail by cancellation).
351- CiConclusion conclusion = 4;
352- // RunnerShutdown is true when the runner ends the job because it was told to
353- // stop (service stopped, signal). Only allowed with CI_CONCLUSION_CANCELLED.
354- // The server then annotates the job with GitHub's message "The runner has
355- // received a shutdown signal. This can happen when the runner service is
356- // stopped, or a manually started runner is canceled."
357- bool runner_shutdown = 5;
358 }452 }
359 453
360 // StepUpdate is the state of one step as the runner knows it.454 // StepUpdate is the state of one step as the runner knows it.
@@ -383,13 +477,67 @@ message StepUpdate {
383 // UpdateJobResponse is empty.477 // UpdateJobResponse is empty.
384 message UpdateJobResponse {}478 message UpdateJobResponse {}
385 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+
386 // AppendLogRequest carries log lines of one step.533 // AppendLogRequest carries log lines of one step.
387 message AppendLogRequest {534 message AppendLogRequest {
388 // JobId is the Job.job_id. It must be the job of the token.535 // JobId is the Job.job_id. It must be the job of the token.
389 string job_id = 1;536 string job_id = 1;
390 // Step is the number of the step the lines belong to (see537 // Step is the number of the step the lines belong to (see
391- // UpdateJobRequest.steps), at least 1. A step that UpdateJob has not538+ // UpdateJobRequest.steps), at least 1, and at least the step of the last
392- // announced yet is created by the first lines.539+ // stored line. A step that UpdateJob has not announced yet is created by the
540+ // first lines. See the rules on AppendLog.
393 int32 step = 2;541 int32 step = 2;
394 // Offset is the number of the first line of the request in the log of the542 // Offset is the number of the first line of the request in the log of the
395 // job, counted from 0 across all steps. See the rules on AppendLog.543 // job, counted from 0 across all steps. See the rules on AppendLog.
@@ -409,6 +557,15 @@ message LogLine {
409 // carriage return is kept. It is the line as GitHub's raw log has it,557 // carriage return is kept. It is the line as GitHub's raw log has it,
410 // including "##[group]" and "##[endgroup]" markers and "##[error]" lines.558 // including "##[group]" and "##[endgroup]" markers and "##[error]" lines.
411 string text = 2;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;
412 }569 }
413 570
414 // AppendLogResponse tells how much of the log the server holds.571 // AppendLogResponse tells how much of the log the server holds.
@@ -433,13 +590,28 @@ message WatchJobRequest {
433 string job_id = 1;590 string job_id = 1;
434 }591 }
435 592
436-// WatchJobResponse is one message of the stream: a request to cancel the job,593+// WatchJobResponse is one message of the stream.
437-// or a keep-alive.
438 message WatchJobResponse {594 message WatchJobResponse {
439- // Reason is why the job must stop. CANCEL_REASON_UNSPECIFIED marks a595+ // Msg is what the message carries. New kinds may be added: a client ignores
440- // keep-alive: the other fields are empty and the client ignores it. New596+ // the ones it does not know.
441- // reasons may be added; a runner that does not know one cancels the job and597+ oneof msg {
442- // reports CI_CONCLUSION_CANCELLED.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.
443 CancelReason reason = 1;615 CancelReason reason = 1;
444 // Message is a short human-readable explanation, for the job log, for616 // Message is a short human-readable explanation, for the job log, for
445 // example "Canceled by the owner". At most 1,024 characters.617 // example "Canceled by the owner". At most 1,024 characters.

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