diff --git a/index.js b/index.js index 8ee32f7..8f8cc10 100644 --- a/index.js +++ b/index.js @@ -2,4 +2,4 @@ require = require('esm')(module); const mod = require('./src/index.mjs').default; -module.exports = mod; +mod(); diff --git a/index.mjs b/index.mjs index 5329a9f..ab478a1 100644 --- a/index.mjs +++ b/index.mjs @@ -1,3 +1,3 @@ import mod from './src/index.mjs'; -export default mod; +mod(); diff --git a/src/index.mjs b/src/index.mjs index 8ace411..deca71e 100644 --- a/src/index.mjs +++ b/src/index.mjs @@ -1,3 +1,63 @@ -export default function() { - // es6 module code goes here +import dotenv from 'dotenv'; +import createRepo from './lib/create_repo.mjs'; +import getComments from './lib/get_comments.mjs'; +import getPull from './lib/get_pull.mjs'; +import getCommits from './lib/get_commits.mjs'; +import getCommitStatus from './lib/get_commit_status.mjs'; +import History from './lib/history.mjs'; + +// load env vars from .env file +dotenv.config(); + +export default async function() { + // parse repo name from cli and create repo instance + const repo = createRepo(process.argv.splice(2)[0]); + const { COMMENT_BODY_REGEXP, COMMENT_BODY_REGEXP_FLAGS, COMMENT_ACTOR } = process.env; + + // load the history module + const history = new History(); + + // fetch comment info from event stream, filter for only new comments + const comments = (await getComments(repo, { + body: new RegExp(COMMENT_BODY_REGEXP, COMMENT_BODY_REGEXP_FLAGS), + actor: COMMENT_ACTOR, + })).filter(comment => !history.get(comment.id)); + + // read pull data, filter out any closed pulls + const pulls = (await Promise.all( + comments.map(async comment => { + const pull = await getPull(repo, comment.number); + if (pull.state !== 'open') return false; + return { comment, pull }; + }) + )).filter(Boolean); + + const records = (await Promise.all( + pulls.map(async ({ pull, comment }) => { + const commit = await getCommits(repo, pull.number, true); + const buildStatus = await getCommitStatus(repo, commit.sha); + // do nothing if the build has not started, or is pending or successful + if (!buildStatus || buildStatus.state === 'pending' || buildStatus.state === 'success') + return false; + return { comment, pull, commit, buildStatus }; + }) + )).filter(Boolean); + + console.log(records); + process.exit(); + + /* + + TODO: + + - [x] keep track of seen comment ids, only process new ones + - [x] check the pr's status and only retest if no longer "Pending" + - [ ] add a retest comment + - POST /repos/:owner/:repo/issues/:number/comments + - [ ] delete the build comment + - DELETE /repos/:owner/:repo/issues/comments/:comment_id + - [ ] delete ALL build failure comments + - [ ] delete the retest comment + + */ } diff --git a/src/lib/create_repo.mjs b/src/lib/create_repo.mjs new file mode 100644 index 0000000..12ae949 --- /dev/null +++ b/src/lib/create_repo.mjs @@ -0,0 +1,21 @@ +/* eslint no-console: 0 */ +import Octokat from 'octokat'; + +function usageError() { + console.error('You must provide a github repo in the form of "owner/repo"'); + process.exit(1); +} + +export default function(repo) { + if (typeof repo !== 'string' || repo.length === 0) usageError(); + const settings = { + token: process.env.GITHUB_ACCESS_TOKEN, + }; + + const octo = new Octokat(settings); + const [owner, name] = repo.split('/'); + + if (!owner || !name) usageError(); + + return octo.repos(owner, name); +} diff --git a/src/lib/get_comments.mjs b/src/lib/get_comments.mjs new file mode 100644 index 0000000..ccd3292 --- /dev/null +++ b/src/lib/get_comments.mjs @@ -0,0 +1,23 @@ +export default async function getEvents(repo, { body, actor } = {}) { + const { items: events } = await repo.events.fetch(); + + return events + .map(comment => { + // only comments created by specific user + if (comment.type !== 'IssueCommentEvent') return false; + if (comment.payload.action !== 'created') return false; + if (comment.actor.login !== actor) return false; + if (body && !body.test(comment.payload.comment.body)) return false; + + return { + id: comment.id, + number: comment.payload.issue.number, + owner: comment.payload.issue.user.login, + labels: comment.payload.issue.labels.map(label => label.name), + comment_id: comment.payload.comment.id, + comment_author: comment.payload.comment.user.login, + comment_body: comment.payload.comment.body, + }; + }) + .filter(Boolean); +} diff --git a/src/lib/get_commit_status.mjs b/src/lib/get_commit_status.mjs new file mode 100644 index 0000000..881adbc --- /dev/null +++ b/src/lib/get_commit_status.mjs @@ -0,0 +1,22 @@ +function getBuildStatus({ items: statuses }) { + const buildStatus = statuses.find(status => status.context === 'kibana-ci'); + if (!buildStatus) return buildStatus; + return { + id: buildStatus.id, + url: buildStatus.url, + state: buildStatus.state, + }; +} + +export default async function getCommitStatus(repo, sha) { + const statuses = await repo.commits(sha).statuses.fetch(); + + const buildStatus = await (async stats => { + if (stats.lastPage) { + return getBuildStatus(await stats.lastPage.fetch()); + } + return getBuildStatus(stats); + })(statuses); + + return buildStatus; +} diff --git a/src/lib/get_commits.mjs b/src/lib/get_commits.mjs new file mode 100644 index 0000000..7a8ba38 --- /dev/null +++ b/src/lib/get_commits.mjs @@ -0,0 +1,23 @@ +function formatCommit(commit) { + return { + sha: commit.sha, + owner: commit.committer.login, + message: commit.commit.message, + }; +} + +function formatCommits(commits, lastOnly) { + if (lastOnly) return formatCommit(commits.pop()); + return commits.map(c => formatCommit(c)); +} + +export default async function getCommits(repo, id, lastOnly = true) { + const commits = await repo.pulls(id).commits.fetch(); + + if (commits.lastPage) { + const { items } = await commits.lastPage.fetch(); + return formatCommits(items, lastOnly); + } + + return formatCommits(commits.items, lastOnly); +} diff --git a/src/lib/get_pull.mjs b/src/lib/get_pull.mjs new file mode 100644 index 0000000..1296505 --- /dev/null +++ b/src/lib/get_pull.mjs @@ -0,0 +1,11 @@ +export default async function getPull(repo, id) { + const pull = await repo.pulls(id).fetch(); + return { + id: pull.id, + url: pull.url, + number: pull.number, + state: pull.state, + title: pull.title, + labels: pull.labels.map(label => label.name), + }; +}