-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add [GithubCheckRuns] service (#7759)
* 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
Showing
3 changed files
with
294 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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') | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', | ||
}) |