diff --git a/config/custom-environment-variables.yml b/config/custom-environment-variables.yml index 352d72dd4c..87b75a1589 100644 --- a/config/custom-environment-variables.yml +++ b/config/custom-environment-variables.yml @@ -45,6 +45,7 @@ private: azure_devops_token: 'AZURE_DEVOPS_TOKEN' bintray_user: 'BINTRAY_USER' bintray_apikey: 'BINTRAY_API_KEY' + drone_token: 'DRONE_TOKEN' gh_client_id: 'GH_CLIENT_ID' gh_client_secret: 'GH_CLIENT_SECRET' gh_token: 'GH_TOKEN' diff --git a/doc/server-secrets.md b/doc/server-secrets.md index cd7a7ff360..c4bf55a3c6 100644 --- a/doc/server-secrets.md +++ b/doc/server-secrets.md @@ -43,6 +43,13 @@ An Azure DevOps Token (PAT) is required for accessing [private Azure DevOps proj The bintray API [requires authentication](https://bintray.com/docs/api/#_authentication) Create an account and obtain a token from the user profile page. +## Drone + +- `DRONE_TOKEN` (yml: `drone_token`) + +The self-hosted Drone API [requires authentication](https://0-8-0.docs.drone.io/api-authentication/) +Login to your Drone instance and obtain a token from the user profile page. + ## GitHub - `GH_TOKEN` (yml: `gh_token`) diff --git a/services/build-status.js b/services/build-status.js index 82c9221307..971f803869 100644 --- a/services/build-status.js +++ b/services/build-status.js @@ -13,7 +13,13 @@ const greenStatuses = [ const orangeStatuses = ['partially succeeded', 'unstable', 'timeout'] -const redStatuses = ['error', 'failed', 'failing', 'infrastructure_failure'] +const redStatuses = [ + 'error', + 'failed', + 'failing', + 'failure', + 'infrastructure_failure', +] const otherStatuses = [ 'building', diff --git a/services/build-status.spec.js b/services/build-status.spec.js index ce8f9e9169..e0293f4821 100644 --- a/services/build-status.spec.js +++ b/services/build-status.spec.js @@ -58,6 +58,7 @@ test(renderBuildStatusBadge, () => { given({ status: 'error' }), given({ status: 'failed' }), given({ status: 'failing' }), + given({ status: 'failure' }), given({ status: 'infrastructure_failure' }), ]).assert('should be red', b => expect(b).to.include({ color: 'red' })) }) diff --git a/services/drone/drone-build.service.js b/services/drone/drone-build.service.js new file mode 100644 index 0000000000..9d918fb932 --- /dev/null +++ b/services/drone/drone-build.service.js @@ -0,0 +1,107 @@ +'use strict' + +const Joi = require('joi') +const serverSecrets = require('../../lib/server-secrets') +const { isBuildStatus, renderBuildStatusBadge } = require('../build-status') +const { optionalUrl } = require('../validators') +const { BaseJsonService } = require('..') + +const DroneBuildSchema = Joi.object({ + status: Joi.alternatives() + .try(isBuildStatus, Joi.equal('none')) + .required(), +}).required() + +const queryParamSchema = Joi.object({ + server: optionalUrl, +}).required() + +module.exports = class DroneBuild extends BaseJsonService { + static get category() { + return 'build' + } + + static get route() { + return { + queryParamSchema, + base: 'drone/build', + pattern: ':user/:repo/:branch*', + } + } + + static get defaultBadgeData() { + return { + label: 'build', + } + } + + async handle({ user, repo, branch }, { server }) { + const options = { + qs: { + ref: branch ? `refs/heads/${branch}` : undefined, + }, + } + if (serverSecrets.drone_token) { + options.headers = { + Authorization: `Bearer ${serverSecrets.drone_token}`, + } + } + if (!server) { + server = 'https://cloud.drone.io' + } + const json = await this._requestJson({ + options, + schema: DroneBuildSchema, + url: `${server}/api/repos/${user}/${repo}/builds/latest`, + errorMessages: { + 401: 'repo not found or not authorized', + }, + }) + return renderBuildStatusBadge({ status: json.status }) + } + + static get examples() { + return [ + { + title: 'Drone (cloud)', + pattern: ':user/:repo', + namedParams: { + user: 'drone', + repo: 'drone', + }, + staticPreview: renderBuildStatusBadge({ status: 'success' }), + }, + { + title: 'Drone (cloud) with branch', + pattern: ':user/:repo/:branch', + namedParams: { + user: 'drone', + repo: 'drone', + branch: 'master', + }, + staticPreview: renderBuildStatusBadge({ status: 'success' }), + }, + { + title: 'Drone (self-hosted)', + pattern: ':user/:repo', + queryParams: { server: 'https://drone.shields.io' }, + namedParams: { + user: 'badges', + repo: 'shields', + }, + staticPreview: renderBuildStatusBadge({ status: 'success' }), + }, + { + title: 'Drone (self-hosted) with branch', + pattern: ':user/:repo/:branch', + queryParams: { server: 'https://drone.shields.io' }, + namedParams: { + user: 'badges', + repo: 'shields', + branch: 'feat/awesome-thing', + }, + staticPreview: renderBuildStatusBadge({ status: 'success' }), + }, + ] + } +} diff --git a/services/drone/drone-build.tester.js b/services/drone/drone-build.tester.js new file mode 100644 index 0000000000..1ea88667cf --- /dev/null +++ b/services/drone/drone-build.tester.js @@ -0,0 +1,62 @@ +'use strict' + +const Joi = require('joi') +const { isBuildStatus } = require('../build-status') +const t = (module.exports = require('../tester').createServiceTester()) +const { mockDroneCreds, token, restore } = require('./drone-test-helpers') + +t.create('cloud-hosted build status on default branch') + .get('/drone/drone.json') + .expectBadge({ + label: 'build', + message: Joi.alternatives().try(isBuildStatus, Joi.equal('none')), + }) + +t.create('cloud-hosted build status on named branch') + .get('/drone/drone/master.json') + .expectBadge({ + label: 'build', + message: Joi.alternatives().try(isBuildStatus, Joi.equal('none')), + }) + +t.create('cloud-hosted build status on unknown repo') + .get('/this-repo/does-not-exist.json') + .expectBadge({ + label: 'build', + message: 'repo not found or not authorized', + }) + +t.create('self-hosted build status on default branch') + .before(mockDroneCreds) + .get('/badges/shields.json?server=https://drone.shields.io') + .intercept(nock => + nock('https://drone.shields.io/api/repos', { + reqheaders: { authorization: `Bearer ${token}` }, + }) + .get('/badges/shields/builds/latest') + .reply(200, { status: 'success' }) + ) + .finally(restore) + .expectBadge({ + label: 'build', + message: 'passing', + }) + +t.create('self-hosted build status on named branch') + .before(mockDroneCreds) + .get( + '/badges/shields/feat/awesome-thing.json?server=https://drone.shields.io' + ) + .intercept(nock => + nock('https://drone.shields.io/api/repos', { + reqheaders: { authorization: `Bearer ${token}` }, + }) + .get('/badges/shields/builds/latest') + .query({ ref: 'refs/heads/feat/awesome-thing' }) + .reply(200, { status: 'success' }) + ) + .finally(restore) + .expectBadge({ + label: 'build', + message: 'passing', + }) diff --git a/services/drone/drone-test-helpers.js b/services/drone/drone-test-helpers.js new file mode 100644 index 0000000000..8cc3c5b2c5 --- /dev/null +++ b/services/drone/drone-test-helpers.js @@ -0,0 +1,21 @@ +'use strict' + +const sinon = require('sinon') +const serverSecrets = require('../../lib/server-secrets') + +const token = 'my-token' + +function mockDroneCreds() { + serverSecrets['drone_token'] = undefined + sinon.stub(serverSecrets, 'drone_token').value(token) +} + +function restore() { + sinon.restore() +} + +module.exports = { + token, + mockDroneCreds, + restore, +}