This repository has been archived by the owner on Aug 27, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 63
/
Specification.js
183 lines (161 loc) · 5.55 KB
/
Specification.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
import Check from './Check'
import { logger } from '../../common/debug'
import * as EVENTS from '../model/GithubEvents'
const CHECK_TYPE = 'specification'
const CONTEXT = 'zappr/pr/specification'
const ACTIONS = ['opened', 'edited', 'reopened', 'synchronize']
const DEFAULT_REQUIRED_LENGTH = 8
const ISSUE_PATTERN = /^(?:[-\w]+\/[-\w]+)?#\d+$/
const URL_PATTERN = /\bhttps?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/i
const [MINIMUM_LENGTH, CONTAINS_URL, CONTAINS_ISSUE_NUMBER, TEMPLATE_DIFFERS_FROM_BODY] =
['minimum-length', 'contains-url', 'contains-issue-number',
'differs-from-body']
const debug = logger(CHECK_TYPE)
const info = logger(CHECK_TYPE, 'info')
const error = logger(CHECK_TYPE, 'error')
const status = (description, state = 'success') => ({
description,
state,
context: CONTEXT
})
const isLongEnough = (str, requiredLength) => (str || '').length > requiredLength
const containsPattern = pattern => str => (str || '').split(' ')
.some(s => pattern.test(s))
const containsUrl = containsPattern(URL_PATTERN)
const containsIssueNumber = containsPattern(ISSUE_PATTERN)
export default class Specification extends Check {
static TYPE = CHECK_TYPE
static CONTEXT = CONTEXT
static NAME = 'Specification check'
static HOOK_EVENTS = [EVENTS.PULL_REQUEST]
/**
* @param {GithubService} github
*/
constructor(github) {
super()
this.github = github
}
async execute(config, hookPayload, token) {
const {action, pull_request: pr, repository: repo} = hookPayload
if (ACTIONS.indexOf(action) === -1 || !pr || 'open' !== pr.state) {
debug(`${repo.full_name}#${pr.number}: Nothing to do, action was "${action}" with state "${pr.state}".`)
return
}
await this.validate(config, pr, repo, token)
}
/**
* Should do next validation steps:
* * check if title's length is more than required length
* * check if body has one of the following (in order):
* * at least one issue number
* * at least one link
* * it's length is more than required length
*
* @param config config object
* @param pr Github's PR object
* @param repo Github's repository object
* @param token access token
*/
async validate(config, pr, repo, token) {
const {title = '', body = '', head: {sha}} = pr
const {owner: {login: user}} = repo
const {
specification: {
title: titleChecks = {},
body: bodyChecks = {},
template: templateChecks = {}
} = {}
} = config
try {
await Promise.all([
this._validateTitle(title, titleChecks),
this._validateTemplate(body, user, repo.name, token, templateChecks),
this._validateBody(body, bodyChecks)
])
info(`${repo.full_name}#${pr.number}: Set status to success`)
return this.github.setCommitStatus(user, repo.name, sha,
status('PR has passed specification checks'), token)
} catch (e) {
info(`${repo.full_name}#${pr.number}: Set status to failure: ${e.message}`)
return this.github.setCommitStatus(user, repo.name, sha, status(
e.message, 'failure'), token)
}
}
/**
*
* @param {string} title to be validated
* @param {Object} checks part of `specification` that contains title's checks
*/
_validateTitle(title, checks = {}) {
const {
[MINIMUM_LENGTH]: {
enabled: shouldCheckLength = true,
length: requiredLength = DEFAULT_REQUIRED_LENGTH
} = {}
} = checks
if (shouldCheckLength && !isLongEnough(title, requiredLength)) {
throw new Error(`PR's title is too short (${title.length}/${requiredLength})`)
}
}
async _validateTemplate(body, user, repo, token, checks = {}) {
const shouldCheckWasAdjusted = checks[TEMPLATE_DIFFERS_FROM_BODY]
if (!shouldCheckWasAdjusted) {
return
}
let template
try {
template = await this.github.readPullRequestTemplate(user, repo, token)
} catch (e) {
info(`${user}/${repo}: No PULL_REQUEST_TEMPLATE found`)
return
}
if (template.trim() === body.trim()) {
throw new Error(`PR's body is the same as template`)
}
}
/**
* @param {string} body to be validated
* @param {Object} checks part of `specification` that contains body's checks
*/
_validateBody(body, checks = {}) {
const {
[MINIMUM_LENGTH]: {
enabled: shouldCheckLength = true,
length: requiredLength = DEFAULT_REQUIRED_LENGTH
} = {},
[CONTAINS_URL]: shouldCheckUrl = true,
[CONTAINS_ISSUE_NUMBER]: shouldCheckIssue = true
} = checks
const checksMapping = {
[CONTAINS_URL]: {
enabled: shouldCheckUrl,
fn: containsUrl.bind(null, body)
},
[CONTAINS_ISSUE_NUMBER]: {
enabled: shouldCheckIssue,
fn: containsIssueNumber.bind(null, body)
},
[MINIMUM_LENGTH]: {
enabled: shouldCheckLength,
fn: isLongEnough.bind(null, body, requiredLength)
}
}
// array to force the order
const [success, failedChecks] = [
CONTAINS_ISSUE_NUMBER, CONTAINS_URL, MINIMUM_LENGTH
].reduce(([success, failedChecks], checkName) => {
const {enabled, fn: check} = checksMapping[checkName]
if (enabled) {
const res = check()
if (!res) {
failedChecks.push(`'${checkName}'`)
}
success = success || res
}
return [success, failedChecks]
}, [false, []])
if (!success && failedChecks.length > 0) {
throw new Error(`PR's body failed check ${failedChecks[0]}`)
}
}
}