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

badges / shields / 12851394349

18 Jan 2025 07:16PM UTC coverage: 91.115% (-1.4%) from 92.488%
12851394349

push

github

web-flow
fix badge-maker package tests (#10809)

6572 of 6836 branches covered (96.14%)

8 of 8 new or added lines in 3 files covered. (100.0%)

48 existing lines in 11 files now uncovered.

49254 of 54057 relevant lines covered (91.11%)

113.73 hits per line

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

93.35
/core/server/server.js
1
/**
3✔
2
 * @module
3✔
3
 */
3✔
4

3✔
5
import path from 'path'
3✔
6
import url, { fileURLToPath } from 'url'
3✔
7
import { bootstrap } from 'global-agent'
3✔
8
import cloudflareMiddleware from 'cloudflare-middleware'
3✔
9
import Camp from '@shields_io/camp'
3✔
10
import originalJoi from 'joi'
3✔
11
import makeBadge from '../../badge-maker/lib/make-badge.js'
3✔
12
import GithubConstellation from '../../services/github/github-constellation.js'
3✔
13
import LibrariesIoConstellation from '../../services/librariesio/librariesio-constellation.js'
3✔
14
import { loadServiceClasses } from '../base-service/loader.js'
3✔
15
import { makeSend } from '../base-service/legacy-result-sender.js'
3✔
16
import { handleRequest } from '../base-service/legacy-request-handler.js'
3✔
17
import { clearResourceCache } from '../base-service/resource-cache.js'
3✔
18
import { rasterRedirectUrl } from '../badge-urls/make-badge-url.js'
3✔
19
import {
3✔
20
  fileSize,
3✔
21
  nonNegativeInteger,
3✔
22
  optionalUrl,
3✔
23
  url as requiredUrl,
3✔
24
} from '../../services/validators.js'
3✔
25
import log from './log.js'
3✔
26
import PrometheusMetrics from './prometheus-metrics.js'
3✔
27
import InfluxMetrics from './influx-metrics.js'
3✔
28
const { URL } = url
3✔
29

3✔
30
const Joi = originalJoi
3✔
31
  .extend(base => ({
3✔
32
    type: 'arrayFromString',
3✔
33
    base: base.array(),
3✔
34
    coerce: (value, state, options) => ({
3✔
35
      value: typeof value === 'string' ? value.split(' ') : value,
41!
36
    }),
3✔
37
  }))
3✔
38
  .extend(base => ({
3✔
39
    type: 'string',
3✔
40
    base: base.string(),
3✔
41
    messages: {
3✔
42
      'string.origin':
3✔
43
        'needs to be an origin string, e.g. https://host.domain with optional port and no trailing slash',
3✔
44
    },
3✔
45
    rules: {
3✔
46
      origin: {
3✔
47
        validate(value, helpers) {
3✔
48
          let origin
40✔
49
          try {
40✔
50
            ;({ origin } = new URL(value))
40✔
51
          } catch (e) {}
40!
52
          if (origin !== undefined && origin === value) {
40✔
53
            return value
40✔
54
          } else {
40!
UNCOV
55
            return helpers.error('string.origin')
×
UNCOV
56
          }
×
57
        },
3✔
58
      },
3✔
59
    },
3✔
60
  }))
3✔
61

3✔
62
const origins = Joi.arrayFromString().items(Joi.string().origin())
3✔
63
const defaultService = Joi.object({ authorizedOrigins: origins }).default({
3✔
64
  authorizedOrigins: [],
3✔
65
})
3✔
66

3✔
67
const publicConfigSchema = Joi.object({
3✔
68
  bind: {
3✔
69
    port: Joi.alternatives().try(
3✔
70
      Joi.number().port(),
3✔
71
      Joi.string().pattern(/^\\\\\.\\pipe\\.+$/),
3✔
72
    ),
3✔
73
    address: Joi.alternatives().try(
3✔
74
      Joi.string().ip().required(),
3✔
75
      Joi.string().hostname().required(),
3✔
76
    ),
3✔
77
  },
3✔
78
  metrics: {
3✔
79
    prometheus: {
3✔
80
      enabled: Joi.boolean().required(),
3✔
81
      endpointEnabled: Joi.boolean().required(),
3✔
82
    },
3✔
83
    influx: {
3✔
84
      enabled: Joi.boolean().required(),
3✔
85
      url: Joi.string()
3✔
86
        .uri()
3✔
87
        .when('enabled', { is: true, then: Joi.required() }),
3✔
88
      timeoutMilliseconds: Joi.number()
3✔
89
        .integer()
3✔
90
        .min(1)
3✔
91
        .when('enabled', { is: true, then: Joi.required() }),
3✔
92
      intervalSeconds: Joi.number().integer().min(1).when('enabled', {
3✔
93
        is: true,
3✔
94
        then: Joi.required(),
3✔
95
      }),
3✔
96
      instanceIdFrom: Joi.string()
3✔
97
        .equal('hostname', 'env-var', 'random')
3✔
98
        .when('enabled', { is: true, then: Joi.required() }),
3✔
99
      instanceIdEnvVarName: Joi.string().when('instanceIdFrom', {
3✔
100
        is: 'env-var',
3✔
101
        then: Joi.required(),
3✔
102
      }),
3✔
103
      envLabel: Joi.string().when('enabled', {
3✔
104
        is: true,
3✔
105
        then: Joi.required(),
3✔
106
      }),
3✔
107
      hostnameAliases: Joi.object(),
3✔
108
    },
3✔
109
  },
3✔
110
  ssl: {
3✔
111
    isSecure: Joi.boolean().required(),
3✔
112
    key: Joi.string(),
3✔
113
    cert: Joi.string(),
3✔
114
  },
3✔
115
  redirectUrl: optionalUrl,
3✔
116
  rasterUrl: optionalUrl,
3✔
117
  cors: {
3✔
118
    // This doesn't actually do anything
3✔
119
    // TODO: maybe remove in future?
3✔
120
    // https://github.com/badges/shields/pull/8311#discussion_r945337530
3✔
121
    allowedOrigin: Joi.array().items(optionalUrl).required(),
3✔
122
  },
3✔
123
  services: Joi.object({
3✔
124
    bitbucketServer: defaultService,
3✔
125
    drone: defaultService,
3✔
126
    github: {
3✔
127
      baseUri: requiredUrl,
3✔
128
      debug: {
3✔
129
        enabled: Joi.boolean().required(),
3✔
130
        intervalSeconds: Joi.number().integer().min(1).required(),
3✔
131
      },
3✔
132
      restApiVersion: Joi.date().raw().required(),
3✔
133
    },
3✔
134
    gitea: defaultService,
3✔
135
    gitlab: defaultService,
3✔
136
    jira: defaultService,
3✔
137
    jenkins: Joi.object({
3✔
138
      authorizedOrigins: origins,
3✔
139
      requireStrictSsl: Joi.boolean(),
3✔
140
      requireStrictSslToAuthenticate: Joi.boolean(),
3✔
141
    }).default({ authorizedOrigins: [] }),
3✔
142
    nexus: defaultService,
3✔
143
    npm: defaultService,
3✔
144
    obs: defaultService,
3✔
145
    pypi: {
3✔
146
      baseUri: requiredUrl,
3✔
147
    },
3✔
148
    sonar: defaultService,
3✔
149
    teamcity: defaultService,
3✔
150
    weblate: defaultService,
3✔
151
    trace: Joi.boolean().required(),
3✔
152
  }).required(),
3✔
153
  cacheHeaders: { defaultCacheLengthSeconds: nonNegativeInteger },
3✔
154
  handleInternalErrors: Joi.boolean().required(),
3✔
155
  fetchLimit: fileSize,
3✔
156
  userAgentBase: Joi.string().required(),
3✔
157
  requestTimeoutSeconds: nonNegativeInteger,
3✔
158
  requestTimeoutMaxAgeSeconds: nonNegativeInteger,
3✔
159
  documentRoot: Joi.string().default(
3✔
160
    path.resolve(
3✔
161
      path.dirname(fileURLToPath(import.meta.url)),
3✔
162
      '..',
3✔
163
      '..',
3✔
164
      'public',
3✔
165
    ),
3✔
166
  ),
3✔
167
  requireCloudflare: Joi.boolean().required(),
3✔
168
}).required()
3✔
169

3✔
170
const privateConfigSchema = Joi.object({
3✔
171
  azure_devops_token: Joi.string(),
3✔
172
  curseforge_api_key: Joi.string(),
3✔
173
  discord_bot_token: Joi.string(),
3✔
174
  dockerhub_username: Joi.string(),
3✔
175
  dockerhub_pat: Joi.string(),
3✔
176
  drone_token: Joi.string(),
3✔
177
  gh_client_id: Joi.string(),
3✔
178
  gh_client_secret: Joi.string(),
3✔
179
  gh_token: Joi.string(),
3✔
180
  gitea_token: Joi.string(),
3✔
181
  gitlab_token: Joi.string(),
3✔
182
  jenkins_user: Joi.string(),
3✔
183
  jenkins_pass: Joi.string(),
3✔
184
  jira_user: Joi.string(),
3✔
185
  jira_pass: Joi.string(),
3✔
186
  bitbucket_username: Joi.string(),
3✔
187
  bitbucket_password: Joi.string(),
3✔
188
  bitbucket_server_username: Joi.string(),
3✔
189
  bitbucket_server_password: Joi.string(),
3✔
190
  librariesio_tokens: Joi.arrayFromString().items(Joi.string()),
3✔
191
  nexus_user: Joi.string(),
3✔
192
  nexus_pass: Joi.string(),
3✔
193
  npm_token: Joi.string(),
3✔
194
  obs_user: Joi.string(),
3✔
195
  obs_pass: Joi.string(),
3✔
196
  redis_url: Joi.string().uri({ scheme: ['redis', 'rediss'] }),
3✔
197
  opencollective_token: Joi.string(),
3✔
198
  pepy_key: Joi.string(),
3✔
199
  postgres_url: Joi.string().uri({ scheme: 'postgresql' }),
3✔
200
  reddit_client_id: Joi.string(),
3✔
201
  reddit_client_secret: Joi.string(),
3✔
202
  sentry_dsn: Joi.string(),
3✔
203
  sl_insight_userUuid: Joi.string(),
3✔
204
  sl_insight_apiToken: Joi.string(),
3✔
205
  sonarqube_token: Joi.string(),
3✔
206
  stackapps_api_key: Joi.string(),
3✔
207
  teamcity_user: Joi.string(),
3✔
208
  teamcity_pass: Joi.string(),
3✔
209
  twitch_client_id: Joi.string(),
3✔
210
  twitch_client_secret: Joi.string(),
3✔
211
  influx_username: Joi.string(),
3✔
212
  influx_password: Joi.string(),
3✔
213
  weblate_api_key: Joi.string(),
3✔
214
  youtube_api_key: Joi.string(),
3✔
215
}).required()
3✔
216
const privateMetricsInfluxConfigSchema = privateConfigSchema.append({
3✔
217
  influx_username: Joi.string().required(),
3✔
218
  influx_password: Joi.string().required(),
3✔
219
})
3✔
220

3✔
221
function addHandlerAtIndex(camp, index, handlerFn) {
2✔
222
  camp.stack.splice(index, 0, handlerFn)
2✔
223
}
2✔
224

3✔
225
function isOnHeroku() {
1✔
226
  return !!process.env.DYNO
1✔
227
}
1✔
228

3✔
229
function isOnFly() {
1✔
230
  return !!process.env.FLY_APP_NAME
1✔
231
}
1✔
232

3✔
233
/**
3✔
234
 * The Server is based on the web framework Scoutcamp. It creates
3✔
235
 * an http server, sets up helpers for token persistence and monitoring.
3✔
236
 * Then it loads all the services, injecting dependencies as it
3✔
237
 * asks each one to register its route with Scoutcamp.
3✔
238
 */
3✔
239
class Server {
3✔
240
  /**
3✔
241
   * Badge Server Constructor
3✔
242
   *
3✔
243
   * @param {object} config Configuration object read from config yaml files
3✔
244
   * by https://www.npmjs.com/package/config and validated against
3✔
245
   * publicConfigSchema and privateConfigSchema
3✔
246
   * @see https://github.com/badges/shields/blob/master/doc/production-hosting.md#configuration
3✔
247
   * @see https://github.com/badges/shields/blob/master/doc/server-secrets.md
3✔
248
   */
3✔
249
  constructor(config) {
3✔
250
    const publicConfig = Joi.attempt(config.public, publicConfigSchema)
26✔
251
    const privateConfig = this.validatePrivateConfig(
26✔
252
      config.private,
26✔
253
      privateConfigSchema,
26✔
254
    )
26✔
255
    // We want to require an username and a password for the influx metrics
26✔
256
    // only if the influx metrics are enabled. The private config schema
26✔
257
    // and the public config schema are two separate schemas so we have to run
26✔
258
    // validation manually.
26✔
259
    if (publicConfig.metrics.influx && publicConfig.metrics.influx.enabled) {
26✔
260
      this.validatePrivateConfig(
9✔
261
        config.private,
9✔
262
        privateMetricsInfluxConfigSchema,
9✔
263
      )
9✔
264
    }
9✔
265
    this.config = {
18✔
266
      public: publicConfig,
18✔
267
      private: privateConfig,
18✔
268
    }
18✔
269

18✔
270
    this.githubConstellation = new GithubConstellation({
18✔
271
      service: publicConfig.services.github,
18✔
272
      private: privateConfig,
18✔
273
    })
18✔
274

18✔
275
    this.librariesioConstellation = new LibrariesIoConstellation({
18✔
276
      private: privateConfig,
18✔
277
    })
18✔
278

18✔
279
    if (publicConfig.metrics.prometheus.enabled) {
26✔
280
      this.metricInstance = new PrometheusMetrics()
3✔
281
      if (publicConfig.metrics.influx.enabled) {
3✔
282
        this.influxMetrics = new InfluxMetrics(
1✔
283
          this.metricInstance,
1✔
284
          Object.assign({}, publicConfig.metrics.influx, {
1✔
285
            username: privateConfig.influx_username,
1✔
286
            password: privateConfig.influx_password,
1✔
287
          }),
1✔
288
        )
1✔
289
      }
1✔
290
    }
3✔
291
  }
26✔
292

3✔
293
  validatePrivateConfig(privateConfig, privateConfigSchema) {
3✔
294
    try {
29✔
295
      return Joi.attempt(privateConfig, privateConfigSchema)
29✔
296
    } catch (e) {
29✔
297
      const badPaths = e.details.map(({ path }) => path)
2✔
298
      throw Error(
2✔
299
        `Private configuration is invalid. Check these paths: ${badPaths.join(
2✔
300
          ',',
2✔
301
        )}`,
2✔
302
      )
2✔
303
    }
2✔
304
  }
29✔
305

3✔
306
  get port() {
3✔
307
    const {
×
UNCOV
308
      port,
×
UNCOV
309
      ssl: { isSecure },
×
UNCOV
310
    } = this.config.public
×
UNCOV
311
    return port || (isSecure ? 443 : 80)
×
UNCOV
312
  }
×
313

3✔
314
  get baseUrl() {
3✔
315
    const {
16✔
316
      bind: { address, port },
16✔
317
      ssl: { isSecure },
16✔
318
    } = this.config.public
16✔
319

16✔
320
    return url.format({
16✔
321
      protocol: isSecure ? 'https' : 'http',
16!
322
      hostname: address,
16✔
323
      port,
16✔
324
      pathname: '/',
16✔
325
    })
16✔
326
  }
16✔
327

3✔
328
  // See https://www.viget.com/articles/heroku-cloudflare-the-right-way/
3✔
329
  requireCloudflare() {
3✔
330
    // Set `req.ip`, which is expected by `cloudflareMiddleware()`. This is set
1✔
331
    // by Express but not Scoutcamp.
1✔
332
    addHandlerAtIndex(this.camp, 0, function (req, res, next) {
1✔
333
      if (isOnHeroku()) {
1!
UNCOV
334
        // On Heroku, `req.socket.remoteAddress` is the Heroku router. However,
×
335
        // the router ensures that the last item in the `X-Forwarded-For` header
×
336
        // is the real origin.
×
337
        // https://stackoverflow.com/a/18517550/893113
×
338
        req.ip = req.headers['x-forwarded-for'].split(', ').pop()
×
339
      } else if (isOnFly()) {
1!
UNCOV
340
        // On Fly we can use the Fly-Client-IP header
×
UNCOV
341
        // https://fly.io/docs/reference/runtime-environment/#request-headers
×
UNCOV
342
        req.ip = req.headers['fly-client-ip']
×
UNCOV
343
          ? req.headers['fly-client-ip']
×
UNCOV
344
          : req.socket.remoteAddress
×
345
      } else {
1✔
346
        req.ip = req.socket.remoteAddress
1✔
347
      }
1✔
348
      next()
1✔
349
    })
1✔
350
    addHandlerAtIndex(this.camp, 1, cloudflareMiddleware())
1✔
351
  }
1✔
352

3✔
353
  /**
3✔
354
   * Set up Scoutcamp routes for 404/not found responses
3✔
355
   */
3✔
356
  registerErrorHandlers() {
3✔
357
    const { camp, config } = this
9✔
358
    const {
9✔
359
      public: { rasterUrl },
9✔
360
    } = config
9✔
361

9✔
362
    camp.route(/\.(gif|jpg)$/, (query, match, end, request) => {
9✔
363
      const [, format] = match
1✔
364
      makeSend(
1✔
365
        'svg',
1✔
366
        request.res,
1✔
367
        end,
1✔
368
      )(
1✔
369
        makeBadge({
1✔
370
          label: '410',
1✔
371
          message: `${format} no longer available`,
1✔
372
          color: 'lightgray',
1✔
373
          format: 'svg',
1✔
374
        }),
1✔
375
      )
1✔
376
    })
9✔
377

9✔
378
    if (!rasterUrl) {
9!
379
      camp.route(
×
380
        /^\/((?!img|assets\/)).*\.png$/,
×
381
        (query, match, end, request) => {
×
382
          makeSend(
×
383
            'svg',
×
384
            request.res,
×
385
            end,
×
386
          )(
×
387
            makeBadge({
×
388
              label: '404',
×
389
              message: 'raster badges not available',
×
390
              color: 'lightgray',
×
391
              format: 'svg',
×
UNCOV
392
            }),
×
UNCOV
393
          )
×
UNCOV
394
        },
×
UNCOV
395
      )
×
UNCOV
396
    }
×
397

9✔
398
    camp.notfound(/(\.svg|\.json|)$/, (query, match, end, request) => {
9✔
399
      const [, extension] = match
11✔
400
      const format = (extension || '.svg').replace(/^\./, '')
11✔
401

11✔
402
      makeSend(
11✔
403
        format,
11✔
404
        request.res,
11✔
405
        end,
11✔
406
      )(
11✔
407
        makeBadge({
11✔
408
          label: '404',
11✔
409
          message: 'badge not found',
11✔
410
          color: 'red',
11✔
411
          format,
11✔
412
        }),
11✔
413
      )
11✔
414
    })
9✔
415
  }
9✔
416

3✔
417
  /**
3✔
418
   * Set up a couple of redirects:
3✔
419
   * One for the raster badges.
3✔
420
   * Another to redirect the base URL /
3✔
421
   * (we use this to redirect {@link https://img.shields.io/}
3✔
422
   * to {@link https://shields.io/} )
3✔
423
   */
3✔
424
  registerRedirects() {
3✔
425
    const { config, camp } = this
9✔
426
    const {
9✔
427
      public: { rasterUrl, redirectUrl },
9✔
428
    } = config
9✔
429

9✔
430
    if (rasterUrl) {
9✔
431
      // Redirect to the raster server for raster versions of modern badges.
9✔
432
      camp.route(
9✔
433
        /^\/((?!img|assets\/)).*\.png$/,
9✔
434
        (queryParams, match, end, ask) => {
9✔
435
          ask.res.statusCode = 301
2✔
436
          ask.res.setHeader(
2✔
437
            'Location',
2✔
438
            rasterRedirectUrl({ rasterUrl }, ask.req.url),
2✔
439
          )
2✔
440

2✔
441
          const cacheDuration = (30 * 24 * 3600) | 0 // 30 days.
2✔
442
          ask.res.setHeader('Cache-Control', `max-age=${cacheDuration}`)
2✔
443

2✔
444
          ask.res.end()
2✔
445
        },
9✔
446
      )
9✔
447
    }
9✔
448

9✔
449
    if (redirectUrl) {
9✔
450
      camp.route(/^\/$/, (data, match, end, ask) => {
9✔
451
        ask.res.statusCode = 302
1✔
452
        ask.res.setHeader('Location', redirectUrl)
1✔
453
        ask.res.end()
1✔
454
      })
9✔
455
    }
9✔
456
  }
9✔
457

3✔
458
  /**
3✔
459
   * Iterate all the service classes defined in /services,
3✔
460
   * load each service and register a Scoutcamp route for each service.
3✔
461
   */
3✔
462
  async registerServices() {
3✔
463
    const { config, camp, metricInstance } = this
9✔
464
    const { apiProvider: githubApiProvider } = this.githubConstellation
9✔
465
    const { apiProvider: librariesIoApiProvider } =
9✔
466
      this.librariesioConstellation
9✔
467
    ;(await loadServiceClasses()).forEach(serviceClass =>
9✔
468
      serviceClass.register(
5,274✔
469
        {
5,274✔
470
          camp,
5,274✔
471
          handleRequest,
5,274✔
472
          githubApiProvider,
5,274✔
473
          librariesIoApiProvider,
5,274✔
474
          metricInstance,
5,274✔
475
        },
5,274✔
476
        {
5,274✔
477
          handleInternalErrors: config.public.handleInternalErrors,
5,274✔
478
          cacheHeaders: config.public.cacheHeaders,
5,274✔
479
          rasterUrl: config.public.rasterUrl,
5,274✔
480
          private: config.private,
5,274✔
481
          public: config.public,
5,274✔
482
        },
5,274✔
483
      ),
9✔
484
    )
9✔
485
  }
9✔
486

3✔
487
  bootstrapAgent() {
3✔
488
    /*
9✔
489
    Bootstrap global agent.
9✔
490
    This allows self-hosting users to configure a proxy with
9✔
491
    HTTP_PROXY, HTTPS_PROXY, NO_PROXY variables
9✔
492
    */
9✔
493
    if (!('GLOBAL_AGENT_ENVIRONMENT_VARIABLE_NAMESPACE' in process.env)) {
9✔
494
      process.env.GLOBAL_AGENT_ENVIRONMENT_VARIABLE_NAMESPACE = ''
3✔
495
    }
3✔
496

9✔
497
    const proxyPrefix = process.env.GLOBAL_AGENT_ENVIRONMENT_VARIABLE_NAMESPACE
9✔
498
    const HTTP_PROXY = process.env[`${proxyPrefix}HTTP_PROXY`] || null
9✔
499
    const HTTPS_PROXY = process.env[`${proxyPrefix}HTTPS_PROXY`] || null
9✔
500

9✔
501
    if (HTTP_PROXY || HTTPS_PROXY) {
9!
UNCOV
502
      bootstrap()
×
UNCOV
503
    }
×
504
  }
9✔
505

3✔
506
  /**
3✔
507
   * Start the HTTP server:
3✔
508
   * Bootstrap Scoutcamp,
3✔
509
   * Register handlers,
3✔
510
   * Start listening for requests on this.baseUrl()
3✔
511
   */
3✔
512
  async start() {
3✔
513
    const {
9✔
514
      bind: { port, address: hostname },
9✔
515
      ssl: { isSecure: secure, cert, key },
9✔
516
      requireCloudflare,
9✔
517
    } = this.config.public
9✔
518

9✔
519
    this.bootstrapAgent()
9✔
520

9✔
521
    log.log(`Server is starting up: ${this.baseUrl}`)
9✔
522

9✔
523
    const camp = (this.camp = Camp.create({
9✔
524
      documentRoot: this.config.public.documentRoot,
9✔
525
      port,
9✔
526
      hostname,
9✔
527
      secure,
9✔
528
      staticMaxAge: 300,
9✔
529
      cert,
9✔
530
      key,
9✔
531
    }))
9✔
532

9✔
533
    if (requireCloudflare) {
9✔
534
      this.requireCloudflare()
1✔
535
    }
1✔
536

9✔
537
    const { githubConstellation, metricInstance } = this
9✔
538
    await githubConstellation.initialize(camp)
9✔
539
    if (metricInstance) {
9✔
540
      if (this.config.public.metrics.prometheus.endpointEnabled) {
3✔
541
        metricInstance.registerMetricsEndpoint(camp)
1✔
542
      }
1✔
543
      if (this.influxMetrics) {
3✔
544
        this.influxMetrics.startPushingMetrics()
1✔
545
      }
1✔
546
    }
3✔
547

9✔
548
    camp.handle((req, res, next) => {
9✔
549
      // https://github.com/badges/shields/issues/3273
2,434✔
550
      res.setHeader('Access-Control-Allow-Origin', '*')
2,434✔
551
      // https://github.com/badges/shields/issues/10419
2,434✔
552
      res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin')
2,434✔
553

2,434✔
554
      next()
2,434✔
555
    })
9✔
556

9✔
557
    this.registerErrorHandlers()
9✔
558
    this.registerRedirects()
9✔
559
    await this.registerServices()
9✔
560

9✔
561
    camp.timeout = this.config.public.requestTimeoutSeconds * 1000
9✔
562
    if (this.config.public.requestTimeoutSeconds > 0) {
9✔
563
      camp.on('timeout', socket => {
9✔
564
        const maxAge = this.config.public.requestTimeoutMaxAgeSeconds
1✔
565
        socket.write('HTTP/1.1 408 Request Timeout\r\n')
1✔
566
        socket.write('Content-Type: text/html; charset=UTF-8\r\n')
1✔
567
        socket.write('Content-Encoding: UTF-8\r\n')
1✔
568
        socket.write(`Cache-Control: max-age=${maxAge}, s-maxage=${maxAge}\r\n`)
1✔
569
        socket.write('Connection: close\r\n\r\n')
1✔
570
        socket.write('Request Timeout')
1✔
571
        socket.end()
1✔
572
      })
9✔
573
    }
9✔
574
    camp.listenAsConfigured()
9✔
575

9✔
576
    await new Promise(resolve => camp.on('listening', () => resolve()))
9✔
577
  }
9✔
578

3✔
579
  static resetGlobalState() {
3✔
580
    // This state should be migrated to instance state. When possible, do not add new
2,397✔
581
    // global state.
2,397✔
582
    clearResourceCache()
2,397✔
583
  }
2,397✔
584

3✔
585
  reset() {
3✔
586
    this.constructor.resetGlobalState()
2,397✔
587
  }
2,397✔
588

3✔
589
  /**
3✔
590
   * Stop the HTTP server and clean up helpers
3✔
591
   */
3✔
592
  async stop() {
3✔
593
    if (this.camp) {
9✔
594
      await new Promise(resolve => this.camp.close(resolve))
9✔
595
      this.camp = undefined
9✔
596
    }
9✔
597

9✔
598
    if (this.cleanupMonitor) {
9!
UNCOV
599
      this.cleanupMonitor()
×
UNCOV
600
      this.cleanupMonitor = undefined
×
UNCOV
601
    }
×
602

9✔
603
    if (this.githubConstellation) {
9✔
604
      await this.githubConstellation.stop()
9✔
605
      this.githubConstellation = undefined
9✔
606
    }
9✔
607

9✔
608
    if (this.metricInstance) {
9✔
609
      if (this.influxMetrics) {
3✔
610
        this.influxMetrics.stopPushingMetrics()
1✔
611
      }
1✔
612
      this.metricInstance.stop()
3✔
613
    }
3✔
614
  }
9✔
615
}
3✔
616

3✔
617
export default Server
3✔
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