Skip to content

Commit

Permalink
Add [GithubCheckRuns] service (#7759)
Browse files Browse the repository at this point in the history
* Add [GithubCheckRuns] service

* Adjust ref parameter

* Rework

* Prettier

* Prettier

* Function

* Prettier

* Change CR to LF

* Adjust after #9233

* Lint camelCase

* Fix camelCase

* Fix prettier

* Switch to openAPI spec for examples

* Fix type of parameters

* Fix too many brackets

* Lint

* Add optional name filter

* Update tests

* Remove logo
  • Loading branch information
mbtools committed May 25, 2024
1 parent 6f0e7f0 commit 12f54f3
Show file tree
Hide file tree
Showing 3 changed files with 294 additions and 0 deletions.
169 changes: 169 additions & 0 deletions services/github/github-check-runs.service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import Joi from 'joi'
import countBy from 'lodash.countby'
import { pathParam, queryParam } from '../index.js'
import { nonNegativeInteger } from '../validators.js'
import { renderBuildStatusBadge } from '../build-status.js'
import { GithubAuthV3Service } from './github-auth-service.js'
import {
documentation as commonDocumentation,
httpErrorsFor,
} from './github-helpers.js'

const description = `
The Check Runs service shows the status of GitHub action runs.
${commonDocumentation}
`

const schema = Joi.object({
total_count: nonNegativeInteger,
check_runs: Joi.array()
.items(
Joi.object({
name: Joi.string().required(),
status: Joi.equal('completed', 'in_progress', 'queued').required(),
conclusion: Joi.equal(
'action_required',
'cancelled',
'failure',
'neutral',
'skipped',
'success',
'timed_out',
null,
).required(),
}),
)
.default([]),
}).required()

const queryParamSchema = Joi.object({
nameFilter: Joi.string(),
})

export default class GithubCheckRuns extends GithubAuthV3Service {
static category = 'build'
static route = {
base: 'github/check-runs',
pattern: ':user/:repo/:ref+',
queryParamSchema,
}

static openApi = {
'/github/check-runs/{user}/{repo}/{branch}': {
get: {
summary: 'GitHub branch check runs',
description,
parameters: [
pathParam({ name: 'user', example: 'badges' }),
pathParam({ name: 'repo', example: 'shields' }),
pathParam({ name: 'branch', example: 'master' }),
queryParam({
name: 'nameFilter',
description: 'Name of a check run',
example: 'test-lint',
}),
],
},
},
'/github/check-runs/{user}/{repo}/{commit}': {
get: {
summary: 'GitHub commit check runs',
description,
parameters: [
pathParam({ name: 'user', example: 'badges' }),
pathParam({ name: 'repo', example: 'shields' }),
pathParam({
name: 'commit',
example: '91b108d4b7359b2f8794a4614c11cb1157dc9fff',
}),
queryParam({
name: 'nameFilter',
description: 'Name of a check run',
example: 'test-lint',
}),
],
},
},
'/github/check-runs/{user}/{repo}/{tag}': {
get: {
summary: 'GitHub tag check runs',
description,
parameters: [
pathParam({ name: 'user', example: 'badges' }),
pathParam({ name: 'repo', example: 'shields' }),
pathParam({ name: 'tag', example: '3.3.0' }),
queryParam({
name: 'nameFilter',
description: 'Name of a check run',
example: 'test-lint',
}),
],
},
},
}

static defaultBadgeData = { label: 'checks' }

static transform(
{ total_count: totalCount, check_runs: checkRuns },
nameFilter,
) {
const filteredCheckRuns =
nameFilter && nameFilter.length > 0
? checkRuns.filter(checkRun => checkRun.name === nameFilter)
: checkRuns

return {
total: totalCount,
statusCounts: countBy(filteredCheckRuns, 'status'),
conclusionCounts: countBy(filteredCheckRuns, 'conclusion'),
}
}

static mapState({ total, statusCounts, conclusionCounts }) {
let state
if (total === 0) {
state = 'no check runs'
} else if (statusCounts.queued) {
state = 'queued'
} else if (statusCounts.in_progress) {
state = 'pending'
} else if (statusCounts.completed) {
// all check runs are completed, now evaluate conclusions
const orangeStates = ['action_required', 'stale']
const redStates = ['cancelled', 'failure', 'timed_out']

// assume "passing (green)"
state = 'passing'
for (const stateValue of Object.keys(conclusionCounts)) {
if (orangeStates.includes(stateValue)) {
// orange state renders "passing (orange)"
state = 'partially succeeded'
} else if (redStates.includes(stateValue)) {
// red state renders "failing (red)"
state = 'failing'
break
}
}
} else {
state = 'unknown status'
}
return state
}

async handle({ user, repo, ref }, { nameFilter }) {
// https://docs.github.com/en/rest/checks/runs#list-check-runs-for-a-git-reference
const json = await this._requestJson({
url: `/repos/${user}/${repo}/commits/${ref}/check-runs`,
httpErrors: httpErrorsFor('ref or repo not found'),
schema,
})

const state = this.constructor.mapState(
this.constructor.transform(json, nameFilter),
)

return renderBuildStatusBadge({ status: state })
}
}
94 changes: 94 additions & 0 deletions services/github/github-check-runs.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { test, given } from 'sazerac'
import GithubCheckRuns from './github-check-runs.service.js'

describe('GithubCheckRuns.transform', function () {
test(GithubCheckRuns.transform, () => {
given(
{
total_count: 3,
check_runs: [
{ status: 'completed', conclusion: 'success' },
{ status: 'completed', conclusion: 'failure' },
{ status: 'in_progress', conclusion: null },
],
},
'',
).expect({
total: 3,
statusCounts: { completed: 2, in_progress: 1 },
conclusionCounts: { success: 1, failure: 1, null: 1 },
})

given(
{
total_count: 3,
check_runs: [
{ name: 'test1', status: 'completed', conclusion: 'success' },
{ name: 'test2', status: 'completed', conclusion: 'failure' },
{ name: 'test3', status: 'in_progress', conclusion: null },
],
},
'',
).expect({
total: 3,
statusCounts: { completed: 2, in_progress: 1 },
conclusionCounts: { success: 1, failure: 1, null: 1 },
})

given(
{
total_count: 3,
check_runs: [
{ name: 'test1', status: 'completed', conclusion: 'success' },
{ name: 'test2', status: 'completed', conclusion: 'failure' },
{ name: 'test3', status: 'in_progress', conclusion: null },
],
},
'test1',
).expect({
total: 3,
statusCounts: { completed: 1 },
conclusionCounts: { success: 1 },
})
})
})

describe('GithubCheckRuns', function () {
test(GithubCheckRuns.mapState, () => {
given({
total: 0,
statusCounts: null,
conclusionCounts: null,
}).expect('no check runs')
given({
total: 1,
statusCounts: { queued: 1 },
conclusionCounts: null,
}).expect('queued')
given({
total: 1,
statusCounts: { in_progress: 1 },
conclusionCounts: null,
}).expect('pending')
given({
total: 1,
statusCounts: { completed: 1 },
conclusionCounts: { success: 1 },
}).expect('passing')
given({
total: 2,
statusCounts: { completed: 2 },
conclusionCounts: { success: 1, stale: 1 },
}).expect('partially succeeded')
given({
total: 3,
statusCounts: { completed: 3 },
conclusionCounts: { success: 1, stale: 1, failure: 1 },
}).expect('failing')
given({
total: 1,
statusCounts: { somethingelse: 1 },
conclusionCounts: null,
}).expect('unknown status')
})
})
31 changes: 31 additions & 0 deletions services/github/github-check-runs.tester.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { createServiceTester } from '../tester.js'
import { isBuildStatus } from '../build-status.js'
export const t = await createServiceTester()

t.create('check runs - for branch')
.get('/badges/shields/master.json')
.expectBadge({
label: 'checks',
message: isBuildStatus,
})

t.create('check runs - for branch with filter')
.get('/badges/shields/master.json?nameFilter=test-lint')
.expectBadge({
label: 'checks',
message: isBuildStatus,
})

t.create('check runs - no tests')
.get('/badges/shields/5d4ab86b1b5ddfb3c4a70a70bd19932c52603b8c.json')
.expectBadge({
label: 'checks',
message: 'no check runs',
})

t.create('check runs - nonexistent ref')
.get('/badges/shields/this-ref-does-not-exist.json')
.expectBadge({
label: 'checks',
message: 'ref or repo not found',
})

0 comments on commit 12f54f3

Please sign in to comment.