diff --git a/GitForWindowsHelper/index.js b/GitForWindowsHelper/index.js index dc5c325..7c11f72 100644 --- a/GitForWindowsHelper/index.js +++ b/GitForWindowsHelper/index.js @@ -61,6 +61,39 @@ module.exports = async function (context, req) { return withStatus(500, undefined, e.message || JSON.stringify(e, null, 2)) } + try { + const {addIssueToCurrentMilestone} = require('./update-milestones') + if (req.headers['x-github-event'] === 'pull_request' + && ['git-for-windows/build-extra', 'git-for-windows/MINGW-packages', 'git-for-windows/MSYS2-packages'].includes(req.body.repository.full_name) + && req.body.action === 'closed' + && req.body.pull_request.merged === 'true') return ok(await addIssueToCurrentMilestone(context, req)) + } catch (e) { + context.log(e) + return withStatus(500, undefined, e.message || JSON.stringify(e, null, 2)) + } + + try { + const {renameCurrentAndCreateNextMilestone} = require('./update-milestones') + if (req.headers['x-github-event'] === 'pull_request' + && req.body.repository.full_name === 'git-for-windows/git' + && req.body.action === 'opened' + && req.body.pull_request.merged === 'true') return ok(await renameCurrentAndCreateNextMilestone(context, req)) + } catch (e) { + context.log(e) + return withStatus(500, undefined, e.message || JSON.stringify(e, null, 2)) + } + + try { + const {closeReleaseMilestone} = require('./update-milestones') + if (req.headers['x-github-event'] === 'pull_request' + && req.body.repository.full_name === 'git-for-windows/git' + && req.body.action === 'closed' + && req.body.pull_request.merged === 'true') return ok(await closeReleaseMilestone(context, req)) + } catch (e) { + context.log(e) + return withStatus(500, undefined, e.message || JSON.stringify(e, null, 2)) + } + try { const { cascadingRuns, handlePush } = require('./cascading-runs.js') if (req.headers['x-github-event'] === 'check_run' diff --git a/GitForWindowsHelper/milestones.js b/GitForWindowsHelper/milestones.js new file mode 100644 index 0000000..17d42a6 --- /dev/null +++ b/GitForWindowsHelper/milestones.js @@ -0,0 +1,50 @@ +const getCurrentMilestone = async (context, token, owner, repo) => { + return await getMilestoneByName(context, token, owner, repo, 'Next release') +} + +const getMilestoneByName = async (context, token, owner, repo, name) => { + const githubApiRequest = require('./github-api-request') + const milestones = await githubApiRequest(context, token, 'GET', `/repos/${owner}/${repo}/milestones?state=open`) + if (milestones.length === 2) { + const filtered = milestones.filter(m => m.title !== name) + if (filtered.length === 1) milestones.splice(0, 2, filtered) + } + if (milestones.length !== 1) throw new Error(`Expected one milestone, got ${milestones.length}`) + return milestones[0] +} + +const closeMilestone = async (context, token, owner, repo, milestoneNumber, dueOn) => { + const githubApiRequest = require('./github-api-request') + const payload = { + state: 'closed' + } + if (dueOn) payload.due_on = dueOn + await githubApiRequest(context, token, 'PATCH', `/repos/${owner}/${repo}/milestones/${milestoneNumber}`, payload) +} + +const renameMilestone = async (context, token, owner, repo, milestoneNumber, newName) => { + const githubApiRequest = require('./github-api-request') + const payload = { + title: newName + } + await githubApiRequest(context, token, 'PATCH', `/repos/${owner}/${repo}/milestones/${milestoneNumber}`, payload) +} + +const openNextReleaseMilestone = async (context, token, owner, repo) => { + const githubApiRequest = require('./github-api-request') + const milestones = await githubApiRequest(context, token, 'GET', `/repos/${owner}/${repo}/milestones?state=open`) + const filtered = milestones.filter(m => m.title === 'Next release') + if (filtered.length === 1) return filtered[0] + + return await githubApiRequest(context, token, 'POST', `/repos/${owner}/${repo}/milestones`, { + title: 'Next release' + }) +} + +module.exports = { + getCurrentMilestone, + getMilestoneByName, + closeMilestone, + renameMilestone, + openNextReleaseMilestone +} \ No newline at end of file diff --git a/GitForWindowsHelper/update-milestones.js b/GitForWindowsHelper/update-milestones.js new file mode 100644 index 0000000..f0a1977 --- /dev/null +++ b/GitForWindowsHelper/update-milestones.js @@ -0,0 +1,140 @@ +const addIssueToCurrentMilestone= async (context, req) => { + if (req.body.action !== 'closed') return "Nothing to do here: PR has not been closed" + if (req.body.pull_request.merged !== 'true') return "Nothing to do here: PR has been closed, but not by merging" + + const owner = 'git-for-windows' + const repo = 'git' + const sender = req.body.sender.login + + const getToken = (() => { + let token + + const get = async () => { + const getInstallationIdForRepo = require('./get-installation-id-for-repo') + const installationId = await getInstallationIdForRepo(context, owner, repo) + const getInstallationAccessToken = require('./get-installation-access-token') + return await getInstallationAccessToken(context, installationId) + } + + return async () => token || (token = await get()) + })() + + const isAllowed = async (login) => { + if (login === 'gitforwindowshelper[bot]') return true + const getCollaboratorPermissions = require('./get-collaborator-permissions') + const token = await getToken() + const permission = await getCollaboratorPermissions(context, token, owner, repo, login) + return ['ADMIN', 'MAINTAIN', 'WRITE'].includes(permission.toString()) + } + + if (!await isAllowed(sender)) throw new Error(`${sender} is not allowed to do that`) + + const githubApiRequest = require('./github-api-request') + + const candidates = req.body.pull_request.body.match(/(?:[Cc]loses|[Ff]ixes) (?:https:\/\/github\.com\/git-for-windows\/git\/issues\/|#)(\d+)/) + + if (candidates.length !== 1) throw new Error(`Expected 1 candidate issue, got ${candidates.length}`) + + const { getCurrentMilestone } = require('./GitForWindowsHelper/milestones') + const current = await getCurrentMilestone(console, await getToken(), owner, repo) + + const issueNumber = candidates[0] + const issue = await githubApiRequest(context, await getToken(), 'GET', `/repos/${owner}/${repo}/issues/${issueNumber}`) + + if (issue.labels.length>0){ + for (const label of issue.labels) { + if (label.name === "component-update"){ + await githubApiRequest(context, await getToken(), 'PATCH', `/repos/${owner}/${repo}/issues/${issueNumber}`, { + milestone: current.id + }) + + return `Added issue ${issueNumber} to milestone "Next release"` + } + } + } + + throw new Error(`Issue ${issueNumber} isn't a component update`) +} + +const renameCurrentAndCreateNextMilestone = async (context, req) => { + const gitVersionMatch = req.body.pull_request.title.match(/^Rebase to (v\d+\.\d+\.\d+)$/) + if (!gitVersionMatch) throw new Error(`Not a new Git version: ${req.body.pull_request.title}`) + const gitVersion = gitVersionMatch[1] + + const owner = 'git-for-windows' + const repo = 'git' + const sender = req.body.sender.login + + const getToken = (() => { + let token + + const get = async () => { + const getInstallationIdForRepo = require('./get-installation-id-for-repo') + const installationId = await getInstallationIdForRepo(context, owner, repo) + const getInstallationAccessToken = require('./get-installation-access-token') + return await getInstallationAccessToken(context, installationId) + } + + return async () => token || (token = await get()) + })() + + const isAllowed = async (login) => { + if (login === 'gitforwindowshelper[bot]') return true + const getCollaboratorPermissions = require('./get-collaborator-permissions') + const token = await getToken() + const permission = await getCollaboratorPermissions(context, token, owner, repo, login) + return ['ADMIN', 'MAINTAIN', 'WRITE'].includes(permission.toString()) + } + + if (!await isAllowed(sender)) throw new Error(`${sender} is not allowed to do that`) + + const { getCurrentMilestone, renameMilestone, openNextReleaseMilestone } = require('./milestones') + const current = await getCurrentMilestone(console, await getToken(), owner, repo) + await renameMilestone(context, await getToken(), owner, repo,current.id, gitVersion) + await openNextReleaseMilestone(context, await getToken(), owner, repo) +} + +const closeReleaseMilestone = async (context, req) => { + const gitVersionMatch = req.body.pull_request.title.match(/^Rebase to (v\d+\.\d+\.\d+)$/) + if (!gitVersionMatch) throw new Error(`Not a new Git version: ${req.body.pull_request.title}`) + const gitVersion = gitVersionMatch[1] + + const owner = 'git-for-windows' + const repo = 'git' + const sender = req.body.sender.login + + const getToken = (() => { + let token + + const get = async () => { + const getInstallationIdForRepo = require('./get-installation-id-for-repo') + const installationId = await getInstallationIdForRepo(context, owner, repo) + const getInstallationAccessToken = require('./get-installation-access-token') + return await getInstallationAccessToken(context, installationId) + } + + return async () => token || (token = await get()) + })() + + const isAllowed = async (login) => { + if (login === 'gitforwindowshelper[bot]') return true + const getCollaboratorPermissions = require('./get-collaborator-permissions') + const token = await getToken() + const permission = await getCollaboratorPermissions(context, token, owner, repo, login) + return ['ADMIN', 'MAINTAIN', 'WRITE'].includes(permission.toString()) + } + + if (!await isAllowed(sender)) throw new Error(`${sender} is not allowed to do that`) + + const { getMilestoneByName, closeMilestone } = require('./milestones') + const current = await getMilestoneByName(console, await getToken(), owner, repo, gitVersion) + if (current.open_issues > 0) throw new Error(`Milestone ${current.title} has ${current.open_issues} open issue(s)!`) + if (current.closed_issues == 0) throw new Error(`Milestone ${current.title} has no closed issue(s)!`) + await closeMilestone(context, await getToken(), owner, repo,current.id) +} + +module.exports = { + addIssueToCurrentMilestone, + renameCurrentAndCreateNextMilestone, + closeReleaseMilestone +} \ No newline at end of file