1// SPDX-License-Identifier: MIT
2
3syntax = "proto3";
4
5package ermite;
6
7import "google/protobuf/timestamp.proto";
8
9// Artifact is a file that a job of a run uploaded with actions/upload-artifact
10// (artifacts v4): in most cases a zip archive, or a single file uploaded as is
11// ("archive: false"). It belongs to a run, not to one attempt: every job of the
12// run, in any attempt, sees the artifacts of the run. Only finalized artifacts
13// are described; an upload in progress is invisible.
14message Artifact {
15 // Id identifies the artifact on the server: unique across repositories and
16 // runs, never reused, and higher for a later artifact. It is GitHub's
17 // "artifact id" (databaseId), the id of the REST API.
18 int64 id = 1;
19 // Repo is the name of the repository of the run.
20 string repo = 2;
21 // RunId is the run the artifact belongs to.
22 int64 run_id = 3;
23 // Attempt is the attempt of the run whose job uploaded it.
24 int32 attempt = 4;
25 // JobId is the Job.job_id of the job that uploaded it.
26 string job_id = 5;
27 // Name is the name of the artifact, unique among the artifacts of the run:
28 // 1 to 256 characters of valid UTF-8, without control characters, '"', ':',
29 // '<', '>', '|', '*', '?', '\' or '/' (the characters @actions/artifact
30 // refuses).
31 string name = 6;
32 // Size is the number of bytes of the content.
33 int64 size = 7;
34 // Digest is the SHA-256 of the content, "sha256:" and 64 lowercase
35 // hexadecimal characters, as GitHub gives it.
36 string digest = 8;
37 // MediaType is the media type of the content: "application/zip" for an
38 // archive, the type the uploader gave for a file uploaded as is.
39 string media_type = 9;
40 // CreatedAt is when the upload began.
41 google.protobuf.Timestamp created_at = 10;
42 // ExpiresAt is when the server deletes the artifact (see
43 // CreateJobArtifactRequest.expires_at).
44 google.protobuf.Timestamp expires_at = 11;
45}
46
47// JobArtifactService is the artifacts side of a CI runner: what ermite-runner
48// calls when the job's code uploads or downloads artifacts through the
49// results service the runner serves in the sandbox (Twirp
50// github.actions.results.api.v1.ArtifactService and Azure block blobs). The
51// runner translates that protocol; the server stores the content, enforces the
52// limits and decides what the job may see. It is not for people: owners use
53// ArtifactService.
54//
55// Every RPC needs the job token of the job ("Authorization: Bearer
56// ermitejob_..."), and job_id must be the job of the token, with the errors of
57// RunnerService (unauthenticated, permission_denied, and failed_precondition
58// with a JobEnded detail once the job has ended). The run is always the one of
59// the token: no request names a run, so a job can only reach the artifacts of
60// its own run, and the run and job ids that the job's code puts in its Twirp
61// requests are never passed on.
62//
63// An upload goes: CreateJobArtifact (a pending artifact and its id), any number
64// of UploadJobArtifactBlock (in any order and concurrently), one
65// CommitJobArtifact (the order of the blocks: the content), then
66// FinalizeJobArtifact (the artifact becomes visible). Every step is safe to
67// retry after a network error. A pending artifact that is not finalized when
68// its job ends is deleted with its blocks.
69//
70// Limits are settings of the server: the size of one artifact, the space of all
71// the artifacts of a repository (pending uploads included) and the longest
72// retention. A call that would go over a size limit fails with
73// resource_exhausted and a message that names the limit; nothing of the call
74// is kept.
75//
76// Servers that implement this service and ArtifactService list "ci.artifacts"
77// in GetVersionResponse.features; others answer unimplemented. A runner without
78// that feature serves no results service, and artifact actions fail as on a
79// GitHub Enterprise Server without artifacts v4.
80service JobArtifactService {
81 // CreateJobArtifact begins the upload of an artifact of the job's run and
82 // returns its id. Fails with invalid_argument if the name or expires_at break
83 // a rule, already_exists if an artifact of that name was finalized by a job
84 // of the same attempt of the run (the uploader deletes it first to replace
85 // it: DeleteJobArtifact), and resource_exhausted if the artifacts of the
86 // repository already use all their space. A pending artifact of the same
87 // job and name is discarded: a new create is a new upload.
88 rpc CreateJobArtifact(CreateJobArtifactRequest) returns (CreateJobArtifactResponse);
89 // UploadJobArtifactBlock stores one block of a pending artifact. It is a
90 // client stream: the first message names the job, the artifact and the
91 // block, every message may carry data, and the block is the data of all the
92 // messages in order. The block is stored when the client closes the stream
93 // and the server answers; a stream that breaks or fails stores nothing.
94 // Storing a block id again replaces the block, so a retry is safe. Blocks
95 // that the commit does not name are dropped.
96 //
97 // Fails with not_found if the artifact is not a pending artifact of this job,
98 // failed_precondition if it is already committed, invalid_argument if a rule
99 // of the messages is broken, and resource_exhausted if the blocks of the
100 // artifact would exceed the size of one artifact or the space of the
101 // repository.
102 rpc UploadJobArtifactBlock(stream UploadJobArtifactBlockRequest) returns (UploadJobArtifactBlockResponse);
103 // CommitJobArtifact sets the content of a pending artifact: the blocks named,
104 // in that order (Azure's Put Block List). An empty list is an empty content
105 // (a 0-byte upload). The server then knows the size and the digest, and
106 // drops the blocks. A repeat of the same call after a success returns the
107 // same result; a different list on a committed artifact is
108 // failed_precondition. Fails with not_found if the artifact is not a pending
109 // artifact of this job, and invalid_argument if a block id was never
110 // uploaded or the list has more than 50,000 ids.
111 rpc CommitJobArtifact(CommitJobArtifactRequest) returns (CommitJobArtifactResponse);
112 // FinalizeJobArtifact makes the latest committed artifact of the job with
113 // that name visible to the run, and returns it. An artifact of the same name
114 // from an earlier attempt of the run is deleted at the same moment (a re-run
115 // replaces what the first attempt uploaded). A repeat after a success
116 // returns the same artifact. Fails with not_found if the job has no pending
117 // artifact of that name, failed_precondition if it is not committed,
118 // invalid_argument if digest is set and differs from the content, and
119 // already_exists if a job of the same attempt finalized an artifact of that
120 // name in the meantime.
121 rpc FinalizeJobArtifact(FinalizeJobArtifactRequest) returns (FinalizeJobArtifactResponse);
122 // ListJobArtifacts lists the finalized, unexpired artifacts of the job's run,
123 // all attempts, by id. An empty list is not an error.
124 rpc ListJobArtifacts(ListJobArtifactsRequest) returns (ListJobArtifactsResponse);
125 // DownloadJobArtifact streams the content of an artifact of the job's run.
126 // The first message carries the artifact, then the chunks follow in order
127 // from offset. A client that loses the stream calls again with the offset
128 // after the last byte it received. Fails with not_found if the artifact is
129 // not a finalized artifact of the run (or has expired), and out_of_range if
130 // offset is beyond its size.
131 rpc DownloadJobArtifact(DownloadJobArtifactRequest) returns (stream DownloadJobArtifactResponse);
132 // DeleteJobArtifact deletes the finalized artifact of that name of the job's
133 // run (upload-artifact's "overwrite", and its merge action's
134 // "delete-merged"). Fails with not_found if there is none.
135 rpc DeleteJobArtifact(DeleteJobArtifactRequest) returns (DeleteJobArtifactResponse);
136}
137
138// CreateJobArtifactRequest begins an upload.
139message CreateJobArtifactRequest {
140 // JobId is the Job.job_id. It must be the job of the token.
141 string job_id = 1;
142 // Name is the name of the artifact, see Artifact.name.
143 string name = 2;
144 // MediaType is the media type of a file uploaded as is (the toolkit's
145 // mime_type), at most 255 characters. Empty means "application/zip".
146 string media_type = 3;
147 // ExpiresAt is when the artifact should be deleted (the toolkit's
148 // expires_at, from "retention-days"). Unset means the default retention of
149 // the server. The server keeps it between one day from now and its longest
150 // retention, and returns the time it kept.
151 google.protobuf.Timestamp expires_at = 4;
152}
153
154// CreateJobArtifactResponse identifies the pending artifact.
155message CreateJobArtifactResponse {
156 // ArtifactId is the id the artifact will have once finalized (Artifact.id).
157 int64 artifact_id = 1;
158 // ExpiresAt is the expiry the server kept.
159 google.protobuf.Timestamp expires_at = 2;
160}
161
162// UploadJobArtifactBlockRequest is one message of the stream of a block.
163message UploadJobArtifactBlockRequest {
164 // JobId is the Job.job_id, in the first message only. It must be the job of
165 // the token.
166 string job_id = 1;
167 // ArtifactId is the CreateJobArtifactResponse.artifact_id, in the first
168 // message only.
169 int64 artifact_id = 2;
170 // BlockId identifies the block within the artifact: 1 to 64 bytes (Azure's
171 // block id, decoded from base64), in the first message only.
172 bytes block_id = 3;
173 // Data is the next part of the block, at most 1 MiB (1,048,576 bytes). Any
174 // message may have none.
175 bytes data = 4;
176}
177
178// UploadJobArtifactBlockResponse confirms a stored block.
179message UploadJobArtifactBlockResponse {
180 // Size is the number of bytes of the block as stored.
181 int64 size = 1;
182}
183
184// CommitJobArtifactRequest sets the content of a pending artifact.
185message CommitJobArtifactRequest {
186 // JobId is the Job.job_id. It must be the job of the token.
187 string job_id = 1;
188 // ArtifactId is the CreateJobArtifactResponse.artifact_id.
189 int64 artifact_id = 2;
190 // BlockIds are the blocks of the content, in order, each one as given to
191 // UploadJobArtifactBlock. At most 50,000; the same id may appear more than
192 // once.
193 repeated bytes block_ids = 3;
194}
195
196// CommitJobArtifactResponse describes the committed content.
197message CommitJobArtifactResponse {
198 // Size is the number of bytes of the content.
199 int64 size = 1;
200 // Digest is the SHA-256 of the content, see Artifact.digest.
201 string digest = 2;
202}
203
204// FinalizeJobArtifactRequest ends an upload.
205message FinalizeJobArtifactRequest {
206 // JobId is the Job.job_id. It must be the job of the token.
207 string job_id = 1;
208 // Name is the name given to CreateJobArtifact. The toolkit finalizes by name.
209 string name = 2;
210 // Size is the size the uploader counted. Informational: the server keeps the
211 // size of the content it stored, and only logs a difference.
212 int64 size = 3;
213 // Digest is the digest the uploader computed ("sha256:<hex>"), or empty.
214 // When set it must be the digest of the content.
215 string digest = 4;
216}
217
218// FinalizeJobArtifactResponse describes the artifact now visible.
219message FinalizeJobArtifactResponse {
220 // Artifact is the finalized artifact.
221 Artifact artifact = 1;
222}
223
224// ListJobArtifactsRequest selects artifacts of the job's run.
225message ListJobArtifactsRequest {
226 // JobId is the Job.job_id. It must be the job of the token.
227 string job_id = 1;
228 // Name keeps only the artifact of that name. Empty means every name.
229 string name = 2;
230 // ArtifactId keeps only the artifact of that id. 0 means every id.
231 int64 artifact_id = 3;
232}
233
234// ListJobArtifactsResponse lists artifacts of the job's run.
235message ListJobArtifactsResponse {
236 // Artifacts are the artifacts, by id.
237 repeated Artifact artifacts = 1;
238}
239
240// DownloadJobArtifactRequest selects the content of an artifact.
241message DownloadJobArtifactRequest {
242 // JobId is the Job.job_id. It must be the job of the token.
243 string job_id = 1;
244 // ArtifactId is the Artifact.id of an artifact of the job's run.
245 int64 artifact_id = 2;
246 // Offset is the first byte to send, from 0 to the size of the artifact.
247 int64 offset = 3;
248}
249
250// DownloadJobArtifactResponse is one message of the content stream.
251message DownloadJobArtifactResponse {
252 // Artifact describes the artifact. Set in the first message of the stream
253 // only.
254 Artifact artifact = 1;
255 // Chunk is the next part of the content, at most 1 MiB, in order. The first
256 // message may have an empty chunk. The chunks of a stream add up to the size
257 // of the artifact minus the offset.
258 bytes chunk = 2;
259}
260
261// DeleteJobArtifactRequest names the artifact to delete.
262message DeleteJobArtifactRequest {
263 // JobId is the Job.job_id. It must be the job of the token.
264 string job_id = 1;
265 // Name is the name of a finalized artifact of the job's run.
266 string name = 2;
267}
268
269// DeleteJobArtifactResponse identifies the deleted artifact.
270message DeleteJobArtifactResponse {
271 // ArtifactId is the Artifact.id of the artifact that was deleted.
272 int64 artifact_id = 1;
273}
274
275// ArtifactService lets people list, download and delete the artifacts of the
276// runs of a repository (run page, `ermite run download`). Listing and
277// downloading need a signed-in caller with read access to the repository (the
278// repo:read scope for a token), even for a public repository: like GitHub,
279// artifacts are not served anonymously. Deleting needs the ci:write scope. A
280// repository or a run that does not exist or that the caller cannot read gives
281// not_found.
282//
283// Artifacts are deleted when they expire (Artifact.expires_at), with their run
284// (run retention) and with their repository.
285//
286// Only servers with the feature "ci.artifacts" implement it (unimplemented
287// otherwise).
288service ArtifactService {
289 // ListArtifacts lists the artifacts of a run, by id, with the space that the
290 // artifacts of the repository use.
291 rpc ListArtifacts(ListArtifactsRequest) returns (ListArtifactsResponse);
292 // DownloadArtifact streams the content of an artifact, as
293 // JobArtifactService.DownloadJobArtifact does. Fails with not_found if the
294 // artifact is not an artifact of the repository, and out_of_range if offset
295 // is beyond its size.
296 rpc DownloadArtifact(DownloadArtifactRequest) returns (stream DownloadArtifactResponse);
297 // DeleteArtifact deletes an artifact. Fails with not_found if it is not an
298 // artifact of the repository.
299 rpc DeleteArtifact(DeleteArtifactRequest) returns (DeleteArtifactResponse);
300}
301
302// ListArtifactsRequest selects the artifacts of a run.
303message ListArtifactsRequest {
304 // Repo is the repository name.
305 string repo = 1;
306 // RunId is the run.
307 int64 run_id = 2;
308}
309
310// ListArtifactsResponse lists the artifacts of a run.
311message ListArtifactsResponse {
312 // Artifacts are the artifacts of the run, all attempts, by id.
313 repeated Artifact artifacts = 1;
314 // UsedBytes is the space that all the artifacts of the repository use,
315 // uploads in progress included.
316 int64 used_bytes = 2;
317 // QuotaBytes is the space the artifacts of the repository may use.
318 int64 quota_bytes = 3;
319}
320
321// DownloadArtifactRequest selects the content of an artifact.
322message DownloadArtifactRequest {
323 // Repo is the repository name.
324 string repo = 1;
325 // ArtifactId is the Artifact.id.
326 int64 artifact_id = 2;
327 // Offset is the first byte to send, from 0 to the size of the artifact.
328 int64 offset = 3;
329}
330
331// DownloadArtifactResponse is one message of the content stream.
332message DownloadArtifactResponse {
333 // Artifact describes the artifact. Set in the first message of the stream
334 // only.
335 Artifact artifact = 1;
336 // Chunk is the next part of the content, at most 1 MiB, in order. The first
337 // message may have an empty chunk.
338 bytes chunk = 2;
339}
340
341// DeleteArtifactRequest names the artifact to delete.
342message DeleteArtifactRequest {
343 // Repo is the repository name.
344 string repo = 1;
345 // ArtifactId is the Artifact.id.
346 int64 artifact_id = 2;
347}
348
349// DeleteArtifactResponse is empty.
350message DeleteArtifactResponse {}