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

badges / shields / 19885636131

02 Dec 2025 08:36PM UTC coverage: 98.166% (+0.03%) from 98.14%
19885636131

push

github

web-flow
chore(deps): bump joi from 18.0.1 to 18.0.2 (#11550)

Bumps [joi](https://github.com/hapijs/joi) from 18.0.1 to 18.0.2.
- [Commits](https://github.com/hapijs/joi/compare/v18.0.1...v18.0.2)

---
updated-dependencies:
- dependency-name: joi
  dependency-version: 18.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

5949 of 6209 branches covered (95.81%)

49826 of 50757 relevant lines covered (98.17%)

133.64 hits per line

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

93.44
/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
  fileSizeBytes,
3✔
22
  nonNegativeInteger,
3✔
23
  optionalUrl,
3✔
24
  url as requiredUrl,
3✔
25
} from '../../services/validators.js'
3✔
26
import log from './log.js'
3✔
27
import PrometheusMetrics from './prometheus-metrics.js'
3✔
28
import InfluxMetrics from './influx-metrics.js'
3✔
29
const { URL } = url
3✔
30

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

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

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

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

3✔
225
function addHandlerAtIndex(camp, index, handlerFn) {
2✔
226
  camp.stack.splice(index, 0, handlerFn)
2✔
227
}
2✔
228

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

3✔
233
function isOnFly() {
1✔
234
  return !!process.env.FLY_APP_NAME
1✔
235
}
1✔
236

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

18✔
274
    this.githubConstellation = new GithubConstellation({
18✔
275
      service: publicConfig.services.github,
18✔
276
      metricsIntervalSeconds: publicConfig.metrics.influx.intervalSeconds,
18✔
277
      private: privateConfig,
18✔
278
    })
18✔
279

18✔
280
    this.librariesioConstellation = new LibrariesIoConstellation({
18✔
281
      private: privateConfig,
18✔
282
    })
18✔
283

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

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

3✔
311
  get port() {
3✔
312
    const {
×
313
      port,
×
314
      ssl: { isSecure },
×
315
    } = this.config.public
×
316
    return port || (isSecure ? 443 : 80)
×
317
  }
×
318

3✔
319
  get baseUrl() {
3✔
320
    const {
16✔
321
      bind: { address, port },
16✔
322
      ssl: { isSecure },
16✔
323
    } = this.config.public
16✔
324

16✔
325
    return url.format({
16✔
326
      protocol: isSecure ? 'https' : 'http',
16!
327
      hostname: address,
16✔
328
      port,
16✔
329
      pathname: '/',
16✔
330
    })
16✔
331
  }
16✔
332

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

3✔
358
  /**
3✔
359
   * Set up Scoutcamp routes for 404/not found responses
3✔
360
   */
3✔
361
  registerErrorHandlers() {
3✔
362
    const { camp, config } = this
9✔
363
    const {
9✔
364
      public: { rasterUrl },
9✔
365
    } = config
9✔
366

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

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

9✔
403
    camp.notfound(/(\.svg|\.json|)$/, (query, match, end, request) => {
9✔
404
      const [, extension] = match
10✔
405
      const format = (extension || '.svg').replace(/^\./, '')
10✔
406

10✔
407
      request.res.statusCode = 200
10✔
408

10✔
409
      makeSend(
10✔
410
        format,
10✔
411
        request.res,
10✔
412
        end,
10✔
413
      )(
10✔
414
        makeBadge({
10✔
415
          label: '404',
10✔
416
          message: 'badge not found',
10✔
417
          color: 'red',
10✔
418
          format,
10✔
419
        }),
10✔
420
      )
10✔
421
    })
9✔
422
  }
9✔
423

3✔
424
  /**
3✔
425
   * Set up a couple of redirects:
3✔
426
   * One for the raster badges.
3✔
427
   * Another to redirect the base URL /
3✔
428
   * (we use this to redirect {@link https://img.shields.io/}
3✔
429
   * to {@link https://shields.io/} )
3✔
430
   */
3✔
431
  registerRedirects() {
3✔
432
    const { config, camp } = this
9✔
433
    const {
9✔
434
      public: { rasterUrl, redirectUrl },
9✔
435
    } = config
9✔
436

9✔
437
    if (rasterUrl) {
9✔
438
      // Redirect to the raster server for raster versions of modern badges.
9✔
439
      camp.route(
9✔
440
        /^\/((?!img|assets\/)).*\.png$/,
9✔
441
        (queryParams, match, end, ask) => {
9✔
442
          ask.res.statusCode = 301
2✔
443
          ask.res.setHeader(
2✔
444
            'Location',
2✔
445
            rasterRedirectUrl({ rasterUrl }, ask.req.url),
2✔
446
          )
2✔
447

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

2✔
451
          ask.res.end()
2✔
452
        },
9✔
453
      )
9✔
454
    }
9✔
455

9✔
456
    if (redirectUrl) {
9✔
457
      camp.route(/^\/$/, (data, match, end, ask) => {
9✔
458
        ask.res.statusCode = 302
1✔
459
        ask.res.setHeader('Location', redirectUrl)
1✔
460
        ask.res.end()
1✔
461
      })
9✔
462
    }
9✔
463
  }
9✔
464

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

3✔
494
  bootstrapAgent() {
3✔
495
    /*
9✔
496
    Bootstrap global agent.
9✔
497
    This allows self-hosting users to configure a proxy with
9✔
498
    HTTP_PROXY, HTTPS_PROXY, NO_PROXY variables
9✔
499
    */
9✔
500
    if (!('GLOBAL_AGENT_ENVIRONMENT_VARIABLE_NAMESPACE' in process.env)) {
9✔
501
      process.env.GLOBAL_AGENT_ENVIRONMENT_VARIABLE_NAMESPACE = ''
3✔
502
    }
3✔
503

9✔
504
    const proxyPrefix = process.env.GLOBAL_AGENT_ENVIRONMENT_VARIABLE_NAMESPACE
9✔
505
    const HTTP_PROXY = process.env[`${proxyPrefix}HTTP_PROXY`] || null
9✔
506
    const HTTPS_PROXY = process.env[`${proxyPrefix}HTTPS_PROXY`] || null
9✔
507

9✔
508
    if (HTTP_PROXY || HTTPS_PROXY) {
9!
509
      bootstrap()
×
510
    }
×
511
  }
9✔
512

3✔
513
  /**
3✔
514
   * Start the HTTP server:
3✔
515
   * Bootstrap Scoutcamp,
3✔
516
   * Register handlers,
3✔
517
   * Start listening for requests on this.baseUrl()
3✔
518
   */
3✔
519
  async start() {
3✔
520
    const {
9✔
521
      bind: { port, address: hostname },
9✔
522
      ssl: { isSecure: secure, cert, key },
9✔
523
      requireCloudflare,
9✔
524
    } = this.config.public
9✔
525

9✔
526
    this.bootstrapAgent()
9✔
527

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

9✔
530
    const camp = (this.camp = Camp.create({
9✔
531
      documentRoot: this.config.public.documentRoot,
9✔
532
      port,
9✔
533
      hostname,
9✔
534
      secure,
9✔
535
      staticMaxAge: 300,
9✔
536
      cert,
9✔
537
      key,
9✔
538
    }))
9✔
539

9✔
540
    if (requireCloudflare) {
9✔
541
      this.requireCloudflare()
1✔
542
    }
1✔
543

9✔
544
    const { githubConstellation, metricInstance } = this
9✔
545
    await githubConstellation.initialize(camp, metricInstance)
9✔
546
    if (metricInstance) {
9✔
547
      metricInstance.registerMetricsEndpoint(
3✔
548
        camp,
3✔
549
        this.config.public.metrics.prometheus.endpointEnabled,
3✔
550
      )
3✔
551
      if (this.influxMetrics) {
3✔
552
        this.influxMetrics.startPushingMetrics()
1✔
553
      }
1✔
554
    }
3✔
555

9✔
556
    camp.handle((req, res, next) => {
9✔
557
      // https://github.com/badges/shields/issues/3273
2,469✔
558
      res.setHeader('Access-Control-Allow-Origin', '*')
2,469✔
559
      // https://github.com/badges/shields/issues/10419
2,469✔
560
      res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin')
2,469✔
561

2,469✔
562
      next()
2,469✔
563
    })
9✔
564

9✔
565
    this.registerErrorHandlers()
9✔
566
    this.registerRedirects()
9✔
567
    await this.registerServices()
9✔
568

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

9✔
584
    await new Promise(resolve => camp.on('listening', () => resolve()))
9✔
585
  }
9✔
586

3✔
587
  static resetGlobalState() {
3✔
588
    // This state should be migrated to instance state. When possible, do not add new
2,426✔
589
    // global state.
2,426✔
590
    clearResourceCache()
2,426✔
591
  }
2,426✔
592

3✔
593
  reset() {
3✔
594
    this.constructor.resetGlobalState()
2,426✔
595
  }
2,426✔
596

3✔
597
  /**
3✔
598
   * Stop the HTTP server and clean up helpers
3✔
599
   */
3✔
600
  async stop() {
3✔
601
    if (this.camp) {
9✔
602
      await new Promise(resolve => this.camp.close(resolve))
9✔
603
      this.camp = undefined
9✔
604
    }
9✔
605

9✔
606
    if (this.cleanupMonitor) {
9!
607
      this.cleanupMonitor()
×
608
      this.cleanupMonitor = undefined
×
609
    }
×
610

9✔
611
    if (this.githubConstellation) {
9✔
612
      await this.githubConstellation.stop()
9✔
613
      this.githubConstellation = undefined
9✔
614
    }
9✔
615

9✔
616
    if (this.metricInstance) {
9✔
617
      if (this.influxMetrics) {
3✔
618
        this.influxMetrics.stopPushingMetrics()
1✔
619
      }
1✔
620
      this.metricInstance.stop()
3✔
621
    }
3✔
622
  }
9✔
623
}
3✔
624

3✔
625
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