Migrate to TypeScript to avoid silent errors and improve debugging
This commit is contained in:
parent
772c3dc438
commit
aaed828003
|
@ -6,15 +6,11 @@
|
|||
"configurations": [
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Debug",
|
||||
"program": "${workspaceFolder}/node_modules/probot/bin/probot-run.js",
|
||||
"args": [
|
||||
"${workspaceFolder}/index.js"
|
||||
],
|
||||
"env": {
|
||||
"DEBUG": "true"
|
||||
}
|
||||
"request": "attach",
|
||||
"name": "Attach to Process",
|
||||
"restart": true,
|
||||
"protocol": "inspector",
|
||||
"port": 9229
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"typescript.tsdk": "node_modules/typescript/lib"
|
||||
}
|
203
index.js
203
index.js
|
@ -1,203 +0,0 @@
|
|||
// Checks API example
|
||||
// See: https://developer.github.com/v3/checks/ to learn more
|
||||
const pendingChecks = []
|
||||
|
||||
const checkDependencies = require('./lib/dependency-check')
|
||||
const Humanize = require('humanize-plus')
|
||||
|
||||
module.exports = app => {
|
||||
app.on(['check_suite.requested'], checkSuiteRequested)
|
||||
app.on(['check_run.rerequested'], checkRunRerequested)
|
||||
|
||||
async function checkSuiteRequested(context) {
|
||||
if (context.isBot) {
|
||||
return
|
||||
}
|
||||
|
||||
return await checkSuiteAsync(context, context.payload.check_suite)
|
||||
}
|
||||
|
||||
async function checkRunRerequested(context) {
|
||||
if (context.isBot) {
|
||||
return
|
||||
}
|
||||
|
||||
const { check_suite } = context.payload.check_run
|
||||
return await checkSuiteAsync(context, check_suite)
|
||||
}
|
||||
|
||||
async function checkSuiteAsync(context, check_suite) {
|
||||
const { head_branch, head_sha } = check_suite
|
||||
|
||||
// Probot API note: context.repo() => {username: 'hiimbex', repo: 'testing-things'}
|
||||
const check = context.repo({
|
||||
name: 'packages-check-bot',
|
||||
head_branch: head_branch,
|
||||
head_sha: head_sha,
|
||||
started_at: (new Date()).toISOString()
|
||||
})
|
||||
|
||||
try {
|
||||
if (pendingChecks[head_sha]) {
|
||||
// Already running, ignore
|
||||
return
|
||||
}
|
||||
|
||||
context.log.info(`checking ${context.payload.repository.full_name}#${head_branch} (${head_sha}) (check_suite.id #${check_suite.id})
|
||||
Pull requests: ${Humanize.oxford(check_suite.pull_requests.map(pr => pr.url), 5)}`)
|
||||
|
||||
check.status = 'in_progress'
|
||||
check.output = {
|
||||
title: 'package.json check',
|
||||
summary: 'Checking any new/updated dependencies...'
|
||||
}
|
||||
|
||||
if (context.payload.action === 'rerequested') {
|
||||
pendingChecks[head_sha] = { ...check, check_run_id: context.payload.check_run.id }
|
||||
queueCheckAsync(context, check_suite)
|
||||
return
|
||||
} else {
|
||||
pendingChecks[head_sha] = { ...check }
|
||||
const createResponse = await context.github.checks.create(check)
|
||||
context.log.debug(`create checks status: ${createResponse.status}`)
|
||||
|
||||
pendingChecks[head_sha] = { ...check, check_run_id: createResponse.data.id }
|
||||
queueCheckAsync(context, check_suite)
|
||||
return createResponse
|
||||
}
|
||||
} catch (e) {
|
||||
context.log.error(e)
|
||||
|
||||
// Report error back to GitHub
|
||||
check.status = 'completed'
|
||||
check.conclusion = 'cancelled'
|
||||
check.completed_at = (new Date()).toISOString()
|
||||
check.output = {
|
||||
title: 'package.json check',
|
||||
summary: e.message
|
||||
}
|
||||
|
||||
return context.github.checks.create(check)
|
||||
}
|
||||
}
|
||||
|
||||
// For more information on building apps:
|
||||
// https://probot.github.io/docs/
|
||||
|
||||
// To get your app running against GitHub, see:
|
||||
// https://probot.github.io/docs/development/
|
||||
}
|
||||
|
||||
const timeout = ms => new Promise(res => setTimeout(res, ms))
|
||||
|
||||
async function queueCheckAsync(context, check_suite) {
|
||||
try {
|
||||
const { before, head_sha } = check_suite
|
||||
|
||||
const compareResponse = await context.github.repos.compareCommits(context.repo({
|
||||
base: before,
|
||||
head: head_sha
|
||||
}))
|
||||
context.log.debug(`compare commits status: ${compareResponse.status}, ${compareResponse.data.files.length} file(s)`)
|
||||
|
||||
let check = pendingChecks[head_sha]
|
||||
let checkedDepCount = 0
|
||||
let packageJsonFilenames = []
|
||||
const packageFilenameRegex = /^package\.json(.orig)?$/g
|
||||
|
||||
check.output.annotations = undefined
|
||||
for (const file of compareResponse.data.files) {
|
||||
switch (file.status) {
|
||||
case 'added':
|
||||
case 'modified':
|
||||
if (packageFilenameRegex.test(file.filename)) {
|
||||
packageJsonFilenames.push(file.filename)
|
||||
checkedDepCount += await checkPackageFileAsync(check, context, file, head_sha)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
check.status = 'completed'
|
||||
check.completed_at = (new Date()).toISOString()
|
||||
|
||||
if (!check.output.annotations) {
|
||||
check.conclusion = 'neutral'
|
||||
check.output.summary = 'No changes to dependencies'
|
||||
} else if (check.output.annotations.length === 0) {
|
||||
check.conclusion = 'success'
|
||||
check.output.summary = 'All dependencies are good!'
|
||||
} else {
|
||||
check.conclusion = 'failure'
|
||||
|
||||
const warnings = check.output.annotations.filter(a => a.annotation_level === 'warning').length
|
||||
const failures = check.output.annotations.filter(a => a.annotation_level === 'failure').length
|
||||
const uniqueProblemDependencies = [...new Set(check.output.annotations.map(a => a.dependency))]
|
||||
check.output.summary = `Checked ${checkedDepCount} ${Humanize.pluralize(checkedDepCount, 'dependency', 'dependencies')} in ${Humanize.oxford(packageJsonFilenames.map(f => `\`${f}\``), 3)}.
|
||||
${Humanize.boundedNumber(failures, 10)} ${Humanize.pluralize(failures, 'failure')}, ${Humanize.boundedNumber(warnings, 10)} ${Humanize.pluralize(warnings, 'warning')} in ${Humanize.oxford(uniqueProblemDependencies.map(f => `\`${f}\``), 3)} need your attention!`
|
||||
}
|
||||
|
||||
// Remove helper data from annotation objects
|
||||
const annotations = check.output.annotations
|
||||
for (const annotation of annotations) {
|
||||
delete annotation['dependency']
|
||||
}
|
||||
|
||||
for (let annotationIndex = 0; annotationIndex < annotations.length; annotationIndex += 50) {
|
||||
const annotationsSlice = annotations.length > 50 ? annotations.slice(annotationIndex, annotationIndex + 50) : annotations
|
||||
check.output.annotations = annotationsSlice
|
||||
|
||||
for (let attempts = 3; attempts >= 0; ) {
|
||||
try {
|
||||
const updateResponse = await context.github.checks.update({
|
||||
owner: check.owner,
|
||||
repo: check.repo,
|
||||
check_run_id: check.check_run_id,
|
||||
name: check.name,
|
||||
//details_url: check.details_url,
|
||||
external_id: check.external_id,
|
||||
started_at: check.started_at,
|
||||
status: check.status,
|
||||
conclusion: check.conclusion,
|
||||
completed_at: check.completed_at,
|
||||
output: check.output
|
||||
})
|
||||
context.log.debug(`update checks status: ${updateResponse.status}`)
|
||||
break
|
||||
} catch (error) {
|
||||
if (--attempts <= 0) {
|
||||
throw error
|
||||
}
|
||||
context.log.warn(`error while updating check run, will try again in 30 seconds: ${error.message}`)
|
||||
await timeout(30000)
|
||||
}
|
||||
}
|
||||
}
|
||||
check.output.annotations = annotations
|
||||
delete pendingChecks[head_sha]
|
||||
} catch (error) {
|
||||
context.log.error(error)
|
||||
// This function isn't usually awaited for, so there's no point in rethrowing
|
||||
}
|
||||
}
|
||||
|
||||
async function checkPackageFileAsync(check, context, file, head_sha) {
|
||||
const contentsResponse = await context.github.repos.getContents(context.repo({
|
||||
path: file.filename,
|
||||
ref: head_sha
|
||||
}))
|
||||
context.log.debug(`get contents response: ${contentsResponse.status}`)
|
||||
if (contentsResponse.status >= 300) {
|
||||
throw new `HTTP error ${contentsResponse.status} (${contentsResponse.statusText}) fetching ${file.filename}`
|
||||
}
|
||||
|
||||
const contents = Buffer.from(contentsResponse.data.content, 'base64').toString('utf8')
|
||||
const contentsJSON = JSON.parse(contents)
|
||||
let dependencyCount = 0
|
||||
|
||||
dependencyCount += checkDependencies(contents, contentsJSON.dependencies, file, check)
|
||||
dependencyCount += checkDependencies(contents, contentsJSON.devDependencies, file, check)
|
||||
dependencyCount += checkDependencies(contents, contentsJSON.optionalDependencies, file, check)
|
||||
|
||||
return dependencyCount
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
module.exports = {
|
||||
roots: ['<rootDir>/src/', '<rootDir>/test/'],
|
||||
transform: {
|
||||
'^.+\\.tsx?$': 'ts-jest'
|
||||
},
|
||||
testRegex: '(/__tests__/.*|\\.(test|spec))\\.[tj]sx?$',
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node']
|
||||
}
|
|
@ -1,105 +0,0 @@
|
|||
module.exports = checkDependencies
|
||||
|
||||
function checkDependencies(contents, dependencies, file, check) {
|
||||
if (!dependencies) {
|
||||
return 0
|
||||
}
|
||||
|
||||
const urlRegex = /^(http:\/\/|https:\/\/|git\+http:\/\/|git\+https:\/\/|ssh:\/\/|git\+ssh:\/\/|github:)([a-zA-Z0-9_\-./]+)(#(.*))?$/gm
|
||||
const requiredProtocol = 'git+https://'
|
||||
let dependencyCount = 0
|
||||
|
||||
for (const dependency in dependencies) {
|
||||
if (dependencies.hasOwnProperty(dependency)) {
|
||||
++dependencyCount
|
||||
|
||||
const url = dependencies[dependency]
|
||||
const match = urlRegex.exec(url)
|
||||
|
||||
if (!match) {
|
||||
continue
|
||||
}
|
||||
const protocol = match[1]
|
||||
const address = match[2]
|
||||
const tag = match.length > 4 ? match[4] : null
|
||||
const { line } = findLineColumn(contents, contents.indexOf(url))
|
||||
const optimalAddress = address.endsWith('.git') ? address : address.concat('.git')
|
||||
const optimalTag = isTag(tag) ? tag : '#<release-tag>'
|
||||
const suggestedUrl = `${requiredProtocol}${optimalAddress}${optimalTag}`
|
||||
|
||||
const annotationSource = {
|
||||
check: check,
|
||||
dependency: dependency,
|
||||
file: file,
|
||||
line: line
|
||||
}
|
||||
if (protocol !== requiredProtocol) {
|
||||
createAnnotation(annotationSource, suggestedUrl, {
|
||||
annotation_level: 'warning',
|
||||
title: `Found protocol ${protocol} being used in dependency`,
|
||||
message: `Protocol should be ${requiredProtocol}.`
|
||||
})
|
||||
}
|
||||
if (protocol !== 'github:' && !address.endsWith('.git')) {
|
||||
createAnnotation(annotationSource, suggestedUrl, {
|
||||
annotation_level: 'warning',
|
||||
title: 'Address should end with .git for consistency.',
|
||||
message: 'Android builds have been known to fail when dependency addresses don\'t end with .git.'
|
||||
})
|
||||
}
|
||||
if (!tag) {
|
||||
createAnnotation(annotationSource, suggestedUrl, {
|
||||
annotation_level: 'failure',
|
||||
title: 'Dependency is not locked with a tag/release.',
|
||||
message: `${url} is not a deterministic dependency locator.\r\nIf the branch advances, it will be impossible to rebuild the same output in the future.`
|
||||
})
|
||||
} else if (!isTag(tag)) {
|
||||
createAnnotation(annotationSource, suggestedUrl, {
|
||||
annotation_level: 'failure',
|
||||
title: 'Dependency is locked with a branch, instead of a tag/release.',
|
||||
message: `${url} is not a deterministic dependency locator.\r\nIf the branch advances, it will be impossible to rebuild the same output in the future.`
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return dependencyCount
|
||||
}
|
||||
|
||||
function findLineColumn (contents, index) {
|
||||
const lines = contents.split('\n')
|
||||
const line = contents.substr(0, index).split('\n').length
|
||||
|
||||
const startOfLineIndex = (() => {
|
||||
const x = lines.slice(0)
|
||||
x.splice(line - 1)
|
||||
return x.join('\n').length + (x.length > 0)
|
||||
})()
|
||||
|
||||
const col = index - startOfLineIndex
|
||||
|
||||
return { line, col }
|
||||
}
|
||||
|
||||
function createAnnotation(annotationSource, suggestedUrl, annotation) {
|
||||
const { check, dependency, file, line } = annotationSource
|
||||
|
||||
if (!check.output.annotations) {
|
||||
check.output.annotations = []
|
||||
}
|
||||
annotation.message = annotation.message.concat(`\r\n\r\nSuggested URL: ${suggestedUrl}`)
|
||||
check.output.annotations.push({
|
||||
...annotation,
|
||||
dependency: dependency,
|
||||
path: file.filename,
|
||||
start_line: line,
|
||||
end_line: line,
|
||||
raw_details: `{suggestedUrl: ${suggestedUrl}}`
|
||||
})
|
||||
}
|
||||
|
||||
function isTag(tag) {
|
||||
// TODO: We need to check the actual repo to see if it is a branch or a tag
|
||||
return tag && tag !== 'master' && tag !== 'develop'
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"ignore": ["**/*.test.ts", "**/*.spec.ts", ".git", "node_modules"],
|
||||
"watch": ["src"],
|
||||
"exec": "yarn _start-dev",
|
||||
"ext": "ts"
|
||||
}
|
90
package.json
90
package.json
|
@ -1,52 +1,68 @@
|
|||
{
|
||||
"name": "packages-check-bot",
|
||||
"version": "1.0.0",
|
||||
"description": "checks changes to packages.json to ensure that URL schemes match intended pattern and that forks are referenced with a tag, instead of a branch.",
|
||||
"author": "Pedro Pombeiro <pombeirp@users.noreply.github.com> (https://status.im)",
|
||||
"license": "ISC",
|
||||
"repository": "https://github.com/status-im/packages-check-bot.git",
|
||||
"homepage": "https://github.com/status-im/packages-check-bot",
|
||||
"bugs": "https://github.com/status-im/packages-check-bot/issues",
|
||||
"dependencies": {
|
||||
"@types/humanize-plus": "^1.8.0",
|
||||
"@types/nock": "^9.3.0",
|
||||
"humanize-plus": "^1.8.2",
|
||||
"nock": "^10.0.0",
|
||||
"probot": "^7.2.0"
|
||||
},
|
||||
"description": "checks changes to packages.json to ensure that URL schemes match intended pattern and that forks are referenced with a tag, instead of a branch.",
|
||||
"devDependencies": {
|
||||
"@types/jest": "^23.1.5",
|
||||
"@types/node": "^10.12.18",
|
||||
"eslint-plugin-typescript": "^0.12.0",
|
||||
"jest": "^23.4.0",
|
||||
"nodemon": "^1.18.9",
|
||||
"smee-client": "^1.0.2",
|
||||
"standard": "^10.0.3",
|
||||
"ts-jest": "^23.0.0",
|
||||
"tslint": "^5.12.1",
|
||||
"typescript": "3.0.1",
|
||||
"typescript-eslint-parser": "^18.0.0",
|
||||
"typescript-tslint-plugin": "^0.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.3.0"
|
||||
},
|
||||
"homepage": "https://github.com/status-im/packages-check-bot",
|
||||
"jest": {
|
||||
"testEnvironment": "node"
|
||||
},
|
||||
"keywords": [
|
||||
"probot",
|
||||
"github",
|
||||
"probot-app"
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "nodemon",
|
||||
"start": "probot run ./index.js",
|
||||
"lint": "standard --fix",
|
||||
"test": "jest && standard",
|
||||
"test:watch": "jest --watch --notify --notifyMode=change --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"humanize-plus": "^1.8.2",
|
||||
"probot": "^7.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^5.12.0",
|
||||
"jest": "^22.4.3",
|
||||
"nock": "^10.0.0",
|
||||
"nodemon": "^1.17.2",
|
||||
"smee-client": "^1.0.2",
|
||||
"standard": "^10.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.3.0"
|
||||
},
|
||||
"standard": {
|
||||
"env": [
|
||||
"jest"
|
||||
]
|
||||
},
|
||||
"license": "ISC",
|
||||
"name": "packages-check-bot",
|
||||
"nodemonConfig": {
|
||||
"exec": "npm start",
|
||||
"watch": [
|
||||
".env",
|
||||
"."
|
||||
"./lib"
|
||||
]
|
||||
},
|
||||
"jest": {
|
||||
"testEnvironment": "node"
|
||||
}
|
||||
"repository": "https://github.com/status-im/packages-check-bot.git",
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"postinstall": "yarn build",
|
||||
"dev": "./node_modules/nodemon/bin/nodemon.js",
|
||||
"_start-dev": "./scripts/predebug.sh; yarn build && node --inspect ./node_modules/probot/bin/probot-run.js ./lib/index.js",
|
||||
"lint": "standard **/*.ts --fix",
|
||||
"start": "probot run ./lib/index.js",
|
||||
"test": "jest && standard **/*.ts",
|
||||
"test:watch": "jest --watch --notify --notifyMode=change --coverage"
|
||||
},
|
||||
"standard": {
|
||||
"env": [
|
||||
"jest"
|
||||
],
|
||||
"parser": "typescript-eslint-parser",
|
||||
"plugins": [
|
||||
"typescript"
|
||||
]
|
||||
},
|
||||
"version": "1.0.0"
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
#!/bin/bash
|
||||
|
||||
APP_PORT=3000
|
||||
APP_PID="$(lsof -i :${APP_PORT} | awk 'NR!=1 {print $2}' | sort -u | tr '\r\n' ' ')"
|
||||
if [ ! -z "$APP_PID" ]; then
|
||||
kill $APP_PID
|
||||
fi
|
|
@ -0,0 +1,161 @@
|
|||
export class AnnotationResult {
|
||||
title?: string
|
||||
message: string
|
||||
annotationLevel: 'notice' | 'warning' | 'failure'
|
||||
dependency: Dependency
|
||||
path: string
|
||||
startLine: number
|
||||
endLine: number
|
||||
rawDetails?: string
|
||||
|
||||
constructor (title: string,
|
||||
message: string,
|
||||
annotationLevel: "notice" | "warning" | "failure",
|
||||
dependency: Dependency,
|
||||
path: string,
|
||||
startLine: number,
|
||||
endLine: number,
|
||||
rawDetails: string) {
|
||||
this.title = title
|
||||
this.message = message
|
||||
this.annotationLevel = annotationLevel
|
||||
this.dependency = dependency
|
||||
this.path = path
|
||||
this.startLine = startLine
|
||||
this.endLine = endLine
|
||||
this.rawDetails = rawDetails
|
||||
}
|
||||
}
|
||||
|
||||
export class AnalysisResult {
|
||||
checkedDependencyCount!: number
|
||||
annotations!: AnnotationResult[]
|
||||
|
||||
constructor () {
|
||||
this.checkedDependencyCount = 0
|
||||
this.annotations = []
|
||||
}
|
||||
}
|
||||
|
||||
export class Dependency {
|
||||
name!: string
|
||||
url!: string
|
||||
|
||||
constructor (name: string, url: string) {
|
||||
this.name = name
|
||||
this.url = url
|
||||
}
|
||||
}
|
||||
|
||||
type annotationSource = {
|
||||
dependency: Dependency,
|
||||
filename: string,
|
||||
line: number
|
||||
}
|
||||
|
||||
export function getDependenciesFromJSON (dependenciesJSON: any): Dependency[] {
|
||||
const dependencies: Dependency[] = []
|
||||
|
||||
for (const name in dependenciesJSON) {
|
||||
if (dependenciesJSON.hasOwnProperty(name)) {
|
||||
dependencies.push(new Dependency(name, dependenciesJSON[name]))
|
||||
}
|
||||
}
|
||||
|
||||
return dependencies
|
||||
}
|
||||
|
||||
export function checkDependencies (contents: string, dependencies: Dependency[], filename: string, result: AnalysisResult) {
|
||||
if (!dependencies || dependencies.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const urlRegex = /^(http:\/\/|https:\/\/|git\+http:\/\/|git\+https:\/\/|ssh:\/\/|git\+ssh:\/\/|github:)([a-zA-Z0-9_\-./]+)(#(.*))?$/gm
|
||||
const requiredProtocol = 'git+https://'
|
||||
|
||||
result.checkedDependencyCount += dependencies.length
|
||||
|
||||
for (const dependency of dependencies) {
|
||||
const url = dependency.url
|
||||
const match = urlRegex.exec(url)
|
||||
|
||||
if (!match) {
|
||||
continue
|
||||
}
|
||||
const protocol = match[1]
|
||||
const address = match[2]
|
||||
const tag = match.length > 4 ? match[4] : ''
|
||||
const { line } = findLineColumn(contents, contents.indexOf(url))
|
||||
const optimalAddress = address.endsWith('.git') ? address : address.concat('.git')
|
||||
const optimalTag = isTag(tag) ? tag : '#<release-tag>'
|
||||
const suggestedUrl = `${requiredProtocol}${optimalAddress}${optimalTag}`
|
||||
|
||||
const annotationSource: annotationSource = {
|
||||
dependency: dependency,
|
||||
filename: filename,
|
||||
line: line
|
||||
}
|
||||
if (protocol !== requiredProtocol) {
|
||||
result.annotations.push(createAnnotation(annotationSource, suggestedUrl, 'warning',
|
||||
`Found protocol ${protocol} being used in dependency`,
|
||||
`Protocol should be ${requiredProtocol}.`))
|
||||
}
|
||||
if (protocol !== 'github:' && !address.endsWith('.git')) {
|
||||
result.annotations.push(createAnnotation(annotationSource, suggestedUrl, 'warning',
|
||||
'Address should end with .git for consistency.',
|
||||
'Android builds have been known to fail when dependency addresses don\'t end with .git.'
|
||||
))
|
||||
}
|
||||
if (!tag) {
|
||||
result.annotations.push(createAnnotation(annotationSource, suggestedUrl, 'failure',
|
||||
'Dependency is not locked with a tag/release.',
|
||||
`${url} is not a deterministic dependency locator.\r\nIf the branch advances, it will be impossible to rebuild the same output in the future.`
|
||||
))
|
||||
} else if (!isTag(tag)) {
|
||||
result.annotations.push(createAnnotation(annotationSource, suggestedUrl, 'failure',
|
||||
'Dependency is locked with a branch, instead of a tag/release.',
|
||||
`${url} is not a deterministic dependency locator.\r\nIf the branch advances, it will be impossible to rebuild the same output in the future.`
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function findLineColumn (contents: string, index: number) {
|
||||
const lines = contents.split('\n')
|
||||
const line = contents.substr(0, index).split('\n').length
|
||||
|
||||
const startOfLineIndex = (() => {
|
||||
const x = lines.slice(0)
|
||||
x.splice(line - 1)
|
||||
return x.join('\n').length + (x.length > 0 ? 1 : 0)
|
||||
})()
|
||||
|
||||
const col = index - startOfLineIndex
|
||||
|
||||
return { line, col }
|
||||
}
|
||||
|
||||
function createAnnotation (
|
||||
annotationSource: annotationSource,
|
||||
suggestedUrl: string,
|
||||
annotationLevel: "notice" | "warning" | "failure",
|
||||
title: string,
|
||||
message: string): AnnotationResult {
|
||||
const { dependency, filename, line } = annotationSource
|
||||
|
||||
return new AnnotationResult(
|
||||
title,
|
||||
message.concat(`\r\n\r\nSuggested URL: ${suggestedUrl}`),
|
||||
annotationLevel,
|
||||
dependency,
|
||||
filename,
|
||||
line,
|
||||
line,
|
||||
`{suggestedUrl: ${suggestedUrl}}`
|
||||
)
|
||||
}
|
||||
|
||||
function isTag (tag: string): boolean {
|
||||
// TODO: We need to check the actual repo to see if it is a branch or a tag
|
||||
return tag !== '' && tag !== 'master' && tag !== 'develop'
|
||||
}
|
|
@ -0,0 +1,215 @@
|
|||
// Checks API example
|
||||
// See: https://developer.github.com/v3/checks/ to learn more
|
||||
import { Application, Context } from 'probot' // eslint-disable-line no-unused-vars
|
||||
import Octokit from '@octokit/rest'
|
||||
import Humanize from 'humanize-plus'
|
||||
import { checkDependencies, getDependenciesFromJSON, AnalysisResult, AnnotationResult } from './dependency-check'
|
||||
|
||||
const pendingChecks: any = []
|
||||
|
||||
export = (app: Application) => {
|
||||
app.on(['check_suite.requested'], async (context) => { await checkSuiteAsync(context, context.payload.check_suite) })
|
||||
app.on(['check_run.rerequested'], async (context) => {
|
||||
const { check_suite } = context.payload.check_run
|
||||
await checkSuiteAsync(context, check_suite)
|
||||
})
|
||||
|
||||
async function checkSuiteAsync (context: Context, checkSuite: Octokit.ChecksCreateSuiteResponse): Promise<Octokit.Response<Octokit.ChecksCreateResponse>> {
|
||||
const { head_branch: headBranch, head_sha: headSHA } = checkSuite
|
||||
|
||||
// Probot API note: context.repo() => {username: 'hiimbex', repo: 'testing-things'}
|
||||
const check: Octokit.ChecksCreateParams = context.repo({
|
||||
name: 'packages-check-bot',
|
||||
head_branch: headBranch,
|
||||
head_sha: headSHA,
|
||||
started_at: (new Date()).toISOString()
|
||||
})
|
||||
|
||||
try {
|
||||
context.log.info(`checking ${context.payload.repository.full_name}#${headBranch} (${headSHA}) (check_suite.id #${checkSuite.id})
|
||||
Pull requests: ${Humanize.oxford(checkSuite.pull_requests.map(pr => pr.url), 5)}`)
|
||||
|
||||
check.status = 'in_progress'
|
||||
check.output = {
|
||||
title: 'package.json check',
|
||||
summary: 'Checking any new/updated dependencies...'
|
||||
}
|
||||
|
||||
const alreadyQueued = pendingChecks[headSHA]
|
||||
|
||||
if (context.payload.action === 'rerequested') {
|
||||
if (!alreadyQueued) {
|
||||
pendingChecks[headSHA] = { ...check, check_run_id: context.payload.check_run.id }
|
||||
queueCheckAsync(context, checkSuite)
|
||||
}
|
||||
|
||||
const createResponse = await context.github.checks.create(check)
|
||||
context.log.debug(`create checks status: ${createResponse.status}`)
|
||||
return createResponse
|
||||
} else {
|
||||
if (!alreadyQueued) {
|
||||
pendingChecks[headSHA] = { ...check }
|
||||
}
|
||||
const createResponse = await context.github.checks.create(check)
|
||||
context.log.debug(`create checks status: ${createResponse.status}`)
|
||||
|
||||
if (!alreadyQueued) {
|
||||
pendingChecks[headSHA] = { ...check, check_run_id: createResponse.data.id }
|
||||
queueCheckAsync(context, checkSuite)
|
||||
}
|
||||
return createResponse
|
||||
}
|
||||
} catch (e) {
|
||||
context.log.error(e)
|
||||
|
||||
// Report error back to GitHub
|
||||
check.status = 'completed'
|
||||
check.conclusion = 'cancelled'
|
||||
check.completed_at = (new Date()).toISOString()
|
||||
check.output = {
|
||||
title: 'package.json check',
|
||||
summary: e.message
|
||||
}
|
||||
|
||||
return context.github.checks.create(check)
|
||||
}
|
||||
}
|
||||
|
||||
// For more information on building apps:
|
||||
// https://probot.github.io/docs/
|
||||
|
||||
// To get your app running against GitHub, see:
|
||||
// https://probot.github.io/docs/development/
|
||||
}
|
||||
|
||||
const timeout = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
|
||||
|
||||
async function queueCheckAsync (context: Context, checkSuite: Octokit.ChecksCreateSuiteResponse) {
|
||||
try {
|
||||
const { before, head_sha } = checkSuite
|
||||
|
||||
const compareResponse = await context.github.repos.compareCommits(context.repo({
|
||||
base: before,
|
||||
head: head_sha
|
||||
}))
|
||||
context.log.debug(`compare commits status: ${compareResponse.status}, ${compareResponse.data.files.length} file(s)`)
|
||||
|
||||
let check = pendingChecks[head_sha]
|
||||
let checkedDepCount = 0
|
||||
let packageJsonFilenames = []
|
||||
const packageFilenameRegex = /^package\.json(.orig)?$/g
|
||||
|
||||
check.output.annotations = undefined
|
||||
let analysisResult = new AnalysisResult()
|
||||
|
||||
for (const file of compareResponse.data.files) {
|
||||
switch (file.status) {
|
||||
case 'added':
|
||||
case 'modified':
|
||||
if (packageFilenameRegex.test(file.filename)) {
|
||||
packageJsonFilenames.push(file.filename)
|
||||
checkedDepCount += await checkPackageFileAsync(analysisResult, context, file.filename, head_sha)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
check.status = 'completed'
|
||||
check.completed_at = (new Date()).toISOString()
|
||||
|
||||
if (analysisResult.checkedDependencyCount === 0) {
|
||||
check.conclusion = 'neutral'
|
||||
check.output.summary = 'No changes to dependencies'
|
||||
} else if (analysisResult.annotations.length === 0) {
|
||||
check.conclusion = 'success'
|
||||
check.output.summary = 'All dependencies are good!'
|
||||
} else {
|
||||
check.conclusion = 'failure'
|
||||
|
||||
const warnings = analysisResult.annotations.filter(a => a.annotationLevel === 'warning').length
|
||||
const failures = analysisResult.annotations.filter(a => a.annotationLevel === 'failure').length
|
||||
const uniqueProblemDependencies = [...new Set(analysisResult.annotations.map(a => a.dependency))]
|
||||
check.output.summary = `Checked ${checkedDepCount} ${Humanize.pluralize(checkedDepCount, 'dependency', 'dependencies')} in ${Humanize.oxford(packageJsonFilenames.map(f => `\`${f}\``), 3)}.
|
||||
${Humanize.boundedNumber(failures, 10)} ${Humanize.pluralize(failures, 'failure')}, ${Humanize.boundedNumber(warnings, 10)} ${Humanize.pluralize(warnings, 'warning')} in ${Humanize.oxford(uniqueProblemDependencies.map(f => `\`${f}\``), 3)} need your attention!`
|
||||
}
|
||||
|
||||
for (let annotationIndex = 0; annotationIndex < analysisResult.annotations.length; annotationIndex += 50) {
|
||||
const annotationsSlice = analysisResult.annotations.length > 50 ? analysisResult.annotations.slice(annotationIndex, annotationIndex + 50) : analysisResult.annotations
|
||||
|
||||
convertAnnotationResults(check, annotationsSlice)
|
||||
|
||||
for (let attempts = 3; attempts >= 0;) {
|
||||
try {
|
||||
const updateResponse = await context.github.checks.update({
|
||||
owner: check.owner,
|
||||
repo: check.repo,
|
||||
check_run_id: check.check_run_id,
|
||||
name: check.name,
|
||||
//details_url: check.details_url,
|
||||
external_id: check.external_id,
|
||||
started_at: check.started_at,
|
||||
status: check.status,
|
||||
conclusion: check.conclusion,
|
||||
completed_at: check.completed_at,
|
||||
output: check.output
|
||||
})
|
||||
context.log.debug(`update checks status: ${updateResponse.status}`)
|
||||
break
|
||||
} catch (error) {
|
||||
if (--attempts <= 0) {
|
||||
throw error
|
||||
}
|
||||
context.log.warn(`error while updating check run, will try again in 30 seconds: ${error.message}`)
|
||||
await timeout(30000)
|
||||
}
|
||||
}
|
||||
}
|
||||
delete pendingChecks[head_sha]
|
||||
} catch (error) {
|
||||
context.log.error(error)
|
||||
// This function isn't usually awaited for, so there's no point in rethrowing
|
||||
}
|
||||
}
|
||||
|
||||
async function checkPackageFileAsync (analysisResult: AnalysisResult, context: Context, filename: string, headSHA: string) {
|
||||
const contentsResponse: any = await context.github.repos.getContents(context.repo({
|
||||
path: filename,
|
||||
ref: headSHA
|
||||
}))
|
||||
context.log.debug(`get contents response: ${contentsResponse.status}`)
|
||||
if (contentsResponse.status >= 300) {
|
||||
throw new Error(`HTTP error ${contentsResponse.status} (${contentsResponse.statusText}) fetching ${filename}`)
|
||||
}
|
||||
|
||||
const contents = Buffer.from(contentsResponse.data.content, 'base64').toString('utf8')
|
||||
const contentsJSON = JSON.parse(contents)
|
||||
|
||||
checkDependencies(contents, getDependenciesFromJSON(contentsJSON.dependencies), filename, analysisResult)
|
||||
checkDependencies(contents, getDependenciesFromJSON(contentsJSON.devDependencies), filename, analysisResult)
|
||||
checkDependencies(contents, getDependenciesFromJSON(contentsJSON.optionalDependencies), filename, analysisResult)
|
||||
|
||||
return analysisResult.checkedDependencyCount
|
||||
}
|
||||
|
||||
function convertAnnotationResults (check: Octokit.ChecksUpdateParams, annotationsSlice: AnnotationResult[]) {
|
||||
if (!check.output) {
|
||||
const output: Octokit.ChecksUpdateParamsOutput = {
|
||||
summary: ''
|
||||
}
|
||||
check.output = output
|
||||
}
|
||||
|
||||
check.output.annotations = []
|
||||
for (const annotationResult of annotationsSlice) {
|
||||
const annotation: Octokit.ChecksUpdateParamsOutputAnnotations = {
|
||||
path: annotationResult.path,
|
||||
start_line: annotationResult.startLine,
|
||||
end_line: annotationResult.endLine,
|
||||
annotation_level: annotationResult.annotationLevel,
|
||||
message: annotationResult.message,
|
||||
title: annotationResult.title,
|
||||
raw_details: annotationResult.rawDetails
|
||||
}
|
||||
check.output.annotations.push(annotation)
|
||||
}
|
||||
}
|
|
@ -1,15 +1,18 @@
|
|||
const nock = require('nock')
|
||||
// You can import your modules
|
||||
// import index from '../src/index'
|
||||
|
||||
import nock from 'nock'
|
||||
// Requiring our app implementation
|
||||
const myProbotApp = require('..')
|
||||
const { Probot } = require('probot')
|
||||
import myProbotApp from '../src'
|
||||
import { Probot } from 'probot'
|
||||
// Requiring our fixtures
|
||||
const checkSuitePayload = require('./fixtures/check_suite.requested')
|
||||
const checkRunSuccess = require('./fixtures/check_run.created')
|
||||
import checkSuitePayload from './fixtures/check_suite.requested.json'
|
||||
import checkRunSuccess from './fixtures/check_run.created.json'
|
||||
|
||||
nock.disableNetConnect()
|
||||
|
||||
describe('My Probot app', () => {
|
||||
let probot
|
||||
let probot: any
|
||||
|
||||
beforeEach(() => {
|
||||
probot = new Probot({})
|
||||
|
@ -41,5 +44,8 @@ describe('My Probot app', () => {
|
|||
// For more information about testing with Jest see:
|
||||
// https://facebook.github.io/jest/
|
||||
|
||||
// For more information about using TypeScript in your tests, Jest recommends:
|
||||
// https://github.com/kulshekhar/ts-jest
|
||||
|
||||
// For more information about testing with Nock see:
|
||||
// https://github.com/nock/nock
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"allowJs": false,
|
||||
"lib": ["es2015", "es2017", "es2018"],
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"target": "es5",
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUnusedLocals": false,
|
||||
"pretty": true,
|
||||
"strict": true,
|
||||
"sourceMap": true,
|
||||
"outDir": "./lib",
|
||||
"skipLibCheck": true,
|
||||
"noImplicitAny": true,
|
||||
"esModuleInterop": true,
|
||||
"declaration": true,
|
||||
"resolveJsonModule": true,
|
||||
"downlevelIteration": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "typescript-tslint-plugin"
|
||||
}
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"compileOnSave": false
|
||||
}
|
Loading…
Reference in New Issue