Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

delay and then sort webhook events #6

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion src/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,4 +125,17 @@ module.exports = [
}
}
}
]
].sort(({createsACard: a}, {createsACard: b}) => {
// Ensure that the createsACard commands are at the top of the list.
// This way a new Card is Created, and then it can still be moved during the
// same webhook event.
// For example, a new Issue that already has reviewers set should move to the
// "Needs Review" column.
if (a && !b) {
return -1
} else if (b && !a) {
return 1
} else {
return 0
}
})
203 changes: 122 additions & 81 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,38 @@ const PROJECT_FRAGMENT = `
}
`

let eventQueue = []
let eventTimeout = null
async function delayEvent(logger, webhookName, handler) {
eventQueue.push({webhookName, handler})
clearTimeout(eventTimeout)
eventTimeout = setTimeout(async () => {
// Sort the queue and then fire each event
const creationEventNames = automationCommands.filter(({createsACard}) => createsACard).map(({webhookName}) => webhookName)
const sortedEvents = eventQueue.sort(({webhookName: a}, {webhookName: b}) => {
const isA = creationEventNames.indexOf(a) >= 0
const isB = creationEventNames.indexOf(b) >= 0
if (isA && !isB) {
return -1
} else if (isB && !isA) {
return 1
} else {
return 0
}
})
eventQueue = []
eventTimeout = null
for (const {handler} of sortedEvents) {
try {
await handler()
} catch (err) {
logger.error(err)
}
}
}, 200)
}


