• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

google / alioth / 19401636943

16 Nov 2025 06:33AM UTC coverage: 26.349% (+0.01%) from 26.337%
19401636943

Pull #329

github

web-flow
Merge 5e14ea4ba into a844b2ad9
Pull Request #329: fw-cfg device refactor

396 of 648 branches covered (61.11%)

Branch coverage included in aggregate %.

0 of 83 new or added lines in 11 files covered. (0.0%)

287 existing lines in 8 files now uncovered.

3564 of 14381 relevant lines covered (24.78%)

21.75 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/alioth-cli/src/boot.rs
1
// Copyright 2025 Google LLC
2
//
3
// Licensed under the Apache License, Version 2.0 (the "License");
4
// you may not use this file except in compliance with the License.
5
// You may obtain a copy of the License at
6
//
7
//     https://www.apache.org/licenses/LICENSE-2.0
8
//
9
// Unless required by applicable law or agreed to in writing, software
10
// distributed under the License is distributed on an "AS IS" BASIS,
11
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
// See the License for the specific language governing permissions and
13
// limitations under the License.
14

15
use std::collections::HashMap;
16
use std::ffi::CString;
17
#[cfg(target_arch = "x86_64")]
18
use std::fs::File;
19
use std::path::{Path, PathBuf};
20

21
use alioth::board::BoardConfig;
22
#[cfg(target_arch = "x86_64")]
23
use alioth::device::fw_cfg::FwCfgItemParam;
24
use alioth::errors::{DebugTrace, trace_error};
25
#[cfg(target_os = "macos")]
26
use alioth::hv::Hvf;
27
use alioth::hv::{self, Coco};
28
#[cfg(target_os = "linux")]
29
use alioth::hv::{Kvm, KvmConfig};
30
use alioth::loader::{ExecType, Payload};
31
use alioth::mem::{MemBackend, MemConfig};
32
#[cfg(target_os = "linux")]
33
use alioth::vfio::{CdevParam, ContainerParam, GroupParam, IoasParam};
34
#[cfg(target_os = "linux")]
35
use alioth::virtio::DeviceId;
36
use alioth::virtio::dev::balloon::BalloonParam;
37
use alioth::virtio::dev::blk::BlkFileParam;
38
use alioth::virtio::dev::entropy::EntropyParam;
39
use alioth::virtio::dev::fs::shared_dir::SharedDirParam;
40
#[cfg(target_os = "linux")]
41
use alioth::virtio::dev::fs::vu::VuFsParam;
42
#[cfg(target_os = "linux")]
43
use alioth::virtio::dev::net::tap::NetTapParam;
44
#[cfg(target_os = "macos")]
45
use alioth::virtio::dev::net::vmnet::NetVmnetParam;
46
use alioth::virtio::dev::vsock::UdsVsockParam;
47
#[cfg(target_os = "linux")]
48
use alioth::virtio::dev::vsock::VhostVsockParam;
49
#[cfg(target_os = "linux")]
50
use alioth::virtio::vu::frontend::VuFrontendParam;
51
use alioth::virtio::worker::WorkerApi;
52
use alioth::vm::Machine;
53
use clap::Args;
54
use serde::Deserialize;
55
use serde_aco::{Help, help_text};
56
use snafu::{ResultExt, Snafu};
57

58
use crate::objects::{DOC_OBJECTS, parse_objects};
59

60
#[trace_error]
UNCOV
61
#[derive(Snafu, DebugTrace)]
×
62
#[snafu(module, context(suffix(false)))]
63
pub enum Error {
64
    #[snafu(display("Failed to parse {arg}"))]
65
    ParseArg {
66
        arg: String,
67
        error: serde_aco::Error,
68
    },
69
    #[snafu(display("Failed to parse objects"), context(false))]
70
    ParseObjects { source: crate::objects::Error },
71
    #[cfg(target_os = "linux")]
72
    #[snafu(display("Failed to access system hypervisor"))]
73
    Hypervisor { source: alioth::hv::Error },
74
    #[snafu(display("Failed to create a VM"))]
75
    CreateVm { source: alioth::vm::Error },
76
    #[snafu(display("Failed to create a device"))]
77
    CreateDevice { source: alioth::vm::Error },
78
    #[cfg(target_arch = "x86_64")]
79
    #[snafu(display("Failed to open {path:?}"))]
80
    OpenFile {
81
        path: Box<Path>,
82
        error: std::io::Error,
83
    },
84
    #[cfg(target_arch = "x86_64")]
85
    #[snafu(display("Failed to configure the fw-cfg device"))]
86
    FwCfg { error: std::io::Error },
87
    #[snafu(display("Failed to boot a VM"))]
88
    BootVm { source: alioth::vm::Error },
89
    #[snafu(display("VM did not shutdown peacefully"))]
90
    WaitVm { source: alioth::vm::Error },
91
}
92

