diff --git a/src/commands.js b/src/commands.js index dd3a15bc..4ca18e31 100644 --- a/src/commands.js +++ b/src/commands.js @@ -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 + } +}) diff --git a/src/index.js b/src/index.js index c665ba5a..d5ea0fb1 100644 --- a/src/index.js +++ b/src/index.js @@ -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 @@ -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}`) + }) + }) }) }