module.exports = (robot) => {
const logger = robot.log.child({name: 'project-bot'})
// Increase the maxListenerCount by the number of automationCommands
Expand All @@ -60,113 +92,122 @@ module.exports = (robot) => {
logger.trace(`Attaching listener for ${webhookName}`)
robot.on(webhookName, async function (context) {
const issueUrl = context.payload.issue ? context.payload.issue.html_url : context.payload.pull_request.html_url.replace('/pull/', '/issues/')
logger.trace(`Event received for ${webhookName}`)

// A couple commands occur when a new Issue or Pull Request is created.
// In those cases, a new Card needs to be created, rather than moving an existing card.
if (createsACard) {
const graphResult = await retryQuery(context, `
query getAllProjectCards($issueUrl: URI!) {
resource(url: $issueUrl) {
... on Issue {
id
repository {
owner {
url
${''/* Projects can be attached to an Organization... */}
... on Organization {
projects(first: 10, states: [OPEN]) {
nodes {
${PROJECT_FRAGMENT}
logger.debug(`Event received for ${webhookName}. Delaying a bit so it can be sorted`)
// Webhooks sometimes come in out-of-order. This is a problem when a new Issue (or PullRequest)
// comes in that is assigned to someone.
// Sometimes the `issues.assigned` event is received before the `issues.opened` event is.
//
// Delay firing the event so these can be sorted
delayEvent(logger, webhookName, async () => {
logger.debug(`Starting delayed event for ${webhookName}`)
// A couple commands occur when a new Issue or Pull Request is created.
// In those cases, a new Card needs to be created, rather than moving an existing card.
if (createsACard) {
const graphResult = await retryQuery(context, `
query getAllProjectCards($issueUrl: URI!) {
resource(url: $issueUrl) {
... on Issue {
id
repository {
owner {
url
${''/* Projects can be attached to an Organization... */}
... on Organization {
projects(first: 10, states: [OPEN]) {
nodes {
${PROJECT_FRAGMENT}
}
}
}
}
}
${''/* ... or on a Repository */}
projects(first: 10, states: [OPEN]) {
nodes {
${PROJECT_FRAGMENT}
${''/* ... or on a Repository */}
projects(first: 10, states: [OPEN]) {
nodes {
${PROJECT_FRAGMENT}
}
}
}
}
}
}
}
`, {issueUrl: issueUrl})
const {resource} = graphResult
`, {issueUrl: issueUrl})
const {resource} = graphResult

let allProjects = []
if (resource.repository.owner.projects) {
// Add Org Projects
allProjects = allProjects.concat(resource.repository.owner.projects.nodes)
}
if (resource.repository.projects) {
allProjects = allProjects.concat(resource.repository.projects.nodes)
}
let allProjects = []
if (resource.repository.owner.projects) {
// Add Org Projects
allProjects = allProjects.concat(resource.repository.owner.projects.nodes)
}
if (resource.repository.projects) {
allProjects = allProjects.concat(resource.repository.projects.nodes)
}

// Loop through all of the Automation Cards and see if any match
const automationRules = extractAutomationRules(allProjects).filter(({ruleName: rn}) => rn === ruleName)
// Loop through all of the Automation Cards and see if any match
const automationRules = extractAutomationRules(allProjects).filter(({ruleName: rn}) => rn === ruleName)

for (const {column, ruleArgs} of automationRules) {
if (await ruleMatcher(logger, context, ruleArgs)) {
logger.info(`Creating Card for "${issueUrl}" to column ${column.id} because of "${ruleName}" and value: "${ruleArgs}"`)
await context.github.query(`
mutation createCard($contentId: ID!, $columnId: ID!) {
addProjectCard(input: {contentId: $contentId, projectColumnId: $columnId}) {
clientMutationId
for (const {column, ruleArgs} of automationRules) {
if (await ruleMatcher(logger, context, ruleArgs)) {
logger.info(`Creating Card for "${issueUrl}" to column ${column.id} because of "${ruleName}" and value: "${ruleArgs}"`)
await context.github.query(`
mutation createCard($contentId: ID!, $columnId: ID!) {
addProjectCard(input: {contentId: $contentId, projectColumnId: $columnId}) {
clientMutationId
}
}
}
`, {contentId: resource.id, columnId: column.id})
`, {contentId: resource.id, columnId: column.id})
}
}
}
} else {
// Check if we need to move the Issue (or Pull request)
const graphResult = await retryQuery(context, `
query getCardAndColumnAutomationCards($url: URI!) {
resource(url: $url) {
... on Issue {
projectCards(first: 10) {
nodes {
id
url
column {
name
} else {
// Check if we need to move the Issue (or Pull request)
const graphResult = await retryQuery(context, `
query getCardAndColumnAutomationCards($url: URI!) {
resource(url: $url) {
... on Issue {
projectCards(first: 10) {
nodes {
id
}
project {
${PROJECT_FRAGMENT}
url
column {
name
id
}
project {
${PROJECT_FRAGMENT}
}
}
}
}
}
}
`, {url: issueUrl})
logger.trace(graphResult, 'Retrieved results')
const {resource} = graphResult
// sometimes there are no projectCards
if (!resource.projectCards) {
logger.error(issueUrl, resource, 'Not even an array for project cards. Odd')
}
`, {url: issueUrl})
logger.debug(graphResult, 'Retrieved results')
const {resource} = graphResult
// sometimes there are no projectCards
if (!resource.projectCards) {
logger.error(issueUrl, resource, 'Not even an array for project cards. Odd')
}
const cardsForIssue = resource.projectCards ? resource.projectCards.nodes : []
const cardsForIssue = resource.projectCards ? resource.projectCards.nodes : []

for (const issueCard of cardsForIssue) {
const automationRules = extractAutomationRules([issueCard.project]).filter(({ruleName: rn}) => rn === ruleName)
for (const issueCard of cardsForIssue) {
const automationRules = extractAutomationRules([issueCard.project]).filter(({ruleName: rn}) => rn === ruleName)

for (const {column, ruleArgs} of automationRules) {
if (await ruleMatcher(logger, context, ruleArgs)) {
logger.info(`Moving Card ${issueCard.id} for "${issueUrl}" to column ${column.id} because of "${ruleName}" and value: "${ruleArgs}"`)
await context.github.query(`
mutation moveCard($cardId: ID!, $columnId: ID!) {
moveProjectCard(input: {cardId: $cardId, columnId: $columnId}) {
clientMutationId
for (const {column, ruleArgs} of automationRules) {
if (await ruleMatcher(logger, context, ruleArgs)) {
logger.info(`Moving Card ${issueCard.id} for "${issueUrl}" to column ${column.id} because of "${ruleName}" and value: "${ruleArgs}"`)
await context.github.query(`
mutation moveCard($cardId: ID!, $columnId: ID!) {
moveProjectCard(input: {cardId: $cardId, columnId: $columnId}) {
clientMutationId
}
}
}
`, {cardId: issueCard.id, columnId: column.id})
`, {cardId: issueCard.id, columnId: column.id})
}
}
}
}
}
logger.debug(`Done executing handler for ${webhookName}`)
})

})
})
}