93
#[derive(Debug, Deserialize, Clone, Help)]
94
#[cfg_attr(target_os = "macos", derive(Default))]
95
enum Hypervisor {
96
    /// KVM backed by the Linux kernel.
97
    #[cfg(target_os = "linux")]
98
    #[serde(alias = "kvm")]
99
    Kvm(KvmConfig),
100
    /// macOS Hypervisor Framework.
101
    #[cfg(target_os = "macos")]
102
    #[serde(alias = "hvf")]
103
    #[default]
104
    Hvf,
105
}
106

107
#[cfg(target_os = "linux")]
108
impl Default for Hypervisor {
UNCOV
109
    fn default() -> Self {
×
UNCOV
110
        Hypervisor::Kvm(KvmConfig::default())
×
UNCOV
111
    }
×
112
}
113

114
#[derive(Debug, Deserialize, Clone, Help)]
115
enum FsParam {
116
    /// VirtIO FS device backed by a shared directory.
117
    #[serde(alias = "dir")]
118
    Dir(SharedDirParam),
119
    #[cfg(target_os = "linux")]
120
    /// VirtIO FS device backed by a vhost-user process, e.g. virtiofsd.
121
    #[serde(alias = "vu")]
122
    Vu(VuFsParam),
123
}
124

125
#[derive(Debug, Deserialize, Clone, Help)]
126
enum VsockParam {
127
    #[cfg(target_os = "linux")]
128
    /// Vsock device backed by host kernel vhost-vsock module.
129
    #[serde(alias = "vhost")]
130
    Vhost(VhostVsockParam),
131
    /// Vsock device mapped to a Unix domain socket.
132
    #[serde(alias = "uds")]
133
    Uds(UdsVsockParam),
134
}
135

136
#[cfg(target_os = "linux")]
137
#[derive(Deserialize, Help)]
138
struct VuSocket {
139
    socket: Box<Path>,
140
}
141

142
#[derive(Deserialize, Help)]
143
enum NetParam {
144
    /// VirtIO net device backed by TUN/TAP, MacVTap, or IPVTap.
145
    #[cfg(target_os = "linux")]
146
    #[serde(alias = "tap")]
147
    Tap(NetTapParam),
148
    /// VirtIO net device backed by vmnet framework.
149
    #[cfg(target_os = "macos")]
150
    #[serde(alias = "vmnet")]
151
    Vmnet(NetVmnetParam),
152
    /// vhost-user net device over a Unix domain socket.
153
    #[cfg(target_os = "linux")]
154
    #[serde(alias = "vu")]
155
    Vu(VuSocket),
156
}
157

158
#[derive(Deserialize, Help)]
159
enum BlkParam {
160
    /// VirtIO block device backed a disk image file.
161
    #[serde(alias = "file")]
162
    File(BlkFileParam),
163
    #[cfg(target_os = "linux")]
164
    #[serde(alias = "vu")]
165
    /// vhost-user block device over a Unix domain socket.
166
    Vu(VuSocket),
167
}
168

169
#[derive(Args, Debug, Clone)]
170
#[command(arg_required_else_help = true, alias("run"))]
171
pub struct BootArgs {
172
    #[arg(long, help(
173
        help_text::<Hypervisor>("Specify the Hypervisor to run on.")
174
    ), value_name = "HV")]
175
    hypervisor: Option<String>,
176

177
    /// Path to a Linux kernel image.
178
    #[arg(short, long, value_name = "PATH")]
179
    kernel: Option<Box<Path>>,
180

181
    /// Path to an ELF kernel with PVH note.
182
    #[cfg(target_arch = "x86_64")]
