Skip to content

Commit eb71f5c

Browse files
authored
Merge pull request #66 from dscho/auto-close-git-for-windows/git-prs-after-release
After a Git for Windows version has been released, automatically close the corresponding PR
2 parents 57ec7de + fc88436 commit eb71f5c

5 files changed

Lines changed: 165 additions & 22 deletions

File tree

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
module.exports = async (context, req) => {
2+
if (req.body.action !== 'completed') return "Nothing to do here: workflow run did not complete yet"
3+
if (req.body.workflow_run.conclusion !== 'success') return "Nothing to do here: workflow run did not succeed"
4+
5+
const releaseWorkflowRunID = req.body.workflow_run.id
6+
const owner = 'git-for-windows'
7+
const repo = 'git'
8+
const sender = req.body.sender.login
9+
10+
const getToken = (() => {
11+
let token
12+
13+
const get = async () => {
14+
const getInstallationIdForRepo = require('./get-installation-id-for-repo')
15+
const installationId = await getInstallationIdForRepo(context, owner, repo)
16+
const getInstallationAccessToken = require('./get-installation-access-token')
17+
return await getInstallationAccessToken(context, installationId)
18+
}
19+
20+
return async () => token || (token = await get())
21+
})()
22+
23+
const isAllowed = async (login) => {
24+
const getCollaboratorPermissions = require('./get-collaborator-permissions')
25+
const token = await getToken()
26+
const permission = await getCollaboratorPermissions(context, token, owner, repo, login)
27+
return ['ADMIN', 'MAINTAIN', 'WRITE'].includes(permission.toString())
28+
}
29+
30+
if (!await isAllowed(sender)) throw new Error(`${sender} is not allowed to do that`)
31+
32+
const { searchIssues } = require('./search')
33+
const items = await searchIssues(context, 'org:git-for-windows is:pr is:open in:comments "The release-git workflow run was started"')
34+
35+
const githubApiRequest = require('./github-api-request')
36+
37+
const needle = `The \`release-git\` workflow run [was started](https://github.com/git-for-windows/git-for-windows-automation/actions/runs/${releaseWorkflowRunID})`
38+
const candidates = []
39+
for (const item of items) {
40+
if (!['OWNER', 'MEMBER'].includes(item.author_association)) continue
41+
for (const match of item.text_matches) {
42+
const commentURL = match.object_url
43+
if (!commentURL.startsWith('https://api.github.com/')) continue
44+
const data = await githubApiRequest(context, await getToken(), 'GET', commentURL.substring(22))
45+
if (data.body.indexOf(needle) >=0) candidates.push(item)
46+
}
47+
}
48+
49+
if (candidates.length !== 1) throw new Error(`Expected 1 candidate PR, got ${candidates.length}`)
50+
51+
const prNumber = candidates[0].number
52+
const pr = await githubApiRequest(context, await getToken(), 'GET', `/repos/${owner}/${repo}/pulls/${prNumber}`)
53+
const sha = pr.head.sha
54+
55+
await githubApiRequest(context, await getToken(), 'PATCH', `/repos/${owner}/${repo}/git/refs/heads/main`, {
56+
sha,
57+
force: false // require fast-forward
58+
})
59+
60+
return `Took care of pushing the \`main\` branch to close PR ${prNumber}`
61+
}

GitForWindowsHelper/index.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,18 @@ module.exports = async function (context, req) {
4949
return withStatus(500, undefined, e.message || JSON.stringify(e, null, 2))
5050
}
5151

