Skip to content

Support closing PR #25

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

Merged
merged 2 commits into from
Aug 23, 2021
Merged
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
145 changes: 88 additions & 57 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,14 @@ async function run() {

// Merge if they say they have access
if (context.eventName === "issue_comment" || context.eventName === "pull_request_review") {
mergeIfLGTMAndHasAccess()
const bodyLower = getPayloadBody().toLowerCase();
if (bodyLower.includes("lgtm")) {
new Actor().mergeIfHasAccess();
} else if (bodyLower.includes("@github-actions close")) {
new Actor().closeIfHasAccess();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should it require same access as merge? Should this also cover issues?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it makes sense for closing PR/issues to instead only check on whether someone exists in the codeowners - given that the action can only have social downsides and not security downsides

Happy to ship this change myself tomorrow, and build it on this post merge 👍🏻

} else {
console.log("Doing nothing because the body does not include a command")
}
}
}

Expand Down Expand Up @@ -104,77 +111,101 @@ function pathListToMarkdown(files) {
return files.map(i => `* [\`${i}\`](https://github.com/${context.repo.owner}/${context.repo.repo}/tree/HEAD${i})`).join("\n");
}

async function mergeIfLGTMAndHasAccess() {
function getPayloadBody() {
const body = context.payload.comment ? context.payload.comment.body : context.payload.review.body
if (!body) {
// For debugging #8
console.log(JSON.stringify(context))
if (body == null) {
throw new Error(`No body found, ${JSON.stringify(context)}`)
}
return body;
}

if (!body || !body.toLowerCase().includes("lgtm")) {
console.log("Comment does not include LGTM ('looks good to me') so not merging")
process.exit(0)
class Actor {
constructor() {
this.cwd = core.getInput('cwd') || process.cwd()
this.octokit = getOctokit(process.env.GITHUB_TOKEN)
this.thisRepo = { owner: context.repo.owner, repo: context.repo.repo }
this.issue = context.payload.issue || context.payload.pull_request
this.sender = context.payload.sender.login
}

// Setup
const cwd = core.getInput('cwd') || process.cwd()
const octokit = getOctokit(process.env.GITHUB_TOKEN)
const thisRepo = { owner: context.repo.owner, repo: context.repo.repo }
const issue = context.payload.issue || context.payload.pull_request
const sender = context.payload.sender.login
async getTargetPRIfHasAccess() {
const { octokit, thisRepo, sender, issue, cwd } = this;
core.info(`\n\nLooking at the ${context.eventName} from ${sender} in '${issue.title}' to see if we can proceed`)

core.info(`\n\nLooking at the ${context.eventName} from ${sender} in '${issue.title}' to see if we can merge`)
const changedFiles = await getPRChangedFiles(octokit, thisRepo, issue.number)
core.info(`Changed files: \n - ${changedFiles.join("\n - ")}`)

const changedFiles = await getPRChangedFiles(octokit, thisRepo, issue.number)
core.info(`Changed files: \n - ${changedFiles.join("\n - ")}`)
const filesWhichArentOwned = getFilesNotOwnedByCodeOwner("@" + sender, changedFiles, cwd)
if (filesWhichArentOwned.length !== 0) {
console.log(`@${sender} does not have access to \n - ${filesWhichArentOwned.join("\n - ")}\n`)
listFilesWithOwners(changedFiles, cwd)
await octokit.issues.createComment({ ...thisRepo, issue_number: issue.number, body: `Sorry @${sender}, you don't have access to these files: ${pathListToMarkdown(filesWhichArentOwned)}.` })
return
}

const filesWhichArentOwned = getFilesNotOwnedByCodeOwner("@" + sender, changedFiles, cwd)
if (filesWhichArentOwned.length !== 0) {
console.log(`@${sender} does not have access to merge \n - ${filesWhichArentOwned.join("\n - ")}\n`)
listFilesWithOwners(changedFiles, cwd)
await octokit.issues.createComment({ ...thisRepo, issue_number: issue.number, body: `Sorry @${sender}, you don't have access to merge:\n${pathListToMarkdown(filesWhichArentOwned)}` });
return
const prInfo = await octokit.pulls.get({ ...thisRepo, pull_number: issue.number })
if (prInfo.data.state.toLowerCase() !== "open") {
await octokit.issues.createComment({ ...thisRepo, issue_number: issue.number, body: `Sorry @${sender}, this PR isn't open.` });
return
}
return prInfo
}

// Don't try merge unmergable stuff
const prInfo = await octokit.pulls.get({ ...thisRepo, pull_number: issue.number })
if (!prInfo.data.mergeable) {
await octokit.issues.createComment({ ...thisRepo, issue_number: issue.number, body: `Sorry @${sender}, this PR has merge conflicts. They'll need to be fixed before this can be merged.` });
return
}
async mergeIfHasAccess() {
const prInfo = await this.getTargetPRIfHasAccess()
if (!prInfo) {
return
}

if (prInfo.data.state.toLowerCase() !== "open") {
await octokit.issues.createComment({ ...thisRepo, issue_number: issue.number, body: `Sorry @${sender}, this PR isn't open.` });
return
}
const { octokit, thisRepo, issue, sender } = this;

// Don't try merge unmergable stuff
if (!prInfo.data.mergeable) {
await octokit.issues.createComment({ ...thisRepo, issue_number: issue.number, body: `Sorry @${sender}, this PR has merge conflicts. They'll need to be fixed before this can be merged.` });
return
}

// Don't merge red PRs
const statusInfo = await octokit.repos.listCommitStatusesForRef({ ...thisRepo, ref: prInfo.data.head.sha })
const failedStatus = statusInfo.data
// Check only the most recent for a set of duplicated statuses
.filter(
(thing, index, self) =>
index === self.findIndex((t) => t.target_url === thing.target_url)
)
.find(s => s.state !== "success")

if (failedStatus) {
await octokit.issues.createComment({ ...thisRepo, issue_number: issue.number, body: `Sorry @${sender}, this PR could not be merged because it wasn't green. Blocked by [${failedStatus.context}](${failedStatus.target_url}): '${failedStatus.description}'.` });
return
}

// Don't merge red PRs
const statusInfo = await octokit.repos.listCommitStatusesForRef({ ...thisRepo, ref: prInfo.data.head.sha })
const failedStatus = statusInfo.data
// Check only the most recent for a set of duplicated statuses
.filter(
(thing, index, self) =>
index === self.findIndex((t) => t.target_url === thing.target_url)
)
.find(s => s.state !== "success")

if (failedStatus) {
await octokit.issues.createComment({ ...thisRepo, issue_number: issue.number, body: `Sorry @${sender}, this PR could not be merged because it wasn't green. Blocked by [${failedStatus.context}](${failedStatus.target_url}): '${failedStatus.description}'.` });
return
core.info(`Creating comments and merging`)
try {
// @ts-ignore
await octokit.pulls.merge({ ...thisRepo, pull_number: issue.number, merge_method: core.getInput('merge_method') || 'merge' });
await octokit.issues.createComment({ ...thisRepo, issue_number: issue.number, body: `Merging because @${sender} is a code-owner of all the changes - thanks!` });
} catch (error) {
core.info(`Merging (or commenting) failed:`)
core.error(error)
core.setFailed("Failed to merge")

const linkToCI = `https://github.com/${thisRepo.owner}/${thisRepo.repo}/runs/${process.env.GITHUB_RUN_ID}?check_suite_focus=true`
await octokit.issues.createComment({ ...thisRepo, issue_number: issue.number, body: `There was an issue merging, maybe try again ${sender}. <a href="${linkToCI}">Details</a>` });
}
}

core.info(`Creating comments and merging`)
try {
// @ts-ignore
await octokit.pulls.merge({ ...thisRepo, pull_number: issue.number, merge_method: core.getInput('merge_method') || 'merge' });
await octokit.issues.createComment({ ...thisRepo, issue_number: issue.number, body: `Merging because @${sender} is a code-owner of all the changes - thanks!` });
} catch (error) {
core.info(`Merging (or commenting) failed:`)
core.error(error)
core.setFailed("Failed to merge")
async closeIfHasAccess() {
const prInfo = await this.getTargetPRIfHasAccess()
if (!prInfo) {
return
}

const { octokit, thisRepo, issue, sender } = this;

const linkToCI = `https://github.com/${thisRepo.owner}/${thisRepo.repo}/runs/${process.env.GITHUB_RUN_ID}?check_suite_focus=true`
await octokit.issues.createComment({ ...thisRepo, issue_number: issue.number, body: `There was an issue merging, maybe try again ${sender}. <a href="${linkToCI}">Details</a>` });
core.info(`Creating comments and closing`)
await octokit.pulls.update({ ...thisRepo, pull_number: issue.number, state: "closed" });
await octokit.issues.createComment({ ...thisRepo, issue_number: issue.number, body: `Closing because @${sender} is a code-owner of all the changes.` });
}
}

Expand Down