183
    #[arg(long, value_name = "PATH")]
184
    pvh: Option<Box<Path>>,
185

186
    /// Path to a firmware image.
187
    #[arg(long, short, value_name = "PATH")]
188
    firmware: Option<Box<Path>>,
189

190
    /// Command line to pass to the kernel, e.g. `console=ttyS0`.
191
    #[arg(short, long, alias = "cmd-line", value_name = "ARGS")]
192
    cmdline: Option<CString>,
193

194
    /// Path to an initramfs image.
195
    #[arg(short, long, value_name = "PATH")]
196
    initramfs: Option<Box<Path>>,
197

198
    /// Number of VCPUs assigned to the guest.
199
    #[arg(long, default_value_t = 1)]
200
    num_cpu: u32,
201

202
    /// DEPRECATED: Use --memory instead.
203
    #[arg(long, default_value = "1G")]
204
    mem_size: String,
205

206
    #[arg(short, long, help(
207
        help_text::<MemConfig>("Specify the memory of the guest.")
208
    ))]
209
    memory: Option<String>,
210

211
    /// Add a pvpanic device.
212
    #[arg(long)]
213
    pvpanic: bool,
214

215
    #[cfg(target_arch = "x86_64")]
216
    #[arg(long = "fw-cfg", help(
217
        help_text::<FwCfgItemParam>("Add an extra item to the fw_cfg device.")
218
    ), value_name = "ITEM")]
219
    fw_cfgs: Vec<String>,
220

221
    /// Add a VirtIO entropy device.
222
    #[arg(long)]
223
    entropy: bool,
224

225
    #[arg(long, help(
226
        help_text::<NetParam>("Add a VirtIO net device.")
227
    ))]
228
    net: Vec<String>,
229

230
    #[arg(long, help(
231
        help_text::<BlkParam>("Add a VirtIO block device.")
232
    ))]
233
    blk: Vec<String>,
234

235
    #[arg(long, help(
236
        help_text::<Coco>("Enable confidential compute supported by host platform.")
237
    ))]
238
    coco: Option<String>,
239

240
    #[arg(long, help(
241
        help_text::<FsParam>("Add a VirtIO filesystem device.")
242
    ))]
243
    fs: Vec<String>,
244

245
    #[arg(long, help(
246
        help_text::<VsockParam>("Add a VirtIO vsock device.")
247
    ))]
248
    vsock: Option<String>,
249

250
    #[cfg(target_os = "linux")]
251
    #[arg(long, help(help_text::<CdevParam>(
252
        "Assign a host PCI device to the guest using IOMMUFD API."
253
    ) ))]
254
    vfio_cdev: Vec<String>,
255

256
    #[cfg(target_os = "linux")]
257
    #[arg(long, help(help_text::<IoasParam>("Create a new IO address space.")))]
258
    vfio_ioas: Vec<String>,
259

260
    #[cfg(target_os = "linux")]
261
    #[arg(long, help(help_text::<GroupParam>(
262
        "Assign a host PCI device to the guest using legacy VFIO API."
263
    )))]
264
    vfio_group: Vec<String>,
265

266
    #[cfg(target_os = "linux")]
267
    #[arg(long, help(help_text::<ContainerParam>("Add a new VFIO container.")))]
268
    vfio_container: Vec<String>,
269

270
    #[arg(long)]
271
    #[arg(long, help(help_text::<BalloonParam>("Add a VirtIO balloon device.")))]
272
    balloon: Option<String>,
273

274
    #[arg(short, long("object"), help = DOC_OBJECTS, value_name = "OBJECT")]
275
    objects: Vec<String>,
276
}
277

278
fn add_net<H>(
×
UNCOV
279
    vm: &Machine<H>,
×
280
    args: Vec<String>,
×
281
    objects: &HashMap<&str, &str>,
×
282
) -> Result<(), Error>
×
UNCOV
283
where
×
UNCOV
284
    H: hv::Hypervisor + 'static,
