1// SPDX-License-Identifier: MIT
2
3// Internal wire format between the host process of ermite-runner and the
4// executor running inside a sandbox (vsock in a micro-VM, Unix socket in a
5// container). Frames are length-prefixed on the stream (see frame.go); this file
6// defines the payload. It is internal to ermite-runner: no compatibility is
7// promised between runner versions, which is safe because the host attaches its
8// own executable to every sandbox.
9
10syntax = "proto3";
11
12package ermite.runner.exec.v1;
13
14option go_package = "bnema.dev/ermite-runner/internal/execstream/execpb";
15
16// Frame is the only message on the stream.
17message Frame {
18 oneof body {
19 Hello hello = 1; // executor -> host, first frame
20 Run run = 2; // host -> executor, after Hello
21 Output output = 3; // executor -> host
22 Exit exit = 4; // executor -> host, last frame
23 Cancel cancel = 5; // host -> executor
24 StepStarted step_started = 6; // executor -> host
25 StepCompleted step_completed = 7; // executor -> host
26 Log log = 8; // executor -> host
27 JobResult job_result = 9; // executor -> host, before Exit
28 StepAnnotations step_annotations = 10; // executor -> host, before the StepCompleted of the step
29 StepSummary step_summary = 11; // executor -> host, before the StepCompleted of the step
30 ResolveRequest resolve_request = 12; // executor -> host
31 ResolveResponse resolve_response = 13; // host -> executor, answers a ResolveRequest
32 NamedValue secret = 14; // host -> executor, one per secret, before Run
33 NamedValue var = 15; // host -> executor, one per variable, before Run
34 }
35}
36
37// NamedValue is one secret (Job.secrets) or one variable (Job.vars) of the job.
38// They travel in frames of their own, before Run, rather than in JobSpec: a
39// frame is at most 2 MiB (execstream.MaxFrame) and ermite allows many secrets of
40// 48 KiB, so a map in Run could not carry them. The executor keeps them until
41// Run arrives. Secrets are never logged nor written to disk.
42message NamedValue {
43 string name = 1;
44 string value = 2;
45}
46
47message Hello {
48 string version = 1; // runner version of the executor
49 string sandbox = 2; // "vm" or "container"
50 string hostname = 3;
51}
52
53// Run asks the executor to run a script or a job. Without job it is the stub
54// executor of sandbox-check: the script is run with `sh -c`; an empty script
55// means the built-in diagnostics script. With job the executor runs the job
56// like a GitHub Actions runner and reports it with StepStarted, Log,
57// StepCompleted and JobResult frames, then Exit.
58message Run {
59 string script = 1;
60 map<string, string> env = 2;
61 JobSpec job = 3;
62}
63
64// JobSpec is the job to run: the fields of the job that ermite handed to the
65// host process, minus the tokens (the sandbox never sees them).
66message JobSpec {
67 string workflow_path = 1;
68 string workflow_yaml = 2;
69 string job_key = 3;
70 string matrix = 4; // JSON object
71 string contexts = 5; // JSON object
72 string repo = 6; // repository name
73 int32 timeout_minutes = 7;
74 string runner_name = 8;
75 string runner_version = 9;
76 // runner_environment is the runner.environment value ("github-hosted" or "self-hosted").
77 string runner_environment = 10;
78 bool debug = 11; // ACTIONS_STEP_DEBUG
79 // deadline_ms is what is left of the job timeout when the host sends this
80 // Run, in milliseconds: the timeout counts from the moment the host received
81 // the job (runner.proto: Job.timeout_minutes), not from the start of the
82 // executor, and booting the sandbox takes time. 0 means "use
83 // timeout_minutes from now".
84 int64 deadline_ms = 12;
85 // send_annotations and send_summaries say that the server keeps annotations
86 // (feature ci.annotations) and step summaries (ci.summaries). The executor
87 // sends StepAnnotations and StepSummary frames only when they are set.
88 bool send_annotations = 13;
89 bool send_summaries = 14;
90 // cache_port and cache_token, when set, are the runner's cache service for
91 // this job. The service is not on the network: the executor starts a relay
92 // on the sandbox's own loopback, 127.0.0.1:<cache_port>, that carries each
93 // connection over the host channel (a vsock port, or a Unix socket next to
94 // the executor's) to the service. The executor gives
95 // ACTIONS_CACHE_URL=http://127.0.0.1:<port>/ and the token as
96 // ACTIONS_RUNTIME_TOKEN to node and docker actions only, as the official
97 // runner does, and masks the token. The token is not an ermite credential: it
98 // is an HMAC that only the runner's cache service accepts, scoped to this
99 // job's repository and refs.
100 uint32 cache_port = 15;
101 string cache_token = 16;
102 // Job.secrets and Job.vars are not here: they come in NamedValue frames
103 // before Run (see NamedValue). The executor takes the vars of those frames
104 // for the "vars" context when contexts has none.
105 reserved 17, 18;
106 reserved "secrets", "vars";
107 // secrets_withheld is set by the host when the server sends Job.trusted and
108 // the job is not trusted (feature ci.secrets): "Set up job" then says that
109 // secrets are not available. Never set for servers without the feature, for
110 // which an absent Job.trusted only means "not sent".
111 bool secrets_withheld = 19;
112 // token_permissions are the effective permissions of the job's GITHUB_TOKEN
113 // as the server sent them (Job.permissions, feature ci.secrets): "Set up job"
114 // prints them instead of working them out from the workflow. Unset when the
115 // server sent none (an older server): the executor then reads them from the
116 // workflow.
117 TokenPermissions token_permissions = 20;
118}
119
120// TokenPermissions maps a permission key ("contents", "pull-requests", ...) to
121// "read", "write" or "none"; a key that is absent is none.
122message TokenPermissions {
123 map<string, string> levels = 1;
124}
125
126enum StreamKind {
127 STREAM_KIND_UNSPECIFIED = 0;
128 STREAM_KIND_STDOUT = 1;
129 STREAM_KIND_STDERR = 2;
130}
131
132message Output {
133 StreamKind stream = 1;
134 bytes data = 2;
135 int64 unix_nano = 3;
136}
137
138message Exit {
139 int32 code = 1;
140 string error = 2; // set when the executor failed to run the script at all
141}
142
143// Cancel asks the executor to stop the running script. The executor escalates
144// like the official runner: SIGINT to the script's process group at once,
145// SIGTERM 7.5 s later, SIGKILL 2.5 s after that.
146message Cancel {
147 // reason is for the job log and diagnostics ("cancelled by the user",
148 // "job timeout", ...); it never changes the behaviour.
149 string reason = 1;
150 // kind tells the job executor how the job ends: cancelled, timed out, or
151 // cancelled because the runner shuts down. The stub executor ignores it.
152 CancelKind kind = 2;
153}
154
155enum CancelKind {
156 CANCEL_KIND_UNSPECIFIED = 0; // same as USER
157 CANCEL_KIND_USER = 1;
158 CANCEL_KIND_TIMEOUT = 2;
159 CANCEL_KIND_SHUTDOWN = 3;
160}
161
162// Steps are numbered like GitHub's log: "Set up job" is 1, the steps of the
163// workflow follow, "Complete job" is last.
164message StepStarted {
165 int32 number = 1;
166 string name = 2;
167 int64 unix_nano = 3;
168}
169
170message StepCompleted {
171 int32 number = 1;
172 string name = 2;
173 StepConclusion conclusion = 3;
174 int64 unix_nano = 4;
175 // outcome is steps.<id>.outcome: the result before continue-on-error. It
176 // differs from conclusion only for a failed step with continue-on-error.
177 // UNSPECIFIED means the same as conclusion.
178 StepConclusion outcome = 5;
179}
180
181enum AnnotationLevel {
182 ANNOTATION_LEVEL_UNSPECIFIED = 0;
183 ANNOTATION_LEVEL_NOTICE = 1;
184 ANNOTATION_LEVEL_WARNING = 2;
185 ANNOTATION_LEVEL_FAILURE = 3;
186}
187
188// Annotation is one ::notice, ::warning or ::error (or a problem matcher
189// match), masked. The host validates every field again before it sends them.
190message Annotation {
191 AnnotationLevel level = 1;
192 string title = 2;
193 string message = 3;
194 string file = 4; // relative to the repository when the file is in it
195 int32 start_line = 5;
196 int32 end_line = 6;
197 int32 start_column = 7;
198 int32 end_column = 8;
199}
200
201// StepAnnotations is the complete list of the annotations of a step (the ones
202// the runner keeps: at most 10 per level), replacing any earlier list of that
203// step. Annotations of the steps inside a composite action are those of the
204// step of the workflow that uses it.
205message StepAnnotations {
206 int32 step = 1;
207 repeated Annotation annotations = 2;
208}
209
210// StepSummary is the masked content of GITHUB_STEP_SUMMARY after a step, at
211// most 1 MiB.
212message StepSummary {
213 int32 step = 1;
214 string markdown = 2;
215}
216
217enum StepConclusion {
218 STEP_CONCLUSION_UNSPECIFIED = 0;
219 STEP_CONCLUSION_SUCCESS = 1;
220 STEP_CONCLUSION_FAILURE = 2;
221 STEP_CONCLUSION_CANCELLED = 3;
222 STEP_CONCLUSION_SKIPPED = 4;
223 STEP_CONCLUSION_TIMED_OUT = 5; // jobs only
224}
225
226// Log is one line of the job log, without terminator, valid UTF-8. A line of
227// more than 64 KiB is cut by the executor into pieces at character boundaries;
228// every piece but the first has continued set (the AppendLog rule).
229message Log {
230 int32 step = 1;
231 int64 unix_nano = 2;
232 string text = 3;
233 bool continued = 4;
234}
235
236// JobResult is the outcome of the job; Exit follows.
237message JobResult {
238 StepConclusion conclusion = 1;
239 map<string, string> outputs = 2;
240 string error = 3;
241 bool runner_shutdown = 4;
242}
243
244// ResolveRequest asks the host to resolve actions with RunnerService.
245// ResolveActions: the job token stays in the host, so the executor cannot call
246// it. The executor sends one per level of composite actions (the runner's own
247// "one more round per level") and waits for the ResolveResponse with the same id.
248message ResolveRequest {
249 int64 id = 1;
250 repeated string uses = 2; // "owner/repo[/path]@ref", at most 100
251}
252
253// ResolveResponse answers a ResolveRequest. error is set when the call itself
254// failed (the executor fails the job); actions has one entry per uses otherwise.
255message ResolveResponse {
256 int64 id = 1;
257 string error = 2;
258 repeated ResolvedAction actions = 3;
259}
260
261// ResolvedAction mirrors ermite.ResolvedAction (the token is a secret: the
262// executor masks it).
263message ResolvedAction {
264 string ref = 1;
265 string name_with_owner = 2;
266 string path = 3;
267 string resolved_sha = 4;
268 string tarball_url = 5;
269 string token = 6;
270 string source = 7;
271 string error = 8;
272 // auth_origin (scheme://host[:port], as the sandbox reaches it) is the only
273 // origin the token may be sent to; empty means the token is never sent. The
274 // host sets it for archives served by the ermite server, from its own
275 // configuration, never from the URL alone.
276 string auth_origin = 9;
277}