-
Notifications
You must be signed in to change notification settings - Fork 5
WIP: Address various issues about milestones #143
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
base: main
Are you sure you want to change the base?
Changes from all commits
a27115e
adc0fb5
b6e6295
3e20881
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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)) | ||
Comment on lines
+88
to
+91
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Alternatively, we could piggy-back onto the code that "pushes" the PR branch to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It does cut out a roundtrip, but it does require duplication of the "is this a pre-release" logic from https://github.com/git-for-windows/git-for-windows-automation/blob/main/.github/actions/github-release/action.yml and maybe some special handling of out of band releases, whereas the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, I missed that. But wouldn't we want much more narrow conditions to be met before calling |
||
} 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' | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should probably be more lenient and test There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've meant to ask about that. |
||
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 | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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`) | ||
Comment on lines
+9
to
+30
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess it is about high time to move this (duplicated) logic into a central place instead, and maybe now would be as good a time as any to do that? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That thought has crossed my mind as well. |
||
|
||
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}`) | ||
Comment on lines
+34
to
+36
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was looking for something more robust, and while there does not seem anything useful in GitHub's REST API, there is something in the GraphQL API. Try this, for example:
Over here, this returns this highly useful answer:
We already have a precedent of a GraphQL call when looking for the "collaborator permission", so we could something similar here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That does look immensely more helpful than the REST API |
||
|
||
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 | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd rather do that when publishing the final release, i.e. at the same time as the current milestone is closed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That would prevent issues with late component updates.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't it the other way round? When a final rebase PR is opened, and
/git-artifacts
produces an installer that has issues that require acomponent-update
ticket to be addressed, we'd still want that in the same milestone, right?