×
285
{
286
    for (index, arg) in args.into_iter().enumerate() {
×
287
        #[cfg(target_os = "linux")]
288
        let param: NetParam = match serde_aco::from_args(&arg, objects) {
×
UNCOV
289
            Ok(p) => p,
×
290
            Err(_) => {
UNCOV
291
                let tap_param = serde_aco::from_args::<NetTapParam>(&arg, objects)
×
292
                    .context(error::ParseArg { arg })?;
×
293
                NetParam::Tap(tap_param)
×
294
            }
295
        };
296
        #[cfg(target_os = "macos")]
297
        let param: NetParam =
×
UNCOV
298
            serde_aco::from_args(&arg, objects).context(error::ParseArg { arg })?;
×
UNCOV
299
        match param {
×
300
            #[cfg(target_os = "linux")]
UNCOV
301
            NetParam::Tap(tap_param) => vm.add_virtio_dev(format!("virtio-net-{index}"), tap_param),
×
302
            #[cfg(target_os = "linux")]
UNCOV
303
            NetParam::Vu(sock) => {
×
304
                let param = VuFrontendParam {
×
305
                    id: DeviceId::Net,
×
UNCOV
306
                    socket: sock.socket,
×
307
                };
×
308
                vm.add_virtio_dev(format!("vu-net-{index}"), param)
×
309
            }
310
            #[cfg(target_os = "macos")]
311
            NetParam::Vmnet(p) => vm.add_virtio_dev(format!("virtio-net-{index}"), p),
×
312
        }
313
        .context(error::CreateDevice)?;
×
314
    }
315
    Ok(())
×
316
}
×
317

318
fn add_blk<H>(
×
319
    vm: &Machine<H>,
×
UNCOV
320
    args: Vec<String>,
×
321
    objects: &HashMap<&str, &str>,
×
322
) -> Result<(), Error>
×
NEW
323
where
×
NEW
324
    H: hv::Hypervisor + 'static,
×
325
{
UNCOV
326
    for (index, opt) in args.into_iter().enumerate() {
×
UNCOV
327
        let param: BlkParam = match serde_aco::from_args(&opt, objects) {
×
UNCOV
328
            Ok(param) => param,
×
UNCOV
329
            Err(_) => match serde_aco::from_args(&opt, objects) {
×
330
                Ok(param) => BlkParam::File(param),
×
331
                Err(_) => {
UNCOV
332
                    eprintln!("Please update the cmd line to --blk file,path={opt}");
×
333
                    BlkParam::File(BlkFileParam {
×
334
                        path: PathBuf::from(opt).into(),
×
335
                        readonly: false,
×
336
                        api: WorkerApi::Mio,
×
337
                    })
×
338
                }
339
            },
340
        };
341
        match param {
×
UNCOV
342
            BlkParam::File(p) => vm.add_virtio_dev(format!("virtio-blk-{index}"), p),
×
343
            #[cfg(target_os = "linux")]
344
            BlkParam::Vu(s) => {
×
UNCOV
345
                let p = VuFrontendParam {
×
346
                    id: DeviceId::Block,
×
347
                    socket: s.socket,
×
348
                };
×
349
                vm.add_virtio_dev(format!("vu-net-{index}"), p)
×
350
            }
351
        }
UNCOV
352
        .context(error::CreateDevice)?;
×
353
    }
UNCOV
354
    Ok(())
×
355
}
×
356

