v0.15.0 e4b332d

runner.proto RawHistoryFolder

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

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