1// SPDX-License-Identifier: MIT
2
3syntax = "proto3";
4
5package ermite;
6
7import "google/protobuf/duration.proto";
8import "google/protobuf/timestamp.proto";
9
10// CiStatus is where a run, a job or a check is in its life. It is the same
11// as GitHub's status, without the rarely used values.
12enum CiStatus {
13 // Not set. In a filter it means every status. Never returned by the server.
14 CI_STATUS_UNSPECIFIED = 0;
15 // Waiting: for a runner, for the jobs it needs, or for its turn
16 // (max-parallel).
17 CI_STATUS_QUEUED = 1;
18 // At least one job has been given to a runner and has not ended.
19 CI_STATUS_IN_PROGRESS = 2;
20 // Over. The conclusion says how it ended.
21 CI_STATUS_COMPLETED = 3;
22}
23
24// CiConclusion is how a run, a job, a step or a check ended, with GitHub's
25// values and meaning. It is unset (CI_CONCLUSION_UNSPECIFIED) until the status is
26// COMPLETED.
27enum CiConclusion {
28 // Not completed yet. Never returned by the server for a completed item.
29 CI_CONCLUSION_UNSPECIFIED = 0;
30 // Everything it ran succeeded.
31 CI_CONCLUSION_SUCCESS = 1;
32 // A step failed, or the job failed for another reason described in
33 // RunJob.error (runner lost, queue timeout, log limit, feature not
34 // supported yet, invalid workflow file).
35 CI_CONCLUSION_FAILURE = 2;
36 // Cancelled by the owner, or by fail-fast, or because the runner shut down.
37 CI_CONCLUSION_CANCELLED = 3;
38 // Never ran: its "if" was false, or a job it needs did not succeed, or it was
39 // not selected. Never reported by a runner.
40 CI_CONCLUSION_SKIPPED = 4;
41 // Exceeded its timeout.
42 CI_CONCLUSION_TIMED_OUT = 5;
43 // Neither a success nor a failure. Defined for parity with GitHub; no
44 // workflow job ends this way yet. For merging it counts as a success.
45 CI_CONCLUSION_NEUTRAL = 6;
46 // Waits for a human action (for example the approval of an environment).
47 // Defined for parity with GitHub; the server does not produce it before
48 // environments exist. For merging it counts as a failure.
49 CI_CONCLUSION_ACTION_REQUIRED = 7;
50}
51
52// StepState is where a step is in its life.
53enum StepState {
54 // Not set. Rejected in requests, never returned by the server.
55 STEP_STATE_UNSPECIFIED = 0;
56 // Known but not started.
57 STEP_STATE_PENDING = 1;
58 // Running.
59 STEP_STATE_IN_PROGRESS = 2;
60 // Over. The conclusion says how it ended. A step that did not run because
61 // of its "if" or an earlier failure is COMPLETED with CI_CONCLUSION_SKIPPED.
62 STEP_STATE_COMPLETED = 3;
63}
64
65// RunnerIsolation is how a runner isolates each job from the host and from
66// other jobs. The runner chooses it (auto-detection included) and reports it
67// at registration; the job page shows it.
68enum RunnerIsolation {
69 // Not set. Rejected at registration, never returned by the server.
70 RUNNER_ISOLATION_UNSPECIFIED = 0;
71 // One micro-VM per job, destroyed at the end.
72 RUNNER_ISOLATION_VM = 1;
73 // One container per job with its own Docker daemon, run by the sysbox
74 // runtime. Shares the host kernel.
75 RUNNER_ISOLATION_CONTAINER_SYSBOX = 2;
76 // One privileged container per job with its own Docker daemon. Shares the
77 // host kernel and is equivalent to root on the host: the weakest mode.
78 RUNNER_ISOLATION_CONTAINER_PRIVILEGED = 3;
79}
80
81// CancelReason says why the server asks a runner to stop a job.
82enum CancelReason {
83 // Not set. Never sent in a CancelRequest.
84 CANCEL_REASON_UNSPECIFIED = 0;
85 // The owner cancelled the run (RunService.CancelRun, web or CLI). The job
86 // ends CI_CONCLUSION_CANCELLED.
87 CANCEL_REASON_USER = 1;
88 // Another job of the same matrix failed and the strategy has fail-fast. The
89 // job ends CI_CONCLUSION_CANCELLED.
90 CANCEL_REASON_FAIL_FAST = 2;
91 // The job exceeded its time budget (see Job.timeout_minutes). The job ends
92 // CI_CONCLUSION_TIMED_OUT.
93 CANCEL_REASON_TIMEOUT = 3;
94}
95
96// AnnotationLevel is the severity of an annotation, with the values of
97// GitHub's check run annotation levels ("notice", "warning", "failure"). The
98// workflow commands ::notice, ::warning and ::error produce NOTICE, WARNING and
99// FAILURE.
100enum AnnotationLevel {
101 // Not set. Rejected in requests, never returned by the server.
102 ANNOTATION_LEVEL_UNSPECIFIED = 0;
103 // Information: ::notice.
104 ANNOTATION_LEVEL_NOTICE = 1;
105 // A problem that does not fail the job by itself: ::warning.
106 ANNOTATION_LEVEL_WARNING = 2;
107 // An error: ::error.
108 ANNOTATION_LEVEL_FAILURE = 3;
109}
110
111// ActionSource says where the server found an action.
112enum ActionSource {
113 // Not set. Only in a ResolvedAction that carries an error.
114 ACTION_SOURCE_UNSPECIFIED = 0;
115 // github.com: the archive is downloaded from GitHub, without a token.
116 ACTION_SOURCE_GITHUB = 1;
117 // A repository of this server, which wins over a github.com repository of
118 // the same name (as GitHub Enterprise Server's own actions do).
119 ACTION_SOURCE_ERMITE = 2;
120}
121
122// RunnerService is what a CI runner (ermite-runner) calls to get jobs and
123// report on them. It is not for people: owners use RunService.
124//
125// Three credentials are used, each by different RPCs. All are sent as
126// "Authorization: Bearer <secret>":
127//
128// - RegisterRunner: a stored token with the runner:register scope, or the
129// root token.
130// - FetchJob: the runner token returned by RegisterRunner. It identifies the
131// runner and only works for FetchJob. It is "ermiterun_" followed by 43
132// base64url characters (32 random bytes, no padding).
133// - UpdateJob, AppendLog, Heartbeat, WatchJob, CompleteJob, ResolveActions,
134// UploadSummary: the job token of the Job that FetchJob returned. It is valid for that one job and that
135// one assignment. It is "ermitejob_" followed by 43 base64url characters
136// (32 random bytes, no padding). The job_id of the request must be the
137// one of the token.
138//
139// Errors. unauthenticated means only that the credential is bad: missing,
140// unknown, expired, revoked, of the wrong kind for the RPC, or a job token
141// that was replaced (the job was given again, see FetchJob) or is older than
142// the 24 hours below. A runner that gets it for a job abandons the job at
143// once. A job token that is valid but belongs to another job than the job_id
144// of the request gets permission_denied. Repeated failed authentications are
145// rate limited per client address.
146//
147// The end of a job. A job ends when its conclusion is set, by CompleteJob or
148// by the server (cancellation not honoured, lost runner, log limit, queue
149// timeout). From then on UpdateJob, AppendLog, Heartbeat, WatchJob,
150// ResolveActions and UploadSummary answer failed_precondition with a JobEnded error detail (a JobEnded message in the
151// Connect error details) that gives the conclusion; the runner stops the job
152// and sends nothing more. The job token is still recognised for 24 hours after
153// the end, so that these calls can say so and so that CompleteJob, the only
154// call that still does something, returns the stored result to a runner that
155// lost the answer of its last call or that was cut off by the server. After
156// the 24 hours every call gets unauthenticated.
157//
158// Secrets and trust. Runner tokens and job tokens are secrets: the server
159// never logs them and clients must not log them. The job token stays in the
160// runner host process; it is never given to the sandbox that runs the job's
161// code. The job's repository token (Job.github_token) is another secret: it is
162// the one the sandbox does get, and it only reads the repository of the run.
163// A runner receives the jobs of every repository, with their workflow
164// files, contexts and (from CI-2) their code, and from CI-3 their secrets: the
165// runner:register scope therefore effectively gives read access to the code
166// and the secrets of every repository, private ones included. Give it only to
167// runners you operate.
168//
169// Servers that implement this service list "ci.runners" in
170// GetVersionResponse.features. Those that implement ResolveActions and
171// Job.github_token also list "ci.actions", those that accept annotations
172// (UpdateJobRequest.annotations, CompleteJobRequest.annotations) list
173// "ci.annotations", and those that implement UploadSummary list
174// "ci.summaries". A server without a feature ignores the fields of that
175// feature (protobuf drops unknown fields) and answers unimplemented to its
176// RPCs; a runner that needs one checks GetVersion first.
177service RunnerService {
178 // RegisterRunner registers a runner and returns its runner token, which is
179 // shown only in this response; the runner keeps it. The caller needs the
180 // runner:register scope on a token that is not restricted to repositories
181 // (a runner serves every repository), or the root token. Fails with
182 // unauthenticated without a valid token, permission_denied without the
183 // scope or with a repository-restricted token, invalid_argument if a field
184 // breaks a rule, and already_exists if name is taken by another runner and
185 // replace is not set.
186 rpc RegisterRunner(RegisterRunnerRequest) returns (RegisterRunnerResponse);
187 // FetchJob asks for a job to run (long poll). The server holds the call
188 // until a queued job matches the runner, or about 50 seconds (never more
189 // than 50) pass, then answers; without a job the response has no job and the
190 // runner calls again after retry_after. Clients set a call timeout above 50
191 // seconds (60 is suggested) and must not poll when they have no free
192 // capacity.
193 //
194 // A job matches when every label of its runs-on is among the labels of the
195 // runner (case-insensitive). A job is given to exactly one runner, even when
196 // many runners and many polls compete; the assignment is recorded before
197 // the response is sent. The job stays QUEUED for the owner until it is
198 // acknowledged.
199 //
200 // Acknowledgement. The runner's first Heartbeat for the job is the
201 // acknowledgement, and the runner sends it as soon as it has the job, before
202 // anything else. If none arrives within 30 seconds of the assignment (the
203 // response was lost, or the runner died), the server cancels the assignment:
204 // the job token is revoked, the job goes back to QUEUED with a fresh token
205 // and can be given to any runner, the same one included; nothing the old
206 // assignment stored is kept. This is not a failure of the job. A job whose
207 // assignment is cancelled 3 times in a row is failed with CI_CONCLUSION_FAILURE
208 // and an error saying so.
209 //
210 // Retry after a lost response. poll_id lets a client retry a poll safely:
211 // a call with the same poll_id as an earlier call of the same runner that
212 // assigned a job returns that same assignment (same Job, same token) as long
213 // as it has not been acknowledged or cancelled, instead of taking another.
214 //
215 // A runner that crashed or whose host restarted cannot resume a job: it does
216 // not keep the job token, so the job fails with the lost-runner error after
217 // the heartbeat timeout (see Heartbeat).
218 //
219 // Needs the runner token. Fails with unauthenticated when the runner was
220 // removed (also ending a call in progress) and invalid_argument if labels
221 // or poll_id break a rule.
222 rpc FetchJob(FetchJobRequest) returns (FetchJobResponse);
223 // UpdateJob reports the state of the steps of a job while it runs. Needs the
224 // job token. Each call is applied whole or not at all, and repeating an
225 // identical call changes nothing, so a runner may retry after a network
226 // error. To end the job use CompleteJob.
227 //
228 // Annotations are applied with the steps, in the same atomic step: for each
229 // step_number present in annotations, the annotations of that step stored
230 // so far are replaced by the ones of the request, so a retry never
231 // duplicates them. A step number absent from the request keeps its
232 // annotations. The step of an annotation need not be in the steps of the
233 // same call, and may already be COMPLETED: its annotations can still be
234 // replaced (the rule that a COMPLETED step cannot change applies to
235 // StepUpdate, not to annotations), until the job ends. A runner sends one
236 // UpdateJob (or CompleteJob) at a time for a job, never concurrent calls,
237 // so replacements apply in the order of the calls.
238 //
239 // Fails with invalid_argument if a rule is broken (including the annotation
240 // limits), with failed_precondition if a step goes back (a COMPLETED step
241 // cannot change, a step cannot go from IN_PROGRESS to PENDING) and with
242 // failed_precondition and a JobEnded detail if the job has ended.
243 rpc UpdateJob(UpdateJobRequest) returns (UpdateJobResponse);
244 // AppendLog appends lines to the log of a job. Needs the job token.
245 //
246 // The log of a job is one sequence of lines, numbered from 0 across all
247 // steps; offset is the number of lines the server holds when the request is
248 // applied, that is the number of the first line of the request. Calls must
249 // go in order, one at a time, and are idempotent:
250 //
251 // - offset equal to the server's line count: the lines are appended;
252 // - offset below it: the first (count - offset) lines were already stored
253 // (a retry after a lost response) and are skipped, and the rest is
254 // appended; a request entirely stored changes nothing. The skipped lines
255 // are not compared with the stored ones: a retry whose lines differ from
256 // the first attempt is silently dropped, so a client must resend the
257 // same lines at the same offset;
258 // - offset above it: out_of_range (a gap is a bug of the client; the
259 // message gives the line count the server holds).
260 //
261 // The response gives the line count after the call, so a client can check
262 // itself. Offsets resume only after a network error within the same runner
263 // process; they are not a way to resume a job after a crash.
264 //
265 // A request has at most 1,000 lines whose text adds up to at most 64 KiB
266 // (65,536 bytes), all in the same step; more is invalid_argument. A line
267 // longer than that is cut by the client into pieces, see LogLine.continued.
268 //
269 // Steps. The step of a request must be at least the step of the last line
270 // the server holds, because the log is ordered by step (applied lines only:
271 // lines skipped as already stored are not checked); a lower step is
272 // failed_precondition. Lines for a step that UpdateJob has not announced
273 // create it: it starts IN_PROGRESS at the time of its first line, with an
274 // empty name until an UpdateJob gives one.
275 //
276 // The log of a job is limited to 512 MiB (536,870,912 bytes) as stored by
277 // the server (text, timestamps and separators). The call that would exceed
278 // it appends nothing; the server adds a last line saying that the limit was
279 // reached, ends the job with CI_CONCLUSION_FAILURE and RunJob.error set, and
280 // answers failed_precondition with a JobEnded detail like any call after the
281 // end of a job. The runner then stops the job.
282 //
283 // Lines sent after the end of the job are rejected, so a runner flushes all
284 // its AppendLog lines before it calls CompleteJob.
285 //
286 // Fails with invalid_argument if a rule is broken (step number, text with a
287 // line feed or invalid UTF-8, missing time, a continued first line).
288 rpc AppendLog(AppendLogRequest) returns (AppendLogResponse);
289 // Heartbeat tells the server that the runner is alive and still working on
290 // the job. Needs the job token. The first one acknowledges the assignment
291 // (see FetchJob). The runner then calls it every 30 seconds for as long as
292 // the job runs. The deadline is 2 minutes after the later of the last
293 // heartbeat and the start of the server (so a server restart does not fail
294 // the jobs of runners that could not reach it): past it the server ends the
295 // job with CI_CONCLUSION_FAILURE and the message "The self-hosted runner:
296 // <name> lost communication with the server...". Only Heartbeat counts:
297 // other calls do not keep a job alive.
298 //
299 // A runner that gets unavailable (or any network error) retries Heartbeat
300 // every 5 seconds until it succeeds, instead of waiting for its next 30
301 // second tick, and keeps the job running meanwhile.
302 rpc Heartbeat(HeartbeatRequest) returns (HeartbeatResponse);
303 // WatchJob streams what the runner must react to. Needs the job token. The
304 // runner opens it when it starts the job and keeps it open; it reopens it
305 // after any error other than unauthenticated and the failed_precondition
306 // that says the job ended, and after a clean close of
307 // the stream that did not end with a JobEnded message.
308 //
309 // When the stream opens and a cancellation has already been requested, the
310 // first message is that request, so a reconnect never misses one. Only the
311 // first reason is sent: a job already cancelled by the owner does not get a
312 // second message when its time budget expires. The server also sends a
313 // Keepalive at least every 30 seconds, which the client ignores. When the
314 // job ends the server sends JobEnded, then closes the stream without error.
315 // On a job that has already ended the call fails like the other RPCs
316 // (failed_precondition with a JobEnded detail).
317 //
318 // On a CancelRequest the runner stops the job as the official runner does
319 // (interrupt, then terminate, then kill) and reports the conclusion given
320 // for the reason with CompleteJob.
321 //
322 // The server does not trust the runner to obey. If no conclusion arrives
323 // within 60 seconds after a cancellation request, whatever the reason (user,
324 // fail-fast, timeout), the server ends the job itself: CI_CONCLUSION_CANCELLED
325 // for user and fail-fast, CI_CONCLUSION_TIMED_OUT for timeout, with an error
326 // saying that the runner did not answer, and the steps not completed are
327 // completed as cancelled. The runner's later CompleteJob returns that result.
328 rpc WatchJob(WatchJobRequest) returns (stream WatchJobResponse);
329 // CompleteJob ends a job: it reports the final steps, the outputs and the
330 // conclusion. Needs the job token, which stays valid for this RPC for 24
331 // hours after the end of the job, however it ended. The call is applied whole
332 // or not at all. Once the job has ended, the token works for no other RPC
333 // (they answer failed_precondition with a JobEnded detail).
334 //
335 // It is idempotent. The first call that finds the job running ends it and
336 // answers applied = true. Any later call, a retry after a lost response or a
337 // call after the server ended the job itself, is not validated and changes
338 // nothing: it answers OK with the stored conclusion and error and
339 // applied = false (a runner whose conclusion differs from the stored one
340 // simply logs it; the outputs it sent are dropped).
341 //
342 // Lines sent with AppendLog after this call are rejected: a runner flushes
343 // all its log lines first. Steps that were not reported COMPLETED when the job
344 // ends are completed by the server as CI_CONCLUSION_CANCELLED (or SKIPPED when
345 // they never started and the job did not end by cancellation).
346 //
347 // Annotations are applied as in UpdateJob (replacement per step number, also
348 // for COMPLETED steps and steps not in steps), together with the conclusion.
349 // Summaries are not part of this call: a runner sends them with
350 // UploadSummary before it.
351 //
352 // Fails with invalid_argument if a rule is broken (including the output and
353 // annotation limits) and failed_precondition if a step goes back, as in
354 // UpdateJob.
355 rpc CompleteJob(CompleteJobRequest) returns (CompleteJobResponse);
356 // ResolveActions turns the "uses" of workflow and action steps into what the
357 // runner needs to download the actions, as GitHub's action download info
358 // does. Needs the job token; the job_id must be the one of the token. It
359 // may be called as often as needed while the job runs (the runner calls it
360 // once per level of composite actions, see the runner's depth limit of 9).
361 // The runner then downloads each archive itself, from the sandbox, with the
362 // token of the ResolvedAction when there is one.
363 //
364 // The response has one ResolvedAction per ActionRef, in the same order. A
365 // reference that cannot be resolved (unknown repository, tag or branch, a
366 // repository without access, a "uses" that GitHub itself would reject) does
367 // not fail the call: its ResolvedAction has only ref and error, where error
368 // is the message GitHub gives for the same failure, which the runner prints
369 // as is. Only a failure of the call itself (credential, ended job, bad
370 // request, server error) is an RPC error; the runner then fails the job.
371 //
372 // Resolution is stable for a run attempt: the same ref resolves to the same
373 // sha (ResolvedAction.resolved_sha) for every job and every call of that
374 // attempt. A re-run attempt resolves again. Tokens are not shared: each
375 // job gets its own (ResolvedAction.token).
376 //
377 // Fails with unauthenticated or permission_denied as the other job RPCs,
378 // with failed_precondition and a JobEnded detail if the job has ended, and
379 // with invalid_argument if there are more than 100 refs or if a "uses" is a
380 // local action ("./path") or a Docker image ("docker://"), which the runner
381 // handles without the server.
382 rpc ResolveActions(ResolveActionsRequest) returns (ResolveActionsResponse);
383 // UploadSummary stores the job summary of one step: the Markdown that the
384 // step wrote to the file named by GITHUB_STEP_SUMMARY. Needs the job token.
385 // The runner masks it before sending it, and sends it after the step ended
386 // and before CompleteJob.
387 //
388 // It is idempotent: the summary of a step is replaced by each call, so a
389 // retry changes nothing, and an empty markdown removes the summary of the
390 // step. The Markdown is at most 1 MiB (1,048,576 bytes) and valid UTF-8. The
391 // server reads requests of up to 1 MiB plus the envelope (job id, step
392 // number, framing), so a summary of exactly 1 MiB is accepted; clients set
393 // their own message size limit above that.
394 //
395 // Fails with invalid_argument if a rule is broken (size, UTF-8, step number)
396 // and with failed_precondition and a JobEnded detail if the job has ended.
397 rpc UploadSummary(UploadSummaryRequest) returns (UploadSummaryResponse);
398}
399
400// RegisterRunnerRequest describes the runner to register.
401message RegisterRunnerRequest {
402 // Name identifies the runner for people (job page, logs, ListRunners) and is
403 // unique: 1 to 64 characters among letters, digits, ".", "_" and "-". It is
404 // not a label and never matches runs-on.
405 string name = 1;
406 // Labels are the labels the runner adds to its implicit ones: at most 64,
407 // each 1 to 64 characters among letters, digits, ".", "_" and "-", for
408 // example "ubuntu-latest". Comparison is case-insensitive and duplicates
409 // are removed. The server adds the implicit labels "self-hosted", the os
410 // and the arch ("linux", "x64" or "arm64"); the runner name is not a label.
411 repeated string labels = 2;
412 // Version is the release of the runner, for example "v0.1.0", or "devel".
413 // Informational, at most 64 characters.
414 string version = 3;
415 // Isolation is how the runner isolates jobs. Required.
416 RunnerIsolation isolation = 4;
417 // Arch is the CPU architecture, in GitHub's names: "x64" or "arm64". Other
418 // values are invalid_argument (the set may grow with compatible additions).
419 string arch = 5;
420 // Os is the operating system: "linux". Other values are invalid_argument
421 // (the set may grow with compatible additions).
422 string os = 6;
423 // Replace removes the runner that already has this name, if any, before
424 // registering this one: its runner token is revoked at once, as in
425 // RunService.RemoveRunner, and a job it is running is not affected. The new
426 // runner gets a new runner_id. Needs the same scope as registering. Without
427 // replace, a name in use is already_exists. It is how a runner re-registers
428 // after losing its state.
429 bool replace = 7;
430}
431
432// RegisterRunnerResponse holds the new runner and its secret.
433message RegisterRunnerResponse {
434 // RunnerId identifies the runner in ListRunners and RemoveRunner. It is not
435 // a secret.
436 string runner_id = 1;
437 // RunnerToken is the secret of the runner, to present as "Authorization:
438 // Bearer <runner_token>" to FetchJob: "ermiterun_" and 43 base64url
439 // characters (32 random bytes, no padding). It is returned once: the server
440 // keeps only its SHA-256, the runner must store it (mode 0600). It stops
441 // working when the runner is removed or replaced.
442 string runner_token = 2;
443}
444
445// FetchJobRequest asks for a job.
446message FetchJobRequest {
447 // Labels narrows the labels offered for this call. Empty means all the
448 // labels of the runner, which is the normal case. Otherwise each one must be
449 // a label of the runner (invalid_argument if not) and only jobs whose runs-on
450 // labels are all among them are offered.
451 repeated string labels = 1;
452 // PollId identifies this poll, chosen by the client (for example 16 random
453 // bytes in base64url), 1 to 64 characters among letters, digits, "_" and
454 // "-". A client that retries a call whose response it did not get sends the
455 // same poll_id, and gets the assignment the first call made, if it made one
456 // (see FetchJob). A new poll uses a new id. Empty means no retry protection:
457 // a lost assignment is then recovered by the acknowledgement timeout alone.
458 string poll_id = 2;
459}
460
461// FetchJobResponse holds the job given to the runner, if any.
462message FetchJobResponse {
463 // Job is the job to run. Unset when no job was available before the end of
464 // the poll.
465 Job job = 1;
466 // RetryAfter is how long the client waits before its next FetchJob. Unset or
467 // zero means call again at once. The server sets it when it has no job for
468 // the runner and wants to slow polling down (load, shutdown in progress);
469 // clients obey it and may wait longer, never less. Unset when job is set.
470 google.protobuf.Duration retry_after = 2;
471}
472
473// Job is a job given to a runner: everything needed to run it, nothing more.
474// The runner parses workflow_yaml itself; the protocol does not carry the
475// workflow model, so it does not change when the model grows.
476message Job {
477 // JobId identifies the job on the server. It is opaque, unique across
478 // repositories and runs, and the same for every attempt of the job. With
479 // attempt it names one execution.
480 string job_id = 1;
481 // RunId is the run the job belongs to, the "github.run_id". It is global to
482 // the server.
483 int64 run_id = 2;
484 // Attempt is the attempt of the run this job executes: 1 for the first, and
485 // one more after each RunService.RerunRun. It is the "github.run_attempt".
486 int32 attempt = 3;
487 // JobToken is the secret that authenticates this job's UpdateJob,
488 // AppendLog, Heartbeat, WatchJob and CompleteJob calls ("Authorization:
489 // Bearer <job_token>"): "ermitejob_" and 43 base64url characters (32 random
490 // bytes, no padding). It is random, valid from the assignment until the job
491 // ends (and for CompleteJob 24 hours more), survives a server restart and
492 // is shown only in this response, or in a repeat of the same poll_id. It
493 // must stay in the runner host process and never reach the job's code. A new
494 // assignment of the same job has a new token.
495 string job_token = 4;
496 // WorkflowPath is the path of the workflow file in the repository, for
497 // example ".github/workflows/ci.yml".
498 string workflow_path = 5;
499 // WorkflowYaml is the content of that file at sha, as is, at most 1 MiB
500 // (larger files fail at planning).
501 string workflow_yaml = 6;
502 // JobKey is the key of the job under "jobs:" in the workflow, the
503 // "github.job".
504 string job_key = 7;
505 // Matrix is the JSON object of the matrix values of this instance of the
506 // job, for example {"os":"linux","go":"1.27"}. "{}" when the job has no
507 // matrix. It is the "matrix" context.
508 string matrix = 8;
509 // Contexts is a JSON object with the contexts that the server computed for
510 // this job: "github", "needs", "strategy", "matrix", "inputs" and "vars", in
511 // the shape GitHub gives them. The contexts "job", "runner", "steps" and
512 // "env" belong to the runner and are not in it. Other keys may be added
513 // later; clients ignore those they do not know.
514 string contexts = 9;
515 // Labels are the runs-on labels of the job, evaluated.
516 repeated string labels = 10;
517 // TimeoutMinutes is the timeout of the job in minutes: the value of the
518 // workflow's timeout-minutes, or 360 when it has none. At least 1. The
519 // runner enforces it from the moment it receives the job. The server counts
520 // from the moment of the assignment and adds 60 seconds of slack, so the
521 // runner's own timeout always comes first: when the server's deadline
522 // passes it announces CANCEL_REASON_TIMEOUT, and ends the job itself after
523 // 60 more seconds without a conclusion (see WatchJob).
524 int32 timeout_minutes = 11;
525 // Repo is the name of the repository.
526 string repo = 12;
527 // Sha is the full id of the commit the run is for, 40 lowercase
528 // hexadecimal characters for SHA-1. The runner reads the code at it.
529 string sha = 13;
530 // Ref is the full ref name that triggered the run, for example
531 // "refs/heads/main" or "refs/tags/v1.0.0".
532 string ref = 14;
533 // GithubToken is the job repository token, the "github.token" and
534 // "secrets.GITHUB_TOKEN" of the job: "ermitegh_" and 43 base64url characters
535 // (32 random bytes, no padding). Unlike job_token it is meant for the code of
536 // the job: the runner gives it to the sandbox (actions/checkout clones with
537 // it). It can only read the repository of the run (read-only, whatever
538 // "permissions:" says), and is valid from the assignment until the job ends
539 // (it is revoked then, on cancellation and when the job is lost). It is a
540 // secret: the runner registers it in its masker before anything runs, and
541 // neither the runner nor the server logs it. It is shown only in this
542 // response, or in a repeat of the same poll_id; a new assignment of the same
543 // job has a new one. Empty from servers without the feature "ci.actions".
544 string github_token = 15;
545}
546
547// UpdateJobRequest reports the steps of a running job.
548message UpdateJobRequest {
549 // JobId is the Job.job_id. It must be the job of the token.
550 string job_id = 1;
551 // Steps are the steps whose state changed or that are new. Each one is a
552 // full snapshot of its step, applied by number. At most 1,000 steps per job
553 // (invalid_argument beyond). Steps are numbered like GitHub's log: the
554 // implicit "Set up job" is 1, the steps of the workflow follow in order, and
555 // the implicit "Complete job" is last.
556 repeated StepUpdate steps = 2;
557 // Annotations are the annotations of the steps that the call names: for
558 // each step_number present, all the annotations of that step known so far,
559 // not only the new ones (see the rules on UpdateJob). At most 10 per level
560 // and per step, and each one within the limits of Annotation; otherwise the
561 // whole call is invalid_argument. The server ignores them without the
562 // feature "ci.annotations".
563 repeated Annotation annotations = 3;
564}
565
566// StepUpdate is the state of one step as the runner knows it.
567message StepUpdate {
568 // Number is the 1-based number of the step in the job. It is the number
569 // used by AppendLog.
570 int32 number = 1;
571 // Name is the display name of the step, evaluated: the "name" of the
572 // workflow step, or "Run <first line of the script>", as GitHub shows it.
573 // At most 1,024 characters; the server cuts longer names.
574 string name = 2;
575 // State is where the step is.
576 StepState state = 3;
577 // Conclusion is how the step ended. Required (not UNSPECIFIED) when state is
578 // COMPLETED, must be UNSPECIFIED otherwise. Allowed values: success,
579 // failure, cancelled, skipped.
580 CiConclusion conclusion = 4;
581 // StartedAt is when the step started, by the clock of the runner. Required
582 // from IN_PROGRESS on.
583 google.protobuf.Timestamp started_at = 5;
584 // CompletedAt is when the step ended, by the clock of the runner. Required
585 // when state is COMPLETED.
586 google.protobuf.Timestamp completed_at = 6;
587 // Outcome is how the step ended before continue-on-error was applied, GitHub's
588 // "steps.<id>.outcome", whereas conclusion is the result after it
589 // ("steps.<id>.conclusion"). Same values as conclusion. They differ only for
590 // a step with continue-on-error that failed: outcome FAILURE, conclusion
591 // SUCCESS. Set by the runner when state is COMPLETED and UNSPECIFIED
592 // otherwise; UNSPECIFIED in a COMPLETED step means the same as conclusion
593 // (a runner that does not know the field).
594 CiConclusion outcome = 7;
595}
596
597// UpdateJobResponse is empty.
598message UpdateJobResponse {}
599
600// CompleteJobRequest ends a job.
601message CompleteJobRequest {
602 // JobId is the Job.job_id. It must be the job of the token.
603 string job_id = 1;
604 // Conclusion is how the job ended. Required: a runner reports
605 // CI_CONCLUSION_SUCCESS, CI_CONCLUSION_FAILURE, CI_CONCLUSION_CANCELLED or
606 // CI_CONCLUSION_TIMED_OUT; any other value is invalid_argument. Skipped is
607 // decided by the server and never reported by a runner.
608 CiConclusion conclusion = 2;
609 // Outputs are the outputs of the job, evaluated by the runner. Names are 1
610 // to 255 characters among letters, digits, "_" and "-" and begin with a
611 // letter or "_" and are case-insensitive: two names that differ only by case
612 // are invalid_argument. Each value is at most 1 MiB, and all names and
613 // values together at most 1 MiB (1,048,576 bytes); beyond that the whole call
614 // is rejected with invalid_argument and the runner reports the failure of
615 // the job without outputs, with a line in the log.
616 map<string, string> outputs = 3;
617 // RunnerShutdown is true when the runner ends the job because it was told to
618 // stop (service stopped, signal). Only allowed with CI_CONCLUSION_CANCELLED.
619 // The server then annotates the job with GitHub's message "The runner has
620 // received a shutdown signal. This can happen when the runner service is
621 // stopped, or a manually started runner is canceled." The conclusion
622 // CANCELLED for this case is to be verified against a recording of GitHub.
623 bool runner_shutdown = 4;
624 // Steps are the final steps, with the same meaning as UpdateJobRequest.steps,
625 // applied in the same atomic step as the conclusion.
626 repeated StepUpdate steps = 5;
627 // Annotations are those of the steps that changed since the last call, with
628 // the rules of UpdateJobRequest.annotations (all the annotations of each
629 // step named). The server ignores them without the feature
630 // "ci.annotations".
631 repeated Annotation annotations = 6;
632}
633
634// CompleteJobResponse is the stored result of a job.
635message CompleteJobResponse {
636 // Conclusion is the conclusion of the job as the server stored it. It is the
637 // one of the request when applied is true.
638 CiConclusion conclusion = 1;
639 // Error is the error text stored with the job, as RunJob.error. Empty when
640 // there is none.
641 string error = 2;
642 // Applied is true when this call ended the job, false when the job had
643 // already ended (a retry, or the server ended it first) and nothing changed.
644 bool applied = 3;
645}
646
647// JobEnded says that a job has ended. It is sent in WatchJob and attached, as
648// an error detail, to the failed_precondition errors that RunnerService returns
649// for a job that has ended.
650message JobEnded {
651 // Conclusion is how the job ended.
652 CiConclusion conclusion = 1;
653 // Error is the error text stored with the job, as RunJob.error. Empty when
654 // there is none.
655 string error = 2;
656}
657
658// AppendLogRequest carries log lines of one step.
659message AppendLogRequest {
660 // JobId is the Job.job_id. It must be the job of the token.
661 string job_id = 1;
662 // Step is the number of the step the lines belong to (see
663 // UpdateJobRequest.steps), at least 1, and at least the step of the last
664 // stored line. A step that UpdateJob has not announced yet is created by the
665 // first lines. See the rules on AppendLog.
666 int32 step = 2;
667 // Offset is the number of the first line of the request in the log of the
668 // job, counted from 0 across all steps. See the rules on AppendLog.
669 int64 offset = 3;
670 // Lines are the lines to append, in order: at most 1,000, at most 64 KiB of
671 // text in total.
672 repeated LogLine lines = 4;
673}
674
675// LogLine is one line of a job log.
676message LogLine {
677 // Time is when the line was produced, by the clock of the runner. The server
678 // keeps it with nanosecond precision (RFC 3339) and does not reorder lines.
679 google.protobuf.Timestamp time = 1;
680 // Text is the line without its line terminator, valid UTF-8 (the client
681 // replaces invalid bytes with U+FFFD). It must not contain a line feed; a
682 // carriage return is kept. It is the line as GitHub's raw log has it,
683 // including "##[group]" and "##[endgroup]" markers and "##[error]" lines.
684 string text = 2;
685 // Continued is true when this line is the continuation of the line before
686 // it, which was too long to send whole. A runner cuts a line of more than
687 // 64 KiB into pieces at UTF-8 character boundaries, each piece of at most
688 // 64 KiB, and sets continued on every piece but the first. Each piece is one
689 // line for numbering and offsets; the server and the raw log join the
690 // pieces back, so the original line is rebuilt exactly. The first line of a
691 // job cannot be continued (invalid_argument) and a continued line is in
692 // the same step as the line before it (failed_precondition otherwise).
693 bool continued = 3;
694}
695
696// AppendLogResponse tells how much of the log the server holds.
697message AppendLogResponse {
698 // NextOffset is the number of lines of the job log after the call, which is
699 // the offset of the next request.
700 int64 next_offset = 1;
701}
702
703// HeartbeatRequest names the job that is alive.
704message HeartbeatRequest {
705 // JobId is the Job.job_id. It must be the job of the token.
706 string job_id = 1;
707}
708
709// HeartbeatResponse is empty.
710message HeartbeatResponse {}
711
712// WatchJobRequest names the job to watch.
713message WatchJobRequest {
714 // JobId is the Job.job_id. It must be the job of the token.
715 string job_id = 1;
716}
717
718// WatchJobResponse is one message of the stream.
719message WatchJobResponse {
720 // Msg is what the message carries. New kinds may be added: a client ignores
721 // the ones it does not know.
722 oneof msg {
723 // Keepalive: nothing to do, the stream is alive.
724 Keepalive keepalive = 1;
725 // Cancel: the job must stop.
726 CancelRequest cancel = 2;
727 // Ended: the job has ended. The last message of a stream: a close without
728 // it means the client reconnects.
729 JobEnded ended = 3;
730 }
731}
732
733// Keepalive has no fields.
734message Keepalive {}
735
736// CancelRequest asks the runner to stop a job.
737message CancelRequest {
738 // Reason is why the job must stop. New reasons may be added; a runner that
739 // does not know one cancels the job and reports CI_CONCLUSION_CANCELLED.
740 CancelReason reason = 1;
741 // Message is a short human-readable explanation, for the job log, for
742 // example "Canceled by the owner". At most 1,024 characters.
743 string message = 2;
744 // RequestedAt is when the cancellation was requested, by the clock of the
745 // server.
746 google.protobuf.Timestamp requested_at = 3;
747}
748
749// Annotation is a message attached to a step, and optionally to a place in a
750// file of the repository, like an annotation of a GitHub check run. The runner
751// produces them from the workflow commands ::notice, ::warning and ::error and
752// from problem matchers, after masking.
753//
754// Limits, checked by the server: at most 10 annotations of each level per step
755// (the runner sends no more: the ones beyond only reach the log, as with
756// GitHub). A call that breaks a limit below is rejected whole with
757// invalid_argument. Lengths count characters (Unicode code points), not bytes.
758message Annotation {
759 // Level is the severity. Required.
760 AnnotationLevel level = 1;
761 // Title is the optional title, at most 255 characters. Empty when not given.
762 string title = 2;
763 // Message is the text. Required, not empty, at most 4,096 characters; a
764 // longer one is invalid_argument. The runner never sends a longer one: like
765 // GitHub, it only logs such an annotation.
766 string message = 3;
767 // File is the path the annotation is about, relative to the repository when
768 // the runner can tell, else as given. At most 1,024 characters. Empty when
769 // the annotation is not about a file.
770 string file = 4;
771 // StartLine is the first line, counted from 1. 0 when not given.
772 int32 start_line = 5;
773 // EndLine is the last line, counted from 1, not before start_line. 0 when
774 // not given.
775 int32 end_line = 6;
776 // StartColumn is the first column, counted from 1. 0 when not given.
777 int32 start_column = 7;
778 // EndColumn is the last column, counted from 1. 0 when not given.
779 int32 end_column = 8;
780 // StepNumber is the number of the step that produced it (see
781 // StepUpdate.number), from 1 to 1,000. An annotation of a step inside a
782 // composite action belongs to the step of the workflow that uses the action.
783 int32 step_number = 9;
784}
785
786// ActionRef is an action to resolve.
787message ActionRef {
788 // Uses is the value of "uses" as written, after expression evaluation, for
789 // example "actions/checkout@v4" or "owner/repo/path/to/action@ref". Required.
790 string uses = 1;
791}
792
793// ResolveActionsRequest lists the actions to resolve.
794message ResolveActionsRequest {
795 // JobId is the Job.job_id. It must be the job of the token.
796 string job_id = 1;
797 // Refs are the actions to resolve, at most 100. Duplicates are allowed and
798 // answered one by one.
799 repeated ActionRef refs = 2;
800}
801
802// ResolveActionsResponse holds the resolution of each action.
803message ResolveActionsResponse {
804 // Actions has one entry per ActionRef of the request, in the same order.
805 repeated ResolvedAction actions = 1;
806}
807
808// ResolvedAction is where to download one action, or why it cannot be.
809message ResolvedAction {
810 // Ref is the ActionRef.uses of the request, unchanged.
811 string ref = 1;
812 // NameWithOwner is the repository of the action, "owner/repo", with the
813 // case of the repository as the source gives it. Empty when error is set.
814 string name_with_owner = 2;
815 // Path is the sub-path of the action inside the repository, a clean
816 // relative path: "path/to/action" for "owner/repo/path/to/action@ref", with
817 // "/" separators, no leading or trailing slash, no empty, "." or ".."
818 // element, never absolute. Empty for an action at the root of the
819 // repository.
820 string path = 3;
821 // ResolvedSha is the full id of the commit the ref points to, 40 lowercase
822 // hexadecimal characters for SHA-1 (the commit that an annotated tag points
823 // to, not the tag object). Stable for a run attempt: every job and every
824 // call of the attempt gets the same value for the same ref. Empty when error
825 // is set.
826 string resolved_sha = 4;
827 // TarballUrl is where to download the archive of the repository at
828 // resolved_sha, a gzip-compressed tar with one top-level directory. The
829 // runner downloads it from the sandbox. Empty when error is set.
830 string tarball_url = 5;
831 // Token authenticates the download: the runner sends it as "Authorization:
832 // Basic" with the user "x-access-token". Empty for ACTION_SOURCE_GITHUB.
833 // For ACTION_SOURCE_ERMITE it is a read token for that one repository,
834 // valid until the job ends, so a job can use an action of another private
835 // repository of the server that its own repository token cannot read. It is
836 // issued per job and never shared across jobs (two jobs resolving the same
837 // action get different tokens). It is a secret: the runner masks it, and neither the runner nor the server logs
838 // it.
839 string token = 6;
840 // Source is where the action was found. UNSPECIFIED when error is set.
841 ActionSource source = 7;
842 // Error is why the reference could not be resolved, in the words of GitHub
843 // for the same failure. Empty when the action was resolved, in which case
844 // the other fields describe it.
845 string error = 8;
846}
847
848// UploadSummaryRequest carries the job summary of one step.
849message UploadSummaryRequest {
850 // JobId is the Job.job_id. It must be the job of the token.
851 string job_id = 1;
852 // StepNumber is the number of the step that wrote the summary (see
853 // StepUpdate.number), from 1 to 1,000.
854 int32 step_number = 2;
855 // Markdown is the summary, masked, valid UTF-8, at most 1 MiB (1,048,576
856 // bytes). Empty removes the summary of the step.
857 string markdown = 3;
858}
859
860// UploadSummaryResponse is empty.
861message UploadSummaryResponse {}