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

LibreSign / libresign / 25406519220

05 May 2026 10:47PM UTC coverage: 56.814%. First build
25406519220

Pull #7596

github

web-flow
Merge 60158554f into 81feadf9e
Pull Request #7596: feat: integrate pdf-signature-validator for native validation

157 of 240 new or added lines in 4 files covered. (65.42%)

10722 of 18872 relevant lines covered (56.81%)

7.02 hits per line

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

2.92
/lib/Service/Install/ConfigureCheckService.php
1
<?php
2

3
declare(strict_types=1);
4
/**
5
 * SPDX-FileCopyrightText: 2020-2024 LibreCode coop and contributors
6
 * SPDX-License-Identifier: AGPL-3.0-or-later
7
 */
8

9
namespace OCA\Libresign\Service\Install;
10

11
use OC\AppConfig;
12
use OC\SystemConfig;
13
use OCA\Libresign\AppInfo\Application;
14
use OCA\Libresign\Handler\CertificateEngine\CertificateEngineFactory;
15
use OCA\Libresign\Handler\SignEngine\JSignPdfHandler;
16
use OCA\Libresign\Helper\ConfigureCheckHelper;
17
use OCA\Libresign\Helper\JavaHelper;
18
use OCP\App\IAppManager;
19
use OCP\IAppConfig;
20
use OCP\IURLGenerator;
21
use Psr\Log\LoggerInterface;
22