52+
try {
53+
const finalizeGitForWindowsRelease = require('./finalize-g4w-release')
54+
if (req.headers['x-github-event'] === 'workflow_run'
55+
&& req.body.repository.full_name === 'git-for-windows/git-for-windows-automation'
56+
&& req.body.action === 'completed'
57+
&& req.body.workflow_run.path === '.github/workflows/release-git.yml'
58+
&& req.body.workflow_run.conclusion === 'success') return ok(await finalizeGitForWindowsRelease(context, req))
59+
} catch (e) {
60+
context.log(e)
61+
return withStatus(500, undefined, e.message || JSON.stringify(e, null, 2))
62+
}
63+
5264
try {
5365
const { cascadingRuns } = require('./cascading-runs.js')
5466
if (req.headers['x-github-event'] === 'check_run'

__tests__/index.test.js

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ let mockGitHubApiRequest = jest.fn((_context, _token, method, requestPath, paylo
6767
id: 0,
6868
html_url: `appended-comment-body-${payload.body}`
6969
}
70+
if (method === 'GET' && requestPath.endsWith('/comments/654321')) return {
71+
body: 'The `release-git` workflow run [was started](https://github.com/git-for-windows/git-for-windows-automation/actions/runs/54321)'
72+
}
7073
if (method === 'POST' && requestPath.endsWith('/reactions')) return {
7174
id: `new-reaction-${payload.content}`
7275
}
@@ -75,7 +78,7 @@ let mockGitHubApiRequest = jest.fn((_context, _token, method, requestPath, paylo
7578
data: {
7679
repository:{
7780
collaborators: {
78-
edges: [{ permission: 'WRITE'}]
81+
edges: [{ permission: 'WRITE' }]
7982
}
8083
}
8184
}
@@ -130,6 +133,14 @@ let mockGitHubApiRequest = jest.fn((_context, _token, method, requestPath, paylo
130133
if (method === 'GET' && requestPath.endsWith('/pulls/4323')) return {
131134
head: { sha: 'dee501d15' }
132135
}
136+
if (method === 'GET' && requestPath.endsWith('/pulls/765')) return {
137+
head: { sha: 'c0ffee1ab7e' }
138+
}
139+
if (method === 'PATCH' && requestPath.endsWith('/git/refs/heads/main')) {
140+
if (payload.sha !== 'c0ffee1ab7e') throw new Error(`Unexpected sha: ${payload.sha}`)
141+
if (payload.force !== false) throw new Error(`Unexpected force value: ${payload.force}`)
142+
return {}
143+
}
133144
if (method === 'GET' && requestPath === '/search/issues?q=repo:git-for-windows/git+c8edb521bdabec14b07e9142e48cab77a40ba339+type:pr+%22git-artifacts%22') return {
134145
items: [{
135146
text_matches: [{
@@ -263,7 +274,16 @@ jest.mock('../GitForWindowsHelper/get-installation-id-for-repo', () => {
263274
return mockGetInstallationIDForRepo
264275
})
265276

266-
let mockSearchIssues = jest.fn(() => [])
277+
let mockSearchIssues = jest.fn((_context, searchTerms) => {
278+
if (searchTerms.indexOf('release-git') > 0) return [{
279+
number: 765,
280+
author_association: 'MEMBER',
281+
text_matches: [{
282+
object_url: 'https://api.github.com/repos/git-for-windows/git/issues/comments/654321'
283+
}]
284+
}]
285+
return []
286+
})
267287
jest.mock('../GitForWindowsHelper/search', () => {
268288
return {
269289
searchIssues: mockSearchIssues
@@ -753,3 +773,43 @@ The \`release-git\` workflow run [was started](dispatched-workflow-release-git.y
753773
git_artifacts_i686_workflow_run_id: "686"
754774
})
755775
})
776+
777+
test('a completed `release-git` run updates the `main` branch in git-for-windows/git', async () => {
778+
const context = makeContext({
779+
action: 'completed',
780+
repository: {
781+
full_name: 'git-for-windows/git-for-windows-automation'
782+
},
783+
sender: {
784+
login: 'a-member'
785+
},
786+
workflow_run: {
787+
id: 54321,
788+
path: '.github/workflows/release-git.yml',
789+
conclusion: 'success'
790+
}
791+
}, {
792+
'x-github-event': 'workflow_run'
793+
})
794+
795+
try {
796+
expect(await index(context, context.req)).toBeUndefined()
797+
expect(context.res).toEqual({
798+
body: `Took care of pushing the \`main\` branch to close PR 765`,
799+
headers: undefined,
800+
status: undefined
801+
})
802+
expect(mockGitHubApiRequest).toHaveBeenCalledTimes(4)
803+
expect(mockGitHubApiRequest.mock.calls[3].slice(1)).toEqual([
804+
'installation-access-token',
805+
'PATCH',
806+
'/repos/git-for-windows/git/git/refs/heads/main', {
807+
sha: 'c0ffee1ab7e',
808+
force: false
809+
}
810+
])
811+
} catch (e) {
812+
context.log.mock.calls.forEach(e => console.log(e[0]))
813+
throw e;
814+
}
815+
})

get-webhook-event-payload.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@
9898
const events = [...answer.events]
9999
while (answer.oldest.epoch > since) {
100100
answer = await getAtCursor(answer.oldest.id - 1)
101-
events.push([...answer.events])
101+
events.push(...answer.events)
102102
}
103103

104104
return events

test-pr-comment-delivery.js

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -26,25 +26,35 @@
2626
headers: {}
2727
}
2828

29-
const payloadOffset = contents.indexOf('\n{')
30-
if (payloadOffset < 0) throw new Error(`Could not find start of payload in ${path}`)
31-
contents.substring(0, payloadOffset).split(/\r?\n/).forEach(line => {
32-
const colon = line.indexOf(':')
33-
if (colon < 0) return
34-
35-
const key = line.substring(0, colon).toLowerCase()
36-
const value = line.substring(colon + 1).replace(/^\s+/, '')
37-
38-
if (key === 'request method') req.method = value
39-
else req.headers[key] = value
40-
})
41-
req.rawBody = contents.substring(payloadOffset + 1)
42-
// In https://github.com/organizations/git-for-windows/settings/apps/gitforwindowshelper/advanced,
43-
// the JSON is pretty-printed, but the actual webhook event avoids any
44-
// unnecessary white-space in the body
45-
.replace(/\r?\n\s*("[^"]*":)\s*/g, '$1')
46-
.replace(/\r?\n\s*/g, '')
47-
req.body = JSON.parse(req.rawBody)
29+
if (contents.startsWith('event: {')) {
30+
const event = JSON.parse(contents.substring(7))
31+
Object.keys(event.request.headers).forEach(key => {
32+
req.headers[key.toLowerCase()] = event.request.headers[key]
33+
})
34+
req.body = event.request.payload
35+
req.rawBody = JSON.stringify(req.body)
36+
req.method = 'POST'
37+
} else {
38+
const payloadOffset = contents.indexOf('\n{')
39+
if (payloadOffset < 0) throw new Error(`Could not find start of payload in ${path}`)
40+
contents.substring(0, payloadOffset).split(/\r?\n/).forEach(line => {
41+
const colon = line.indexOf(':')
42+
if (colon < 0) return
43+
44+
const key = line.substring(0, colon).toLowerCase()
45+
const value = line.substring(colon + 1).replace(/^\s+/, '')
46+
47+
if (key === 'request method') req.method = value
48+
else req.headers[key] = value
49+
})
50+
req.rawBody = contents.substring(payloadOffset + 1)
51+
// In https://github.com/organizations/git-for-windows/settings/apps/gitforwindowshelper/advanced,
52+
// the JSON is pretty-printed, but the actual webhook event avoids any
53+
// unnecessary white-space in the body
54+
.replace(/\r?\n\s*("[^"]*":)\s*/g, '$1')
55+
.replace(/\r?\n\s*/g, '')
56+
req.body = JSON.parse(req.rawBody)
57+
}
4858

4959
const context = {
5060
log: console.log,

0 commit comments

Comments
 (0)