From a27115e6512fcf0e3f09b0fd012879358d9bbe0e Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 12 Jun 2025 12:15:20 +0200 Subject: [PATCH 1/4] add a helper script for working with milestones MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Johannes Schindelin Signed-off-by: Matthias Aßhauer --- GitForWindowsHelper/milestones.js | 50 +++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 GitForWindowsHelper/milestones.js diff --git a/GitForWindowsHelper/milestones.js b/GitForWindowsHelper/milestones.js new file mode 100644 index 00000000..17d42a67 --- /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 From adc0fb5a61691b3d8d14782a00860117ef4b3901 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20A=C3=9Fhauer?= Date: Thu, 12 Jun 2025 11:30:25 +0200 Subject: [PATCH 2/4] WIP: Add component update issues to "Next release" milestone MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This fixes https://github.com/git-for-windows/gfw-helper-github-app/issues/5 Signed-off-by: Matthias Aßhauer --- GitForWindowsHelper/index.js | 11 +++++ GitForWindowsHelper/update-milestones.js | 62 ++++++++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 GitForWindowsHelper/update-milestones.js diff --git a/GitForWindowsHelper/index.js b/GitForWindowsHelper/index.js index dc5c325d..9dded427 100644 --- a/GitForWindowsHelper/index.js +++ b/GitForWindowsHelper/index.js @@ -61,6 +61,17 @@ 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 { cascadingRuns, handlePush } = require('./cascading-runs.js') if (req.headers['x-github-event'] === 'check_run' diff --git a/GitForWindowsHelper/update-milestones.js b/GitForWindowsHelper/update-milestones.js new file mode 100644 index 00000000..4f7ec118 --- /dev/null +++ b/GitForWindowsHelper/update-milestones.js @@ -0,0 +1,62 @@ +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`) +} + + +module.exports = { + addIssueToCurrentMilestone, +} \ No newline at end of file From b6e62953e1a3f05e6a206d0dbd6b99b91cf505de Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 12 Jun 2025 12:59:50 +0200 Subject: [PATCH 3/4] WIP: rename the latest milestone when a PR is opened to rebase to the latest Git version MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This fixes https://github.com/git-for-windows/gfw-helper-github-app/issues/6 Co-authored-by: Johannes Schindelin Signed-off-by: Matthias Aßhauer --- GitForWindowsHelper/index.js | 11 +++++++ GitForWindowsHelper/update-milestones.js | 38 ++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/GitForWindowsHelper/index.js b/GitForWindowsHelper/index.js index 9dded427..0dfeb9d8 100644 --- a/GitForWindowsHelper/index.js +++ b/GitForWindowsHelper/index.js @@ -72,6 +72,17 @@ module.exports = async function (context, req) { 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 { cascadingRuns, handlePush } = require('./cascading-runs.js') if (req.headers['x-github-event'] === 'check_run' diff --git a/GitForWindowsHelper/update-milestones.js b/GitForWindowsHelper/update-milestones.js index 4f7ec118..a843e2df 100644 --- a/GitForWindowsHelper/update-milestones.js +++ b/GitForWindowsHelper/update-milestones.js @@ -56,7 +56,45 @@ const addIssueToCurrentMilestone= async (context, req) => { 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) +} module.exports = { addIssueToCurrentMilestone, + renameCurrentAndCreateNextMilestone } \ No newline at end of file From 3e208819ff5422a976fd7bb0ac8c97df7a84c91d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20A=C3=9Fhauer?= Date: Thu, 12 Jun 2025 14:23:40 +0200 Subject: [PATCH 4/4] WIP: After publishing a release, close the corresponding milestone MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This fixes https://github.com/git-for-windows/gfw-helper-github-app/issues/7 Co-authored-by: Johannes Schindelin Signed-off-by: Matthias Aßhauer --- GitForWindowsHelper/index.js | 11 +++++++ GitForWindowsHelper/update-milestones.js | 42 +++++++++++++++++++++++- 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/GitForWindowsHelper/index.js b/GitForWindowsHelper/index.js index 0dfeb9d8..7c11f72c 100644 --- a/GitForWindowsHelper/index.js +++ b/GitForWindowsHelper/index.js @@ -83,6 +83,17 @@ module.exports = async function (context, req) { 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/update-milestones.js b/GitForWindowsHelper/update-milestones.js index a843e2df..f0a19776 100644 --- a/GitForWindowsHelper/update-milestones.js +++ b/GitForWindowsHelper/update-milestones.js @@ -94,7 +94,47 @@ const renameCurrentAndCreateNextMilestone = async (context, req) => { 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 + renameCurrentAndCreateNextMilestone, + closeReleaseMilestone } \ No newline at end of file