23
class ConfigureCheckService {
24
        private string $architecture;
25
        private bool $isCacheDisabled = false;
26
        private array $result = [];
27
        public function __construct(
28
                private IAppConfig $appConfig,
29
                private SystemConfig $systemConfig,
30
                private AppConfig $ocAppConfig,
31
                protected IAppManager $appManager,
32
                protected IURLGenerator $urlGenerator,
33
                private JSignPdfHandler $jSignPdfHandler,
34
                private CertificateEngineFactory $certificateEngineFactory,
35
                private SignSetupService $signSetupService,
36
                private LoggerInterface $logger,
37
                protected JavaHelper $javaHelper,
38
        ) {
39
                $this->architecture = php_uname('m');
7✔
40
        }
41

42
        public function disableCache(): void {
43
                $this->isCacheDisabled = true;
×
44
        }
45

46
        /**
47
         * Get result of all checks
48
         *
49
         * @return ConfigureCheckHelper[]
50
         */
51
        public function checkAll(): array {
52
                if ($this->isCacheDisabled) {
×
53
                        $this->ocAppConfig->clearCache();
×
54
                }
55
                $result = [];
×
56
                $result = array_merge($result, $this->checkSign());
×
57
                $result = array_merge($result, $this->checkCertificate());
×
58
                return $result;
×
59
        }
60

61
        /**
62
         * Check all requirements to sign
63
         *
64
         * @return ConfigureCheckHelper[]
65
         */
66
        public function checkSign(): array {
67
                $return = [];
×
68
                $return = array_merge($return, $this->checkJava());
×
69
                $return = array_merge($return, $this->checkPdftk());
×
70
                $return = array_merge($return, $this->checkJSignPdf());
×
71
                $return = array_merge($return, $this->checkPoppler());
×
72
                $return = array_merge($return, $this->checkImagick());
×
73
                return $return;
×
74
        }
75

76
        public function checkPoppler(): array {
NEW
77
                return $this->checkPdfinfo();
×
78
        }
79

80
        public function checkPdfinfo(): array {
81
                if (!empty($this->result['pdfinfo'])) {
×
82
                        return $this->result['pdfinfo'];
×
83
                }
84
                // The output of this command go to STDERR and exec get the STDOUT
85
                // With 2>&1 the STRERR is redirected to STDOUT
86
                exec('pdfinfo -v 2>&1', $version, $retval);
×
87
                if ($retval !== 0) {
×
88
                        return $this->result['pdfinfo'] = [
×
89
                                (new ConfigureCheckHelper())
×
90
                                        ->setInfoMessage('Poppler utils not installed')
×
91
                                        ->setResource('pdfinfo')
×
92
                                        ->setTip('Install the package poppler-utils at your operational system have a fallback to fetch page dimensions.'),
×
93
                        ];
×
94
                }
95
                if (!$version) {
×
96
                        return $this->result['pdfinfo'] = [
×
97
                                (new ConfigureCheckHelper())
×
98
                                        ->setErrorMessage('Fail to retrieve pdfinfo version')
×
99
                                        ->setResource('pdfinfo')
×
100
                                        ->setTip("The command <pdfinfo -v> executed by PHP haven't any output."),
×
101
                        ];
×
102
                }
103
                $returnValue = preg_match('/pdfinfo version (?<version>.*)/', implode(PHP_EOL, $version), $matches);
×
104
                if (!$returnValue) {
×
105
                        return $this->result['pdfinfo'] = [
×
106
                                (new ConfigureCheckHelper())
×
107
                                        ->setErrorMessage('Fail to retrieve pdfinfo version')
×
108
                                        ->setResource('pdfinfo')
×
109
                                        ->setTip("This is a poppler-utils dependency and wasn't possible to parse the output of command pdfinfo -v"),
×
110
                        ];
×
111
                }
112
                return $this->result['pdfinfo'] = [(new ConfigureCheckHelper())
×
113
                        ->setSuccessMessage('pdfinfo version: ' . $matches['version'])
×
114
                        ->setResource('pdfinfo')
×
115
                ];
×
116
        }
117

118
        /**
119
         * Check all requirements to use JSignPdf
120
         *
121
         * @return ConfigureCheckHelper[]
122
         */
123
        public function checkJSignPdf(): array {
124
                if (!empty($this->result['jsignpdf'])) {
×
125
                        return $this->result['jsignpdf'];
×
126
                }
127

128
                $signatureEngine = $this->appConfig->getValueString(Application::APP_ID, 'signature_engine', 'JSignPdf');
×
129
                if ($signatureEngine !== 'JSignPdf') {
×
130
                        return [];
×
131
                }
132

133
                $jsignpdJarPath = $this->appConfig->getValueString(Application::APP_ID, 'jsignpdf_jar_path');
×
134
                if ($jsignpdJarPath) {
×
135
                        $resultOfVerify = $this->verify('jsignpdf');
×
136
                        if (count($resultOfVerify)) {
×
137
                                [$errorMessage, $tip] = $this->getErrorAndTipToResultOfVerify($resultOfVerify, 'jsignpdf');
×
138
                                return $this->result['jsignpdf'] = [
×
139
                                        (new ConfigureCheckHelper())
×
140
                                                ->setErrorMessage($errorMessage)
×
141
                                                ->setResource('jsignpdf')
×
142
                                                ->setTip($tip),
×
143
                                ];
×
144
                        }
145
                        if (file_exists($jsignpdJarPath)) {
×
146
                                if (!$this->isJavaOk()) {
×
147
                                        return $this->result['jsignpdf'] = [
×
148
                                                (new ConfigureCheckHelper())
×
149
                                                        ->setErrorMessage('Necessary Java to run JSignPdf')
×
150
                                                        ->setResource('jsignpdf')
×
151
                                                        ->setTip('Run occ libresign:install --java'),
×
152
                                        ];
×
153
                                }
154
                                $jsignPdf = $this->jSignPdfHandler->getJSignPdf();
×
155
                                $jsignPdf->setParam($this->jSignPdfHandler->getJSignParam());
×
156
                                $currentVersion = $jsignPdf->getVersion();
×
157
                                $return = [];
×
158
                                if ($currentVersion < InstallService::JSIGNPDF_VERSION) {
×
159
                                        if (!$currentVersion) {
×
160
                                                $message = 'Necessary install the version ' . InstallService::JSIGNPDF_VERSION;
×
161
                                        } else {
162
                                                $message = 'Necessary bump JSignPdf versin from ' . $currentVersion . ' to ' . InstallService::JSIGNPDF_VERSION;
×
163
                                        }
164
                                        $return[] = (new ConfigureCheckHelper())
×
165
                                                ->setErrorMessage($message)
×
166
                                                ->setResource('jsignpdf')
×
167
                                                ->setTip('Run occ libresign:install --jsignpdf');
×
168
                                }
169
                                if ($currentVersion > InstallService::JSIGNPDF_VERSION) {
×
170
                                        $return[] = (new ConfigureCheckHelper())
×
171
                                                ->setErrorMessage('Necessary downgrade JSignPdf versin from ' . $currentVersion . ' to ' . InstallService::JSIGNPDF_VERSION)
×
172
                                                ->setResource('jsignpdf')
×
173
                                                ->setTip('Run occ libresign:install --jsignpdf');
×
174
                                }
175
                                $return[] = (new ConfigureCheckHelper())
×
176
                                        ->setSuccessMessage('JSignPdf version: ' . $currentVersion)
×
177
                                        ->setResource('jsignpdf');
×
178
                                $return[] = (new ConfigureCheckHelper())
×
179
                                        ->setSuccessMessage('JSignPdf path: ' . $jsignpdJarPath)
×
180
                                        ->setResource('jsignpdf');
×
181
                                return $return;
×
182
                        }
183
                        return $this->result['jsignpdf'] = [
×
184
                                (new ConfigureCheckHelper())
×
185
                                        ->setErrorMessage('JSignPdf binary not found: ' . $jsignpdJarPath)
×
186
                                        ->setResource('jsignpdf')
×
187
                                        ->setTip('Run occ libresign:install --jsignpdf'),
×
188
                        ];
×
189
                }
190
                return $this->result['jsignpdf'] = [
×
191
                        (new ConfigureCheckHelper())
×
192
                                ->setErrorMessage('JSignPdf not found')
×
193
                                ->setResource('jsignpdf')
×
194
                                ->setTip('Run occ libresign:install --jsignpdf'),
×
195
                ];
×
196
        }
197

198
        /**
199
         * Check all requirements to use PDFtk
200
         *
201
         * @return ConfigureCheckHelper[]
202
         */
203
        public function checkPdftk(): array {
204
                if (!empty($this->result['pdftk'])) {
×
205
                        return $this->result['pdftk'];
×
206
                }
207
                $pdftkPath = $this->appConfig->getValueString(Application::APP_ID, 'pdftk_path');
×
208
                if ($pdftkPath) {
×
209
                        $resultOfVerify = $this->verify('pdftk');
×
210
                        if (count($resultOfVerify)) {
×
211
                                [$errorMessage, $tip] = $this->getErrorAndTipToResultOfVerify($resultOfVerify, 'pdftk');
×
212
                                return $this->result['pdftk'] = [
×
213
                                        (new ConfigureCheckHelper())
×
214
                                                ->setErrorMessage($errorMessage)
×
215
                                                ->setResource('pdftk')
×
216
                                                ->setTip($tip),
×
217
                                ];
×
218
                        }
219
                        if (file_exists($pdftkPath)) {
×
220
                                if (!$this->isJavaOk()) {
×
221
                                        return $this->result['pdftk'] = [
×
222
                                                (new ConfigureCheckHelper())
×
223
                                                        ->setErrorMessage('Necessary Java to run PDFtk')
×
224
                                                        ->setResource('jsignpdf')
×
225
                                                        ->setTip('Run occ libresign:install --java'),
×
226
                                        ];
×
227
                                }
228
                                $javaPath = $this->javaHelper->getJavaPath();
×
229
                                $version = [];
×
230
                                \exec($javaPath . ' -jar ' . $pdftkPath . ' --version 2>&1', $version, $resultCode);
×
231
                                if ($resultCode !== 0) {
×
232
                                        return $this->result['pdftk'] = [
×
233
                                                (new ConfigureCheckHelper())
×
234
                                                        ->setErrorMessage('Failure to check PDFtk version.')
×
235
                                                        ->setResource('java')
×
236
                                                        ->setTip('Run occ libresign:install --pdftk'),
×
237
                                        ];
×
238
                                }
239
                                if (isset($version[0])) {
×
240
                                        preg_match('/pdftk port to java (?<version>.*) a Handy Tool/', $version[0], $matches);
×
241
                                        if (isset($matches['version'])) {
×
242
                                                if ($matches['version'] === InstallService::PDFTK_VERSION) {
×
243
                                                        return $this->result['pdftk'] = [
×
244
                                                                $this->result['pdftk'][] = (new ConfigureCheckHelper())
×
245
                                                                        ->setSuccessMessage('PDFtk version: ' . InstallService::PDFTK_VERSION)
×
246
                                                                        ->setResource('pdftk'),
×
247
                                                                $this->result['pdftk'][] = (new ConfigureCheckHelper())
×
248
                                                                        ->setSuccessMessage('PDFtk path: ' . $pdftkPath)
×
249
                                                                        ->setResource('pdftk'),
×
250
                                                        ];
×
251
                                                }
252
                                                $message = 'Necessary install the version ' . InstallService::PDFTK_VERSION;
×
253
                                                return $this->result['pdftk'] = [
×
254
                                                        (new ConfigureCheckHelper())
×
255
                                                                ->setErrorMessage($message)
×
256
                                                                ->setResource('jsignpdf')
×
257
                                                                ->setTip('Run occ libresign:install --jsignpdf')
×
258
                                                ];
×
259
                                        }
260
                                }
261
                                return $this->result['pdftk'] = [
×
262
                                        (new ConfigureCheckHelper())
×
263
                                                ->setErrorMessage('PDFtk binary is invalid: ' . $pdftkPath)
×
264
                                                ->setResource('pdftk')
×
265
                                                ->setTip('Run occ libresign:install --pdftk'),
×
266
                                ];
×
267
                        }
268
                        return $this->result['pdftk'] = [
×
269
                                (new ConfigureCheckHelper())
×
270
                                        ->setErrorMessage('PDFtk binary not found: ' . $pdftkPath)
×
271
                                        ->setResource('pdftk')
×
272
                                        ->setTip('Run occ libresign:install --pdftk'),
×
273
                        ];
×
274
                }
275
                return $this->result['pdftk'] = [
×
276
                        (new ConfigureCheckHelper())
×
277
                                ->setErrorMessage('PDFtk not found')
×
278
                                ->setResource('pdftk')
×
279
                                ->setTip('Run occ libresign:install --pdftk'),
×
280
                ];
×
281
        }
282

283
        public function isDebugEnabled(): bool {
284
                return $this->systemConfig->getValue('debug', false) === true;
×
285
        }
286

287
        private function verify(string $resource): array {
288
                $this->signSetupService->willUseLocalCert($this->isDebugEnabled());
×
289
                $result = $this->signSetupService->verify($this->architecture, $resource);
×
290
                if (count($result) === 1 && $this->isDebugEnabled()) {
×
291
                        if (isset($result['SIGNATURE_DATA_NOT_FOUND'])) {
×
292
                                return [];
×
293
                        }
294
                        if (isset($result['EMPTY_SIGNATURE_DATA'])) {
×
295
                                return [];
×
296
                        }
297
                }
298
                return $result;
×
299
        }
300

301
        private function getErrorAndTipToResultOfVerify(array $result, string $resource): array {
302
                if (count($result) === 1 && !$this->isDebugEnabled()) {
×
303
                        if (isset($result['SIGNATURE_DATA_NOT_FOUND'])) {
×
304
                                return [
×
305
                                        'Signature data not found.',
×
306
                                        "Sounds that you are running from source code of LibreSign.\nEnable debug mode by: occ config:system:set debug --value true --type boolean",
×
307
                                ];
×
308
                        }
309
                        if (isset($result['EMPTY_SIGNATURE_DATA'])) {
×
310
                                return [
×
311
                                        'Your signature data is empty.',
×
312
                                        "Sounds that you are running from source code of LibreSign.\nEnable debug mode by: occ config:system:set debug --value true --type boolean",
×
313
                                ];
×
314
                        }
315
                }
316
                if (isset($result['HASH_FILE_ERROR'])) {
×
317
                        if ($this->isDebugEnabled()) {
×
318
                                return [
×
319
                                        'Invalid hash of binaries files.',
×
320
                                        'Debug mode is enabled at your config.php and your LibreSign app was signed using a production signature. If you are not working at development of LibreSign, disable your debug mode or run the command: occ libresign install --' . $resource . ' --use-local-cert',
×
321
                                ];
×
322
                        }
323
                }
324
                $this->logger->error('Invalid hash of binaries files', ['result' => $result]);
×
325
                if ($this->appManager->isEnabledForUser('logreader')) {
×
326
                        return [
×
327
                                'Invalid hash of binaries files.',
×
328
                                'Check your nextcloud.log file on '
×
329
                                . $this->urlGenerator->linkToRouteAbsolute('settings.adminsettings.form', ['section' => 'logging'])
×
330
                                . ' and run occ libresign:install --all',
×
331
                        ];
×
332
                }
333
                return [
×
334
                        'Invalid hash of binaries files.',
×
335
                        'Check your nextcloud.log file and run occ libresign:install --all',
×
336
                ];
×
337
        }
338

339
        /**
340
         * Check all requirements to use Java
341
         *
342
         * @return ConfigureCheckHelper[]
343
         */
344
        private function checkJava(): array {
345
                if (!empty($this->result['java'])) {
×
346
                        return $this->result['java'];
×
347
                }
348

349
                $javaPath = $this->javaHelper->getJavaPath();
×
350
                if ($javaPath) {
×
351
                        $resultOfVerify = $this->verify('java');
×
352
                        if (count($resultOfVerify)) {
×
353
                                [$errorMessage, $tip] = $this->getErrorAndTipToResultOfVerify($resultOfVerify, 'java');
×
354
                                return $this->result['java'] = [
×
355
                                        (new ConfigureCheckHelper())
×
356
                                                ->setErrorMessage($errorMessage)
×
357
                                                ->setResource('java')
×
358
                                                ->setTip($tip),
×
359
                                ];
×
360
                        }
361
                        if (file_exists($javaPath)) {
×
362
                                \exec($javaPath . ' -version 2>&1', $javaVersion, $resultCode);
×
363
                                if (empty($javaVersion)) {
×
364
                                        return $this->result['java'] = [
×
365
                                                (new ConfigureCheckHelper())
×
366
                                                        ->setErrorMessage(
×
367
                                                                'Failed to execute Java. Sounds that your operational system is blocking the JVM.'
×
368
                                                        )
×
369
                                                        ->setResource('java')
×
370
                                                        ->setTip('https://github.com/LibreSign/libresign/issues/2327#issuecomment-1961988790'),
×
371
                                        ];
×
372
                                }
373
                                if ($resultCode !== 0) {
×
374
                                        return $this->result['java'] = [
×
375
                                                (new ConfigureCheckHelper())
×
376
                                                        ->setErrorMessage('Failure to check Java version.')
×
377
                                                        ->setResource('java')
×
378
                                                        ->setTip('Run occ libresign:install --java'),
×
379
                                        ];
×
380
                                }
381
                                $javaVersion = current($javaVersion);
×
382
                                if ($javaVersion !== InstallService::JAVA_VERSION) {
×
383
                                        return $this->result['java'] = [
×
384
                                                (new ConfigureCheckHelper())
×
385
                                                        ->setErrorMessage(
×
386
                                                                sprintf(
×
387
                                                                        'Invalid java version. Found: %s expected: %s',
×
388
                                                                        $javaVersion,
×
389
                                                                        InstallService::JAVA_VERSION
×
390
                                                                )
×
391
                                                        )
×
392
                                                        ->setResource('java')
×
393
                                                        ->setTip('Run occ libresign:install --java'),
×
394
                                        ];
×
395
                                }
396
                                \exec($javaPath . ' -XshowSettings:properties -version 2>&1', $output, $resultCode);
×
397
                                preg_match('/native.encoding = (?<encoding>.*)\n/', implode("\n", $output), $matches);
×
398
                                if (!isset($matches['encoding'])) {
×
399
                                        return $this->result['java'] = [
×
400
                                                (new ConfigureCheckHelper())
×
401
                                                        ->setErrorMessage('Java encoding not found.')
×
402
                                                        ->setResource('java')
×
403
                                                        ->setTip(sprintf('The command %s need to have native.encoding', $javaPath . ' -XshowSettings:properties -version')),
×
404
                                        ];
×
405
                                }
406
                                if (!str_contains($matches['encoding'], 'UTF-8')) {
×
407
                                        $detectedEncoding = trim($matches['encoding']);
×
408
                                        $phpLocale = setlocale(LC_CTYPE, 0);
×
409
                                        $phpLcAll = getenv('LC_ALL');
×
410
                                        $phpLang = getenv('LANG');
×
411

412
                                        $tip = sprintf(
×
413
                                                "Java detected encoding \"%s\" but UTF-8 is required.\n\n"
×
414
                                                . "**Current PHP environment:**\n"
×
415
                                                . "- LC_CTYPE: %s\n"
×
416
                                                . "- LC_ALL: %s\n"
×
417
                                                . "- LANG: %s\n\n"
×
418
                                                . "**To fix this issue:**\n"
×
419
                                                . "1. Set LC_ALL and LANG environment variables (e.g., LC_ALL=en_US.UTF-8) for your web server user\n"
×
420
                                                . "2. Restart your web server after making changes\n"
×
421
                                                . "3. Verify with command: `locale charmap` (should return UTF-8)\n\n"
×
422
                                                . 'For more details, see: [Issue #4872](https://github.com/LibreSign/libresign/issues/4872)',
×
423
                                                $detectedEncoding,
×
424
                                                $phpLocale ?: 'not set',
×
425
                                                $phpLcAll ?: 'not set',
×
426
                                                $phpLang ?: 'not set'
×
427
                                        );
×
428
                                        return $this->result['java'] = [
×
429
                                                (new ConfigureCheckHelper())
×
430
                                                        ->setInfoMessage(sprintf(
×
431
                                                                'Non-UTF-8 encoding detected: %s. This may cause issues with accented or special characters',
×
432
                                                                $detectedEncoding
×
433
                                                        ))
×
434
                                                        ->setResource('java')
×
435
                                                        ->setTip($tip),
×
436
                                        ];
×
437
                                }
438
                                return $this->result['java'] = [
×
439
                                        (new ConfigureCheckHelper())
×
440
                                                ->setSuccessMessage('Java version: ' . $javaVersion)
×
441
                                                ->setResource('java'),
×
442
                                        (new ConfigureCheckHelper())
×
443
                                                ->setSuccessMessage('Java binary: ' . $javaPath)
×
444
                                                ->setResource('java'),
×
445
                                ];
×
446
                        }
447
                        return $this->result['java'] = [
×
448
                                (new ConfigureCheckHelper())
×
449
                                        ->setErrorMessage('Java binary not found: ' . $javaPath)
×
450
                                        ->setResource('java')
×
451
                                        ->setTip('Run occ libresign:install --java'),
×
452
                        ];
×
453
                }
454
                return $this->result['java'] = [
×
455
                        (new ConfigureCheckHelper())
×
456
                                ->setErrorMessage('Java not installed')
×
457
                                ->setResource('java')
×
458
                                ->setTip('Run occ libresign:install --java'),
×
459
                ];
×
460
        }
461

462
        private function isJavaOk() : bool {
463
                $checkJava = $this->checkJava();
×
464
                $error = array_filter(
×
465
                        $checkJava,
×
466
                        fn (ConfigureCheckHelper $config) => $config->getStatus() === 'error'
×
467
                );
×
468
                return empty($error);
×
469
        }
470

471

472
        /**
473
         * Check all requirements to use certificate
474
         *
475
         * @return ConfigureCheckHelper[]
476
         */
477
        public function checkCertificate(): array {
478
                try {
479
                        $return = $this->certificateEngineFactory->getEngine()->configureCheck();
×
480
                } catch (\Throwable) {
×
481
                        $return = [
×
482
                                (new ConfigureCheckHelper())
×
483
                                        ->setErrorMessage('Define the certificate engine to use')
×
484
                                        ->setResource('certificate-engine')
×
485
                                        ->setTip(sprintf('Run occ libresign:configure:%s --help',
×
486
                                                $this->certificateEngineFactory->getEngine()->getName()
×
487
                                        )),
×
488
                        ];
×
489
                }
490
                return $return;
×
491
        }
492

493
        /**
494
         * Check if Imagick extension is loaded
495
         *
496
         * @return ConfigureCheckHelper[]
497
         */
498
        public function checkImagick(): array {
499
                if (!empty($this->result['imagick'])) {
2✔
500
                        return $this->result['imagick'];
×
501
                }
502
                if (!extension_loaded('imagick')) {
2✔
503
                        return $this->result['imagick'] = [
1✔
504
                                (new ConfigureCheckHelper())
1✔
505
                                        ->setInfoMessage('Imagick extension is not loaded')
1✔
506
                                        ->setResource('imagick')
1✔
507
                                        ->setTip('Install php-imagick to enable visible signatures, background images, and signature element rendering.'),
1✔
508
                        ];
1✔
509
                }
510
                return $this->result['imagick'] = [];
1✔
511
        }
512
}
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