357
pub fn boot(args: BootArgs) -> Result<(), Error> {
×
UNCOV
358
    let objects = parse_objects(&args.objects)?;
×
359
    let hv_config = if let Some(hv_cfg_opt) = args.hypervisor {
×
360
        serde_aco::from_args(&hv_cfg_opt, &objects).context(error::ParseArg { arg: hv_cfg_opt })?
×
361
    } else {
UNCOV
362
        Hypervisor::default()
×
363
    };
364
    let hypervisor = match hv_config {
×
365
        #[cfg(target_os = "linux")]
UNCOV
366
        Hypervisor::Kvm(kvm_config) => Kvm::new(kvm_config).context(error::Hypervisor)?,
×
367
        #[cfg(target_os = "macos")]
368
        Hypervisor::Hvf => Hvf {},
×
369
    };
UNCOV
370
    let coco = match args.coco {
×
371
        None => None,
×
372
        Some(c) => Some(serde_aco::from_args(&c, &objects).context(error::ParseArg { arg: c })?),
×
373
    };
374
    let mem_config = if let Some(s) = args.memory {
×
375
        serde_aco::from_args(&s, &objects).context(error::ParseArg { arg: s })?
×
376
    } else {
377
        #[cfg(target_os = "linux")]
378
        eprintln!(
×
379
            "Please update the cmd line to --memory size={},backend=memfd",
380
            args.mem_size
381
        );
382
        let size = serde_aco::from_args(&args.mem_size, &objects)
×
383
            .context(error::ParseArg { arg: args.mem_size })?;
×
384
        MemConfig {
×
385
            size,
×
386
            #[cfg(target_os = "linux")]
×
387
            backend: MemBackend::Memfd,
×
UNCOV
388
            #[cfg(not(target_os = "linux"))]
×
389
            backend: MemBackend::Anonymous,
×
UNCOV
390
            ..Default::default()
×
391
        }
×
392
    };
393
    let board_config = BoardConfig {
×
UNCOV
394
        mem: mem_config,
×
395
        num_cpu: args.num_cpu,
×
396
        coco,
×
397
    };
×
UNCOV
398
    let vm = Machine::new(hypervisor, board_config).context(error::CreateVm)?;
×
399
    #[cfg(target_arch = "x86_64")]
400
    vm.add_com1().context(error::CreateDevice)?;
×
401
    #[cfg(target_arch = "aarch64")]
402
    vm.add_pl011().context(error::CreateDevice)?;
×
403
    #[cfg(target_arch = "aarch64")]
404
    vm.add_pl031();
×
405

NEW
406
    if args.pvpanic {
×
407
        vm.add_pvpanic().context(error::CreateDevice)?;
×
408
    }
×
409

410
    #[cfg(target_arch = "x86_64")]
411
    if args.firmware.is_some() || !args.fw_cfgs.is_empty() {
×
412
        let params = args
×
413
            .fw_cfgs
×
414
            .into_iter()
×
415
            .map(|s| serde_aco::from_args(&s, &objects).context(error::ParseArg { arg: s }))
×
416
            .collect::<Result<Vec<_>, _>>()?;
×
417
        let fw_cfg = vm
×
418
            .add_fw_cfg(params.into_iter())
×
419
            .context(error::CreateDevice)?;
×
420
        let mut dev = fw_cfg.lock();
×
421

422
        if let Some(kernel) = &args.kernel {
×
UNCOV
423
            dev.add_kernel_data(File::open(kernel).context(error::OpenFile {
×
424
                path: kernel.to_owned(),
×
UNCOV
425
            })?)
×
426
            .context(error::FwCfg)?
×
427
        }
×
428
        if let Some(initramfs) = &args.initramfs {
×
429
            dev.add_initramfs_data(File::open(initramfs).context(error::OpenFile {
×
UNCOV
430
                path: initramfs.to_owned(),
×
431
            })?)
×
432
            .context(error::FwCfg)?;
×
433
        }
×
434
        if let Some(cmdline) = &args.cmdline {
×
435
            dev.add_kernel_cmdline(cmdline.clone());
×
436
        }
×
UNCOV
437
    };
×
438

439
    if args.entropy {
×
440
        vm.add_virtio_dev("virtio-entropy", EntropyParam::default())
×
441
            .context(error::CreateDevice)?;
×
442
    }
×
443
    add_net(&vm, args.net, &objects)?;
×
444
    add_blk(&vm, args.blk, &objects)?;
×
UNCOV
445
    for (index, fs) in args.fs.into_iter().enumerate() {
×
UNCOV
446
        let param: FsParam =
×
447
            serde_aco::from_args(&fs, &objects).context(error::ParseArg { arg: fs })?;
×
448
        match param {
×
449
            FsParam::Dir(p) => vm.add_virtio_dev(format!("virtio-fs-{index}"), p),
×
450
            #[cfg(target_os = "linux")]
UNCOV
451
            FsParam::Vu(p) => vm.add_virtio_dev(format!("vu-fs-{index}"), p),
×
452
        }
453
        .context(error::CreateDevice)?;
×
454
    }
455
    if let Some(vsock) = args.vsock {
×
456
        let param =
×
457
            serde_aco::from_args(&vsock, &objects).context(error::ParseArg { arg: vsock })?;
×
UNCOV
458
        match param {
×
459
            #[cfg(target_os = "linux")]
UNCOV
460
            VsockParam::Vhost(p) => vm
×
461
                .add_virtio_dev("vhost-vsock", p)
×
462
                .context(error::CreateDevice)?,
×
463
            VsockParam::Uds(p) => vm
×
464
                .add_virtio_dev("uds-vsock", p)
×
UNCOV
465
                .context(error::CreateDevice)?,
×
466
        };
467
    }
×
468
    if let Some(balloon) = args.balloon {
×
469
        let param: BalloonParam =
×
470
            serde_aco::from_args(&balloon, &objects).context(error::ParseArg { arg: balloon })?;
×
471
        vm.add_virtio_dev("virtio-balloon", param)
×
UNCOV
472
            .context(error::CreateDevice)?;
×
UNCOV
473
    }
×
474

475
    #[cfg(target_os = "linux")]
NEW
476
    for ioas in args.vfio_ioas.into_iter() {
×
NEW
477
        let param: IoasParam =
×
NEW
478
            serde_aco::from_args(&ioas, &objects).context(error::ParseArg { arg: ioas })?;
×
479
        vm.add_vfio_ioas(param).context(error::CreateDevice)?;
×
480
    }
481
    #[cfg(target_os = "linux")]
NEW
482
    for (index, vfio) in args.vfio_cdev.into_iter().enumerate() {
×
NEW
483
        let param: CdevParam =
×
UNCOV
484
            serde_aco::from_args(&vfio, &objects).context(error::ParseArg { arg: vfio })?;
×
NEW
485
        vm.add_vfio_cdev(format!("vfio-{index}").into(), param)
×
486
            .context(error::CreateDevice)?;
×
487
    }
488

489
    #[cfg(target_os = "linux")]
490
    for container in args.vfio_container.into_iter() {
×
UNCOV
491
        let param: ContainerParam = serde_aco::from_args(&container, &objects)
×
UNCOV
492
            .context(error::ParseArg { arg: container })?;
×
UNCOV
493
        vm.add_vfio_container(param).context(error::CreateDevice)?;
×
494
    }
495
    #[cfg(target_os = "linux")]
UNCOV
496
    for (index, group) in args.vfio_group.into_iter().enumerate() {
×
UNCOV
497
        let param: GroupParam =
×
UNCOV
498
            serde_aco::from_args(&group, &objects).context(error::ParseArg { arg: group })?;
×
UNCOV
499
        vm.add_vfio_devs_in_group(&index.to_string(), param)
×
UNCOV
500
            .context(error::CreateDevice)?;
×
501
    }
502

UNCOV
503
    let payload = if let Some(fw) = args.firmware {
×
UNCOV
504
        Some(Payload {
×
UNCOV
505
            executable: fw,
×
UNCOV
506
            exec_type: ExecType::Firmware,
×
UNCOV
507
            initramfs: None,
×
UNCOV
508
            cmdline: None,
×
UNCOV
509
        })
×
UNCOV
510
    } else if let Some(kernel) = args.kernel {
×
UNCOV
511
        Some(Payload {
×
UNCOV
512
            exec_type: ExecType::Linux,
×
UNCOV
513
            executable: kernel,
×
UNCOV
514
            initramfs: args.initramfs,
×
UNCOV
515
            cmdline: args.cmdline,
×
UNCOV
516
        })
×
517
    } else {
518
        #[cfg(target_arch = "x86_64")]
UNCOV
519
        if let Some(pvh_kernel) = args.pvh {
×
UNCOV
520
            Some(Payload {
×
UNCOV
521
                executable: pvh_kernel,
×
UNCOV
522
                exec_type: ExecType::Pvh,
×
UNCOV
523
                initramfs: args.initramfs,
×
UNCOV
524
                cmdline: args.cmdline,
×
UNCOV
525
            })
×
526
        } else {
UNCOV
527
            None
×
528
        }
529
        #[cfg(not(target_arch = "x86_64"))]
UNCOV
530
        None
×
531
    };
UNCOV
532
    if let Some(payload) = payload {
×
UNCOV
533
        vm.add_payload(payload);
×
UNCOV
534
    }
×
535

UNCOV
536
    vm.boot().context(error::BootVm)?;
×
UNCOV
537
    vm.wait().context(error::WaitVm)?;
×
UNCOV
538
    Ok(())
×
UNCOV
539
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc