Add script to trigger automated tests build when PR is moved to IN TEST column. Closes #15
This commit is contained in:
parent
29ac60f3b3
commit
9ff69661a1
|
@ -17,3 +17,5 @@ SLACK_USER_TOKEN=
|
|||
# Bot configuration (optional)
|
||||
#
|
||||
# DRY_RUN_BOUNTY_APPROVAL=true
|
||||
|
||||
JENKINS_URL=https://<user>:<password>@jenkins.example.com
|
|
@ -12,6 +12,10 @@ bounty-project-board:
|
|||
welcome-bot:
|
||||
message: 'Thanks for making your first PR here!'
|
||||
|
||||
automated-tests:
|
||||
repo-full-name: 'status-im/status-react'
|
||||
job-full-name: 'end-to-end-tests/status-app-end-to-end-tests'
|
||||
|
||||
slack:
|
||||
notification:
|
||||
room: 'status-probot'
|
||||
|
|
|
@ -11,9 +11,7 @@ available, etc!
|
|||
|
||||
## What does the bot do?
|
||||
|
||||
Right now the bot has two sets of capabilities:
|
||||
|
||||
- Doing background management in GitHub:
|
||||
- Background management in GitHub:
|
||||
- Assign new PRs to the `Pipeline for QA` project board (`REVIEW` column).
|
||||
- Move existing PRs to the correct `Pipeline for QA` project board column (`REVIEW`/`IN TEST`) depending on whether or not the required conditions are met (is mergeable, at least two reviewers have approved and there is no request for changes).
|
||||
- Assign issues that are labeled `bounty-awaiting-approval` to the `Status SOB Swarm` project board (`bounty-awaiting-approval` column).
|
||||
|
@ -22,6 +20,7 @@ Right now the bot has two sets of capabilities:
|
|||
- Unfurls links on Issues and Pull Request discussions.
|
||||
- Disallows merging of PRs containing WIP in the title.
|
||||
- Mention repo collaborators on Slack when a GHI is assigned the `bounty-awaiting-approval` label.
|
||||
- When a PR is moved to the IN TEST column and the build has passed successfully, then the bot will kick a test automation build in Jenkins (retrying periodically if the PR build is still running).
|
||||
- New functionality will be added in the future (wishlist is being tracked [here](https://docs.google.com/document/d/19NZEJ453av-owAEBXcIPjavbGKMBFlfVcwsuQ_ORzR4/))
|
||||
|
||||
The project board names, column names, welcome message and other values are stored in the `.github/github-bot.yml` file. It can be overriden for each specific repository by adding a file in the same path on the respective repository (see [probot-config](https://github.com/getsentry/probot-config)).
|
||||
|
@ -74,11 +73,12 @@ See the official [docs for deployment](https://probot.github.io/docs/deployment/
|
|||
- [x] Check the box for **Push** events
|
||||
- Repository projects - **Read & Write**
|
||||
- [x] Check the box for **Project for repository projects** events
|
||||
- [x] Check the box for **Project card for repository projects** events
|
||||
- Organization projects - **Read-only**
|
||||
- [x] Check the box for **Project for organization projects** events
|
||||
- Single File - **Read-only**
|
||||
- Path: `.github/github-bot.yml`
|
||||
1. 🔍 Verify that you have **ticked 8 boxes**.
|
||||
1. 🔍 Verify that you have **ticked 9 boxes**.
|
||||
1. Generate a private key pass and save it.
|
||||
1. Installing the bot service:
|
||||
1. Deploy the bot to the cloud.
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
|
||||
// const getConfig = require('probot-config')
|
||||
const defaultConfig = require('../lib/config')
|
||||
const gitHubHelpers = require('../lib/github-helpers')
|
||||
const createScheduler = require('probot-scheduler')
|
||||
const Slack = require('probot-slack-status')
|
||||
const slackHelper = require('../lib/slack')
|
||||
|
@ -40,13 +41,6 @@ async function getProjectFromName (github, ownerName, repoName, projectBoardName
|
|||
return ghprojectsPayload.data.find(p => p.name === projectBoardName)
|
||||
}
|
||||
|
||||
async function getProjectCardForPullRequest (github, columnId, pullRequestUrl) {
|
||||
const ghcardsPayload = await github.projects.getProjectCards({column_id: columnId})
|
||||
const ghcard = ghcardsPayload.data.find(c => c.content_url === pullRequestUrl)
|
||||
|
||||
return ghcard
|
||||
}
|
||||
|
||||
async function checkOpenPullRequests (robot, context) {
|
||||
const github = context.github
|
||||
const repo = context.payload.repository
|
||||
|
@ -133,76 +127,16 @@ async function checkOpenPullRequests (robot, context) {
|
|||
}
|
||||
}
|
||||
|
||||
async function getPullRequestReviewStates (github, repo, pullRequest) {
|
||||
var finalReviewsMap = new Map()
|
||||
const ghreviews = await github.paginate(
|
||||
github.pullRequests.getReviews({owner: repo.owner.login, repo: repo.name, number: pullRequest.number, per_page: 100}),
|
||||
res => res.data)
|
||||
for (var review of ghreviews) {
|
||||
switch (review.state) {
|
||||
case 'APPROVED':
|
||||
case 'CHANGES_REQUESTED':
|
||||
case 'PENDING':
|
||||
finalReviewsMap.set(review.user.id, review.state)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(finalReviewsMap.values())
|
||||
}
|
||||
|
||||
async function getReviewApprovalState (github, robot, repo, pullRequest) {
|
||||
// Get detailed pull request
|
||||
const fullPullRequestPayload = await github.pullRequests.get({owner: repo.owner.login, repo: repo.name, number: pullRequest.number})
|
||||
pullRequest = fullPullRequestPayload.data
|
||||
if (pullRequest.mergeable !== null && pullRequest.mergeable !== undefined && !pullRequest.mergeable) {
|
||||
robot.log.debug(`pullRequest.mergeable is ${pullRequest.mergeable}, considering as failed`)
|
||||
return 'failed'
|
||||
}
|
||||
|
||||
let state
|
||||
switch (pullRequest.mergeable_state) {
|
||||
case 'clean':
|
||||
state = 'approved'
|
||||
break
|
||||
case 'dirty':
|
||||
state = 'failed'
|
||||
break
|
||||
}
|
||||
robot.log.debug(`pullRequest.mergeable_state is ${pullRequest.mergeable_state}, considering state as ${state}`)
|
||||
|
||||
if (state !== 'approved') {
|
||||
return state
|
||||
}
|
||||
|
||||
const threshold = 2 // Minimum number of approvers
|
||||
|
||||
var finalReviews = await getPullRequestReviewStates(github, repo, pullRequest)
|
||||
robot.log.debug(finalReviews)
|
||||
|
||||
const approvedReviews = finalReviews.filter(reviewState => reviewState === 'APPROVED')
|
||||
if (approvedReviews.length >= threshold) {
|
||||
const reviewsWithChangesRequested = finalReviews.filter(reviewState => reviewState === 'CHANGES_REQUESTED')
|
||||
if (reviewsWithChangesRequested.length === 0) {
|
||||
return 'approved'
|
||||
}
|
||||
|
||||
return 'changes_requested'
|
||||
}
|
||||
|
||||
return 'awaiting_reviewers'
|
||||
}
|
||||
|
||||
async function assignPullRequestToCorrectColumn (github, robot, repo, pullRequest, contributorColumn, reviewColumn, testColumn, room) {
|
||||
const ownerName = repo.owner.login
|
||||
const repoOwner = repo.owner.login
|
||||
const repoName = repo.name
|
||||
const prNumber = pullRequest.number
|
||||
|
||||
let state = null
|
||||
try {
|
||||
state = await getReviewApprovalState(github, robot, repo, pullRequest)
|
||||
state = await gitHubHelpers.getReviewApprovalState(github, robot, repoOwner, repoName, prNumber)
|
||||
} catch (err) {
|
||||
robot.log.error(`Couldn't calculate the PR approval state: ${err}`, ownerName, repoName, prNumber)
|
||||
robot.log.error(`Couldn't calculate the PR approval state: ${err}`, repoOwner, repoName, prNumber)
|
||||
}
|
||||
|
||||
let srcColumns, dstColumn
|
||||
|
@ -227,14 +161,14 @@ async function assignPullRequestToCorrectColumn (github, robot, repo, pullReques
|
|||
return
|
||||
}
|
||||
|
||||
robot.log.debug(`assignPullRequestToTest - Handling Pull Request #${prNumber} on repo ${ownerName}/${repoName}. PR should be in ${dstColumn.name} column`)
|
||||
robot.log.debug(`assignPullRequestToTest - Handling Pull Request #${prNumber} on repo ${repoOwner}/${repoName}. PR should be in ${dstColumn.name} column`)
|
||||
|
||||
// Look for PR card in source column(s)
|
||||
let existingGHCard = null
|
||||
let srcColumn = null
|
||||
for (const c of srcColumns) {
|
||||
try {
|
||||
existingGHCard = await getProjectCardForPullRequest(github, c.id, pullRequest.issue_url)
|
||||
existingGHCard = await gitHubHelpers.getProjectCardForIssue(github, c.id, pullRequest.issue_url)
|
||||
if (existingGHCard) {
|
||||
srcColumn = c
|
||||
break
|
||||
|
@ -274,7 +208,7 @@ async function assignPullRequestToCorrectColumn (github, robot, repo, pullReques
|
|||
|
||||
// Look for PR card in destination column
|
||||
try {
|
||||
const existingGHCard = await getProjectCardForPullRequest(github, dstColumn.id, pullRequest.issue_url)
|
||||
const existingGHCard = await gitHubHelpers.getProjectCardForIssue(github, dstColumn.id, pullRequest.issue_url)
|
||||
if (existingGHCard) {
|
||||
robot.log.trace(`Found card in target column, ignoring`, existingGHCard.id, dstColumn.id)
|
||||
return
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
|
||||
// const getConfig = require('probot-config')
|
||||
const slackHelper = require('../lib/slack')
|
||||
const gitHubHelpers = require('../lib/github-helpers')
|
||||
const defaultConfig = require('../lib/config')
|
||||
const Slack = require('probot-slack-status')
|
||||
|
||||
|
@ -106,7 +107,6 @@ async function assignIssueToBountyAwaitingForApproval (context, robot, assign) {
|
|||
return
|
||||
}
|
||||
|
||||
let ghcardPayload = null
|
||||
if (process.env.DRY_RUN) {
|
||||
if (assign) {
|
||||
robot.log.info(`Would have created card for issue`, column.id, payload.issue.id)
|
||||
|
@ -117,7 +117,7 @@ async function assignIssueToBountyAwaitingForApproval (context, robot, assign) {
|
|||
if (assign) {
|
||||
try {
|
||||
// Create project card for the issue in the bounty-awaiting-approval column
|
||||
ghcardPayload = await github.projects.createProjectCard({
|
||||
const ghcardPayload = await github.projects.createProjectCard({
|
||||
column_id: column.id,
|
||||
content_type: 'Issue',
|
||||
content_id: payload.issue.id
|
||||
|
@ -130,7 +130,7 @@ async function assignIssueToBountyAwaitingForApproval (context, robot, assign) {
|
|||
}
|
||||
} else {
|
||||
try {
|
||||
const ghcard = await getProjectCardForIssue(github, column.id, payload.issue.url)
|
||||
const ghcard = await gitHubHelpers.getProjectCardForIssue(github, column.id, payload.issue.url)
|
||||
if (ghcard) {
|
||||
await github.projects.deleteProjectCard({id: ghcard.id})
|
||||
robot.log(`Deleted card: ${ghcard.url}`, ghcard.id)
|
||||
|
@ -150,10 +150,3 @@ async function assignIssueToBountyAwaitingForApproval (context, robot, assign) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function getProjectCardForIssue (github, columnId, issueUrl) {
|
||||
const ghcardsPayload = await github.projects.getProjectCards({column_id: columnId})
|
||||
const ghcard = ghcardsPayload.data.find(c => c.content_url === issueUrl)
|
||||
|
||||
return ghcard
|
||||
}
|
||||
|
|
|
@ -0,0 +1,158 @@
|
|||
// Description:
|
||||
// Script that listens for PRs moving into the 'TO TEST' column
|
||||
// and triggers a Jenkins build.
|
||||
//
|
||||
// Dependencies:
|
||||
// github: "^13.1.0"
|
||||
// jenkins: "^0.20.1"
|
||||
// probot-config: "^0.1.0"
|
||||
//
|
||||
// Author:
|
||||
// PombeirP
|
||||
|
||||
const defaultConfig = require('../lib/config')
|
||||
const gitHubHelpers = require('../lib/github-helpers')
|
||||
const jenkins = require('jenkins')({ baseUrl: process.env.JENKINS_URL, crumbIssuer: true, promisify: true })
|
||||
const HashMap = require('hashmap')
|
||||
|
||||
const pendingPullRequests = new HashMap()
|
||||
|
||||
module.exports = (robot) => {
|
||||
const config = defaultConfig(robot, '.github/github-bot.yml')
|
||||
const projectBoardConfig = config['project-board']
|
||||
const automatedTestsConfig = config['automated-tests']
|
||||
|
||||
if (!process.env.JENKINS_URL) {
|
||||
robot.log.info('trigger-automation-test-build - Jenkins is not configured, not loading script')
|
||||
return
|
||||
}
|
||||
|
||||
if (projectBoardConfig && automatedTestsConfig) {
|
||||
setInterval(checkPendingPullRequests, 5 * 1000 * 60, robot)
|
||||
registerForRelevantCardEvents(robot, { projectBoardConfig: projectBoardConfig, automatedTestingConfig: automatedTestsConfig })
|
||||
}
|
||||
}
|
||||
|
||||
function registerForRelevantCardEvents (robot, config) {
|
||||
robot.on('project_card.created', context => processChangedProjectCard(robot, context, config))
|
||||
robot.on('project_card.moved', context => processChangedProjectCard(robot, context, config))
|
||||
}
|
||||
|
||||
async function processChangedProjectCard (robot, context, config) {
|
||||
const { github, payload } = context
|
||||
|
||||
if (payload.project_card.note) {
|
||||
robot.log.trace(`trigger-automation-test-build - Card is a note, ignoring`)
|
||||
return
|
||||
}
|
||||
|
||||
const { projectBoardConfig, automatedTestingConfig } = config
|
||||
const projectBoardName = projectBoardConfig['name']
|
||||
const testColumnName = projectBoardConfig['test-column-name']
|
||||
const repo = payload.repository
|
||||
|
||||
if (repo.full_name !== automatedTestingConfig['repo-full-name']) {
|
||||
robot.log.trace(`trigger-automation-test-build - Pull request project doesn't match watched repo, exiting`, repo.full_name, automatedTestingConfig['repo-full-name'])
|
||||
return
|
||||
}
|
||||
|
||||
let inTestColumn
|
||||
try {
|
||||
const columnPayload = await github.projects.getProjectColumn({ id: payload.project_card.column_id })
|
||||
|
||||
if (columnPayload.data.name !== testColumnName) {
|
||||
robot.log.trace(`trigger-automation-test-build - Card column name doesn't match watched column name, exiting`, columnPayload.data.name, testColumnName)
|
||||
return
|
||||
}
|
||||
|
||||
inTestColumn = columnPayload.data
|
||||
} catch (error) {
|
||||
robot.log.warn(`trigger-automation-test-build - Error while fetching project column`, payload.project_card.column_id, error)
|
||||
return
|
||||
}
|
||||
|
||||
const last = (a, index) => {
|
||||
return a[a.length + index]
|
||||
}
|
||||
|
||||
let project
|
||||
try {
|
||||
const projectId = last(inTestColumn.project_url.split('/'), -1)
|
||||
const projectPayload = await github.projects.getProject({ id: projectId })
|
||||
|
||||
project = projectPayload.data
|
||||
if (project.name !== projectBoardName) {
|
||||
robot.log.trace(`trigger-automation-test-build - Card column name doesn't match watched column name, exiting`, project.name, projectBoardName)
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
robot.log.warn(`trigger-automation-test-build - Error while fetching project column`, payload.project_card.column_id, error)
|
||||
return
|
||||
}
|
||||
|
||||
const prNumber = last(payload.project_card.content_url.split('/'), -1)
|
||||
const fullJobName = automatedTestingConfig['job-full-name']
|
||||
|
||||
await processPullRequest(github, robot, repo.owner.login, repo.name, prNumber, fullJobName)
|
||||
}
|
||||
|
||||
async function processPullRequest (github, robot, repoOwner, repoName, prNumber, fullJobName) {
|
||||
// Remove the PR from the pending PR list, if it is there
|
||||
pendingPullRequests.delete(prNumber)
|
||||
|
||||
try {
|
||||
const state = await gitHubHelpers.getReviewApprovalState(github, robot, repoOwner, repoName, prNumber)
|
||||
|
||||
switch (state) {
|
||||
case 'unstable':
|
||||
case 'awaiting_reviewers':
|
||||
case 'changes_requested':
|
||||
pendingPullRequests.set(prNumber, { github: github, repoOwner: repoOwner, repoName: repoName, fullJobName: fullJobName })
|
||||
robot.log.debug(`trigger-automation-test-build - State is '${state}', adding to backlog to check periodically`, prNumber)
|
||||
return
|
||||
case 'failed':
|
||||
robot.log.debug(`trigger-automation-test-build - State is '${state}', exiting`, prNumber)
|
||||
return
|
||||
case 'approved':
|
||||
robot.log.debug(`trigger-automation-test-build - State is '${state}', proceeding`, prNumber)
|
||||
break
|
||||
default:
|
||||
robot.log.warn(`trigger-automation-test-build - State is '${state}', ignoring`, prNumber)
|
||||
return
|
||||
}
|
||||
} catch (err) {
|
||||
robot.log.error(`Couldn't calculate the PR approval state: ${err}`, repoOwner, repoName, prNumber)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const args = { parameters: { pr_id: prNumber, apk: `--apk=${prNumber}.apk` } }
|
||||
|
||||
if (process.env.DRY_RUN) {
|
||||
robot.log(`trigger-automation-test-build - Would start ${fullJobName} job in Jenkins`, prNumber, args)
|
||||
} else {
|
||||
robot.log(`trigger-automation-test-build - Starting ${fullJobName} job in Jenkins`, prNumber, args)
|
||||
const buildId = await jenkins.job.build(fullJobName, args)
|
||||
robot.log(`trigger-automation-test-build - Started job in Jenkins`, prNumber, buildId)
|
||||
}
|
||||
} catch (error) {
|
||||
robot.log.error(`trigger-automation-test-build - Error while triggering Jenkins build. Will retry later`, prNumber, error)
|
||||
|
||||
pendingPullRequests.set(prNumber, { github: github, repoOwner: repoOwner, repoName: repoName, fullJobName: fullJobName })
|
||||
}
|
||||
}
|
||||
|
||||
async function checkPendingPullRequests (robot) {
|
||||
const _pendingPullRequests = pendingPullRequests.clone()
|
||||
|
||||
robot.log.trace(`trigger-automation-test-build - Processing ${_pendingPullRequests.size} pending PRs`)
|
||||
|
||||
for (const kvp of _pendingPullRequests.entries()) {
|
||||
const prNumber = kvp[0]
|
||||
const { github, repoOwner, repoName, fullJobName } = kvp[1]
|
||||
|
||||
await processPullRequest(github, robot, repoOwner, repoName, prNumber, fullJobName)
|
||||
}
|
||||
|
||||
robot.log.trace(`trigger-automation-test-build - Finished processing ${_pendingPullRequests.size} pending PRs`)
|
||||
}
|
1
index.js
1
index.js
|
@ -11,6 +11,7 @@ module.exports = async (robot) => {
|
|||
require('./bot_scripts/assign-approved-pr-to-test')(robot)
|
||||
require('./bot_scripts/assign-to-bounty-awaiting-for-approval')(robot)
|
||||
require('./bot_scripts/greet-new-contributor')(robot)
|
||||
require('./bot_scripts/trigger-automation-test-build')(robot)
|
||||
|
||||
await slackCachePromise
|
||||
robot.log.info('Slack user ID cache populated, loading remainder of scripts')
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
// Description:
|
||||
// GtHub-related helpers
|
||||
//
|
||||
// Dependencies:
|
||||
// github: "^13.1.0"
|
||||
//
|
||||
// Author:
|
||||
// PombeirP
|
||||
|
||||
module.exports.getPullRequestReviewStates = _getPullRequestReviewStates
|
||||
module.exports.getReviewApprovalState = _getReviewApprovalState
|
||||
module.exports.getProjectCardForIssue = _getProjectCardForIssue
|
||||
|
||||
async function _getPullRequestReviewStates (github, repoOwner, repoName, prNumber) {
|
||||
let finalReviewsMap = new Map()
|
||||
const ghreviews = await github.paginate(
|
||||
github.pullRequests.getReviews({owner: repoOwner, repo: repoName, number: prNumber, per_page: 100}),
|
||||
res => res.data)
|
||||
for (var review of ghreviews) {
|
||||
switch (review.state) {
|
||||
case 'APPROVED':
|
||||
case 'CHANGES_REQUESTED':
|
||||
case 'PENDING':
|
||||
finalReviewsMap.set(review.user.id, review.state)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(finalReviewsMap.values())
|
||||
}
|
||||
|
||||
async function _getReviewApprovalState (github, robot, repoOwner, repoName, prNumber) {
|
||||
// Get detailed pull request
|
||||
const pullRequestPayload = await github.pullRequests.get({owner: repoOwner, repo: repoName, number: prNumber})
|
||||
const pullRequest = pullRequestPayload.data
|
||||
if (pullRequest.mergeable !== null && pullRequest.mergeable !== undefined && !pullRequest.mergeable) {
|
||||
robot.log.debug(`pullRequest.mergeable is ${pullRequest.mergeable}, considering as failed`)
|
||||
return 'failed'
|
||||
}
|
||||
|
||||
let state
|
||||
switch (pullRequest.mergeable_state) {
|
||||
case 'clean':
|
||||
state = 'approved'
|
||||
break
|
||||
case 'dirty':
|
||||
state = 'failed'
|
||||
break
|
||||
}
|
||||
robot.log.debug(`pullRequest.mergeable_state is ${pullRequest.mergeable_state}, considering state as ${state}`)
|
||||
|
||||
if (state !== 'approved') {
|
||||
return state
|
||||
}
|
||||
|
||||
const threshold = 2 // Minimum number of approvers
|
||||
|
||||
const finalReviews = await _getPullRequestReviewStates(github, repoOwner, repoName, pullRequest.number)
|
||||
robot.log.debug(finalReviews)
|
||||
|
||||
const approvedReviews = finalReviews.filter(reviewState => reviewState === 'APPROVED')
|
||||
if (approvedReviews.length >= threshold) {
|
||||
const reviewsWithChangesRequested = finalReviews.filter(reviewState => reviewState === 'CHANGES_REQUESTED')
|
||||
if (reviewsWithChangesRequested.length === 0) {
|
||||
return 'approved'
|
||||
}
|
||||
|
||||
return 'changes_requested'
|
||||
}
|
||||
|
||||
return 'awaiting_reviewers'
|
||||
}
|
||||
|
||||
async function _getProjectCardForIssue (github, columnId, issueUrl) {
|
||||
const ghcardsPayload = await github.projects.getProjectCards({column_id: columnId})
|
||||
const ghcard = ghcardsPayload.data.find(c => c.content_url === issueUrl)
|
||||
|
||||
return ghcard
|
||||
}
|
|
@ -2538,6 +2538,11 @@
|
|||
"integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=",
|
||||
"dev": true
|
||||
},
|
||||
"hashmap": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/hashmap/-/hashmap-2.3.0.tgz",
|
||||
"integrity": "sha1-sT+2XafIul49uPwbjFuh0ASdryI="
|
||||
},
|
||||
"hawk": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz",
|
||||
|
@ -3103,6 +3108,14 @@
|
|||
"handlebars": "4.0.5"
|
||||
}
|
||||
},
|
||||
"jenkins": {
|
||||
"version": "0.20.1",
|
||||
"resolved": "https://registry.npmjs.org/jenkins/-/jenkins-0.20.1.tgz",
|
||||
"integrity": "sha512-4vpnBYIyy995FaReWP3LAGaVsQgV9WayI1pjEHbF+oIM/nV5DyGSwa/xIojZ7U+ECk8ZcXsCJDOhuJQvCr0AUA==",
|
||||
"requires": {
|
||||
"papi": "0.26.0"
|
||||
}
|
||||
},
|
||||
"jest": {
|
||||
"version": "22.1.4",
|
||||
"resolved": "https://registry.npmjs.org/jest/-/jest-22.1.4.tgz",
|
||||
|
@ -4285,6 +4298,11 @@
|
|||
"resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz",
|
||||
"integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M="
|
||||
},
|
||||
"papi": {
|
||||
"version": "0.26.0",
|
||||
"resolved": "https://registry.npmjs.org/papi/-/papi-0.26.0.tgz",
|
||||
"integrity": "sha1-1hNqFJIHXrwmRSvY4J5EmYDEfdM="
|
||||
},
|
||||
"parse-glob": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz",
|
||||
|
|
|
@ -11,6 +11,8 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"eslint": "^4.16.0",
|
||||
"hashmap": "^2.3.0",
|
||||
"jenkins": "^0.20.1",
|
||||
"mem-cache": "0.0.5",
|
||||
"probot": "^5.0.0",
|
||||
"probot-config": "^0.1.0",
|
||||
|
|
Loading…
Reference in New Issue