anvilsign in

collin/anvil

1//! The wire format between anvil and a job runner (see
2//! `docs/remote-runners.md`).
3//!
4//! Its own crate, depending on nothing but serde, so the runner binary does not
5//! link `anvil-core` (and therefore toasty, SQLite, gix and the rest of the
6//! forge) just to learn the shape of a job.
7//!
8//! The split it encodes: anvil resolves the repo, parses `.anvil/ci.yml`,
9//! checks the image allowlist, opens the secret vault and assembles the shell
10//! script. A runner receives an image, a script, a tar and some limits, and
11//! never parses a pipeline. That keeps this format stable as the pipeline
12//! schema grows.
13
14use serde::{
15 Deserialize,
16 Serialize,
17};
18
19/// Working directory inside the job container, and the root the checkout tar
20/// extracts to. Protocol, not preference: artifact paths are resolved relative
21/// to it on the runner, and `build_tar` roots its entries at it on the server.
22pub const WORKDIR: &str = "/workspace";
23
24/// In-container directory where meta-extractor output lands, one file per
25/// `<artifact>/<key>`. The server writes the redirections into the script; the
26/// runner downloads the directory afterwards. File-per-value sidesteps
27/// quoting and JSON-escaping in shell entirely.
28pub const META_DIR: &str = "/tmp/anvil-meta";
29
30/// Cap on the meta-extractor tar (the values are short strings).
31pub const META_TAR_CAP: u64 = 1024 * 1024;
32
33/// Per-value cap on extractor output, in bytes (after trimming).
34pub const META_VALUE_CAP: usize = 1024;
35
36/// What a runner says about itself when it asks for work.
37#[derive(Clone, Debug, Serialize, Deserialize)]
38pub struct RunnerInfo {
39 /// Operator-chosen, for logs and (later) scheduling. Not a credential.
40 pub name: String,
41 /// The daemon's native platform, e.g. `linux/arm64`. Advertised so the
42 /// dispatcher can eventually prefer a runner that needs no emulation.
43 pub platform: String,
44 pub version: String,
45}
46
47/// One artifact to collect, flattened from `anvil_core::ci::ArtifactSpec`.
48///
49/// The extractor commands themselves are not here — the server has already
50/// baked them into the script. All the runner needs to know is whether to
51/// expect output for this artifact under [`META_DIR`].
52#[derive(Clone, Debug, Serialize, Deserialize)]
53pub struct ArtifactSpec {
54 pub name: String,
55 /// Relative to [`WORKDIR`].
56 pub path: String,
57 pub browse: bool,
58 pub has_meta: bool,
59}
60
61/// Resource bounds and isolation knobs, resolved from `[ci]` server-side.
62///
63/// The runner obeys these; it does not consult a config file of its own for
64/// them. An operator tightening `ci.memory_mb` should not have to redeploy
65/// every runner.
66#[derive(Clone, Debug, Serialize, Deserialize)]
67pub struct Sandbox {
68 pub memory_mb: i64,
69 pub cpus: f64,
70 pub pids_limit: i64,
71 /// 0 disables the wall-clock timeout.
72 pub timeout_secs: u64,
73 pub network: bool,
74 /// Empty keeps the image's default user.
75 pub run_as: String,
76 /// Per-artifact and per-run artifact caps, in MiB; 0 is unlimited.
77 ///
78 /// The per-run budget is charged the *stored* size, which the runner learns
79 /// from the upload response rather than computing — a `browse` directory is
80 /// extracted and any other directory recompressed, so the tar it sent is
81 /// not what lands on disk. Same accounting as the in-process runner did.
82 pub artifact_max_mb: i64,
83 pub artifact_run_max_mb: i64,
84}
85
86/// A claimed job, everything needed to run it.
87///
88/// The checkout is fetched separately (`GET /-/runner/jobs/{run_id}/checkout.tar`)
89/// rather than carried here: base64 in a JSON body inflates a large tree by a
90/// third for no benefit. Secrets *are* carried here, over TLS, so they never
91/// sit behind a separately-fetchable URL.
92#[derive(Clone, Debug, Serialize, Deserialize)]
93pub struct JobSpec {
94 pub run_id: i64,
95 /// Already resolved through `ci.default_image` and checked against
96 /// `ci.allowed_images`.
97 pub image: String,
98 /// `linux/amd64`, `linux/arm64`, … `None` takes the daemon's native
99 /// platform, which is the current behaviour everywhere.
100 pub platform: Option<String>,
101 /// The full `sh -c` program: steps, then the meta-extractor trailer.
102 pub script: String,
103 /// Secret name/value pairs, injected as environment variables.
104 pub env: Vec<(String, String)>,
105 pub artifacts: Vec<ArtifactSpec>,
106 pub sandbox: Sandbox,
107}
108
109/// What storing one artifact produced. Returned by the upload endpoint, and by
110/// the in-process sink, so the runner can charge the run budget the size that
111/// actually landed on disk without knowing how it was laid out.
112#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
113pub struct Stored {
114 pub size: i64,
115 pub is_dir: bool,
116}
117
118/// One artifact the runner pulled out of the container and uploaded. The bytes
119/// went ahead of this; here is the row to record for them.
120#[derive(Clone, Debug, Serialize, Deserialize)]
121pub struct CollectedArtifact {
122 pub name: String,
123 pub size: i64,
124 pub is_dir: bool,
125 pub browse: bool,
126 /// Extractor output for this artifact: a JSON object of key → value.
127 pub meta: String,
128}
129
130/// The outcome of a job, posted once when it finishes.
131///
132/// The whole log arrives in one write, which is exactly what the in-process
133/// runner did — `append_log` was only ever called when the run ended. Live
134/// logs are a follow-up, not a regression.
135#[derive(Clone, Debug, Serialize, Deserialize)]
136pub struct JobResult {
137 pub exit_code: i64,
138 pub log: String,
139 pub artifacts: Vec<CollectedArtifact>,
140 /// The runner could not run the job at all (image pull failed, daemon
141 /// unreachable, timeout). Distinct from a job that ran and exited
142 /// non-zero: this maps to `status::ERROR`, that to `status::FAILURE`.
143 pub runner_error: Option<String>,
144}