Skip to content

Commit 8672dce

Browse files
authored
Merge pull request #117 from dscho/snapshot-builds
Support snapshot builds
2 parents 92ff2e6 + f4eee78 commit 8672dce

File tree

5 files changed

+477
-6
lines changed

5 files changed

+477
-6
lines changed

.vscode/launch.json

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,30 @@
55
"type": "node",
66
"request": "launch",
77
"name": "Debug Tests",
8-
"program": "${workspaceRoot}/node_modules/.bin/jest",
9-
"cwd": "${workspaceRoot}",
8+
"program": "${workspaceFolder}/node_modules/.bin/jest",
9+
"cwd": "${workspaceFolder}",
1010
// to restrict running to a particular test case, add something like "--testNamePattern=/deploy"
1111
"args": ["--runInBand", "--config", "jest.config.js"],
1212
"windows": {
1313
"program": "${workspaceFolder}/node_modules/jest/bin/jest"
1414
},
1515
"console": "integratedTerminal"
1616
},
17+
{
18+
"type": "node",
19+
"request": "launch",
20+
"name": "Test event delivery",
21+
"runtimeArgs": [
22+
"--inspect-brk",
23+
],
24+
"stopOnEntry": false,
25+
"cwd": "${workspaceFolder}",
26+
"args": [
27+
"${workspaceFolder}/test-pr-comment-delivery.js",
28+
"${workspaceFolder}/example-check-run-git-artifacts-completed.yml",
29+
],
30+
"console": "integratedTerminal"
31+
},
1732
{
1833
"name": "Attach to Node Functions",
1934
"type": "node",

GitForWindowsHelper/cascading-runs.js

Lines changed: 303 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,12 +133,314 @@ const cascadingRuns = async (context, req) => {
133133

134134
return comment
135135
}
136+
if (checkRunOwner === 'git-for-windows'
137+
&& checkRunRepo === 'git'
138+
&& name.startsWith('git-artifacts-')) {
139+
const output = req.body.check_run.output
140+
const match = output.summary.match(
141+
/Build Git (\S+) artifacts from commit (\S+) \(tag-git run #(\d+)\)/
142+
)
143+
if (!match) throw new Error(
144+
`Could not parse 'summary' attribute of check-run ${req.body.check_run.id}: ${output.summary}`
145+
)
146+
const [, ver, commit, tagGitWorkflowRunID] = match
147+
const snapshotTag = `prerelease-${ver.replace(/^v/, '')}`
148+
149+
// First, verify that the snapshot has not been uploaded yet
150+
const gitSnapshotsToken = await getToken(context, checkRunOwner, 'git-snapshots')
151+
const githubApiRequest = require('./github-api-request')
152+
try {
153+
const releasePath = `${checkRunOwner}/git-snapshots/releases/tags/${snapshotTag}`
154+
await githubApiRequest(
155+
context,
156+
gitSnapshotsToken,
157+
'GET',
158+
`/repos/${releasePath}`,
159+
)
160+
return `Ignoring ${name} check-run because the snapshot for ${commit} was already uploaded`
161+
+ ` to https://github.com/${releasePath}`
162+
} catch(e) {
163+
if (e?.statusCode !== 404) throw e
164+
// The snapshot does not exist yet
165+
}
166+
167+
// Next, check that the commit is on the `main` branch
168+
const gitToken = await getToken(context, checkRunOwner, checkRunRepo)
169+
const { behind_by } = await githubApiRequest(
170+
context,
171+
gitToken,
172+
'GET',
173+
`/repos/${checkRunOwner}/${checkRunRepo}/compare/HEAD...${commit}`,
174+
)
175+
if (behind_by > 0) {
176+
return `Ignoring ${name} check-run because its corresponding commit ${commit} is not on the main branch`
177+
}
178+
179+
const workFlowRunIDs = {}
180+
const { listCheckRunsForCommit, queueCheckRun } = require('./check-runs')
181+
for (const architecture of ['x86_64', 'i686', 'aarch64']) {
182+
const workflowName = `git-artifacts-${architecture}`
183+
const runs = name === workflowName ? [req.body.check_run] : await listCheckRunsForCommit(
184+
context,
185+
gitToken,
186+
checkRunOwner,
187+
checkRunRepo,
188+
commit,
189+
workflowName
190+
)
191+
const needle =
192+
`Build Git ${ver} artifacts from commit ${commit} (tag-git run #${tagGitWorkflowRunID})`
193+
const latest = runs
194+
.filter(run => run.output.summary === needle)
195+
.sort((a, b) => a.id - b.id)
196+
.pop()
197+
if (latest) {
198+
if (latest.status !== 'completed') {
199+
return `The '${workflowName}' run at ${latest.html_url} did not complete yet.`
200+
}
201+
if (latest.conclusion !== 'success') {
202+
throw new Error(`The '${workflowName}' run at ${latest.html_url} did not succeed.`)
203+
}
204+
205+
const match = latest.output.text.match(
206+
/For details, see \[this run\]\(https:\/\/github.com\/([^/]+)\/([^/]+)\/actions\/runs\/(\d+)\)/
207+
)
208+
if (!match) throw new Error(`Unhandled 'text' attribute of git-artifacts run ${latest.id}: ${latest.url}`)
209+
const owner = match[1]
210+
const repo = match[2]
211+
workFlowRunIDs[architecture] = match[3]
212+
if (owner !== 'git-for-windows' || repo !== 'git-for-windows-automation') {
213+
throw new Error(`Unexpected repository ${owner}/${repo} for git-artifacts run ${latest.id}: ${latest.url}`)
214+
}
215+
} else {
216+
return `Won't trigger 'upload-snapshot' in reaction to ${name} because the '${workflowName}' run does not exist.`
217+
}
218+
}
219+
220+
const checkRunTitle = `Upload snapshot ${snapshotTag}`
221+
await queueCheckRun(
222+
context,
223+
gitToken,
224+
'git-for-windows',
225+
'git',
226+
commit,
227+
'upload-snapshot',
228+
checkRunTitle,
229+
checkRunTitle
230+
)
231+
232+
const gitForWindowsAutomationToken =
233+
await getToken(context, checkRunOwner, 'git-for-windows-automation')
234+
const triggerWorkflowDispatch = require('./trigger-workflow-dispatch')
235+
const answer = await triggerWorkflowDispatch(
236+
context,
237+
gitForWindowsAutomationToken,
238+
'git-for-windows',
239+
'git-for-windows-automation',
240+
'upload-snapshot.yml',
241+
'main', {
242+
git_artifacts_x86_64_workflow_run_id: workFlowRunIDs['x86_64'],
243+
git_artifacts_i686_workflow_run_id: workFlowRunIDs['i686'],
244+
git_artifacts_aarch64_workflow_run_id: workFlowRunIDs['aarch64'],
245+
}
246+
)
247+
248+
return `The 'upload-snapshot' workflow run was started at ${answer.html_url}`
249+
}
136250
return `Not a cascading run: ${name}; Doing nothing.`
137251
}
138252
return `Unhandled action: ${action}`
139253
}
140254

255+
const handlePush = async (context, req) => {
256+
const pushOwner = req.body.repository.owner.login
257+
const pushRepo = req.body.repository.name
258+
const ref = req.body.ref
259+
const commit = req.body.after
260+
261+
if (pushOwner !== 'git-for-windows' || pushRepo !== 'git') {
262+
throw new Error(`Refusing to handle push to ${pushOwner}/${pushRepo}`)
263+
}
264+
265+
if (ref !== 'refs/heads/main') return `Ignoring push to ${ref}`
266+
267+
// See whether there was are already a `tag-git` check-run for this commit
268+
const { listCheckRunsForCommit, queueCheckRun, updateCheckRun } = require('./check-runs')
269+
const gitToken = await getToken(context, pushOwner, pushRepo)
270+
const runs = await listCheckRunsForCommit(
271+
context,
272+
gitToken,
273+
pushOwner,
274+
pushRepo,
275+
commit,
276+
'tag-git'
277+
)
278+
279+
const latest = runs
280+
.sort((a, b) => a.id - b.id)
281+
.pop()
282+
283+
if (latest && latest.status !== 'completed') throw new Error(`The 'tag-git' run at ${latest.html_url} did not complete yet before ${commit} was pushed to ${ref}!`)
284+
285+
const gitForWindowsAutomationToken =
286+
await getToken(context, pushOwner, 'git-for-windows-automation')
287+
const triggerWorkflowDispatch = require('./trigger-workflow-dispatch')
288+
if (!latest) {
289+
// There is no `tag-git` workflow run; Trigger it to build a new snapshot
290+
const tagGitCheckRunTitle = `Tag snapshot Git @${commit}`
291+
const tagGitCheckRunId = await queueCheckRun(
292+
context,
293+
gitForWindowsAutomationToken,
294+
pushOwner,
295+
pushRepo,
296+
commit,
297+
'tag-git',
298+
tagGitCheckRunTitle,
299+
tagGitCheckRunTitle
300+
)
301+
302+
try {
303+
const answer = await triggerWorkflowDispatch(
304+
context,
305+
gitForWindowsAutomationToken,
306+
pushOwner,
307+
'git-for-windows-automation',
308+
'tag-git.yml',
309+
'main', {
310+
rev: commit,
311+
owner: pushOwner,
312+
repo: pushRepo,
313+
snapshot: 'true'
314+
}
315+
)
316+
return `The 'tag-git' workflow run was started at ${answer.html_url}`
317+
} catch (e) {
318+
await updateCheckRun(
319+
context,
320+
gitForWindowsAutomationToken,
321+
pushOwner,
322+
pushRepo,
323+
tagGitCheckRunId, {
324+
status: 'completed',
325+
conclusion: 'failure',
326+
output: {
327+
title: tagGitCheckRunTitle,
328+
summary: tagGitCheckRunTitle,
329+
text: e.message || JSON.stringify(e, null, 2)
330+
}
331+
}
332+
)
333+
throw e
334+
}
335+
}
336+
337+
if (latest.conclusion !== 'success') throw new Error(
338+
`The 'tag-git' run at ${latest.html_url} did not succeed (conclusion = ${latest.conclusion}).`
339+
)
340+
341+
// There is already a `tag-git` workflow run; Is there already an `upload-snapshot` run?
342+
const latestUploadSnapshotRun = (await listCheckRunsForCommit(
343+
context,
344+
gitToken,
345+
pushOwner,
346+
pushRepo,
347+
commit,
348+
'upload-snapshot'
349+
)).pop()
350+
if (latestUploadSnapshotRun) return `The 'upload-snapshot' check-run already exists for ${commit}: ${latestUploadSnapshotRun.html_url}`
351+
352+
// Trigger the `upload-snapshot` run directly
353+
const tagGitCheckRunTitle = `Upload snapshot Git @${commit}`
354+
const tagGitCheckRunId = await queueCheckRun(
355+
context,
356+
await getToken(),
357+
pushOwner,
358+
pushRepo,
359+
commit,
360+
'tag-git',
361+
tagGitCheckRunTitle,
362+
tagGitCheckRunTitle
363+
)
364+
365+
const match = latest.output.summary.match(/^Tag Git (\S+) @([0-9a-f]+)$/)
366+
if (!match) throw new Error(`Unexpected summary '${latest.output.summary}' of tag-git run: ${latest.html_url}`)
367+
if (!match[2] === commit) throw new Error(`Unexpected revision ${match[2]} '${latest.output.summary}' of tag-git run: ${latest.html_url}`)
368+
const ver = match[1]
369+
370+
try {
371+
const workFlowRunIDs = {}
372+
for (const architecture of ['x86_64', 'i686', 'aarch64']) {
373+
const workflowName = `git-artifacts-${architecture}`
374+
const runs = await listCheckRunsForCommit(
375+
context,
376+
gitToken,
377+
pushOwner,
378+
pushRepo,
379+
commit,
380+
workflowName
381+
)
382+
const needle =
383+
`Build Git ${ver} artifacts from commit ${commit} (tag-git run #${latest.id})`
384+
const latest2 = runs
385+
.filter(run => run.output.summary === needle)
386+
.sort((a, b) => a.id - b.id)
387+
.pop()
388+
if (latest2) {
389+
if (latest2.status !== 'completed' || latest2.conclusion !== 'success') {
390+
throw new Error(`The '${workflowName}' run at ${latest2.html_url} did not succeed.`)
391+
}
392+
393+
const match = latest2.output.text.match(
394+
/For details, see \[this run\]\(https:\/\/github.com\/([^/]+)\/([^/]+)\/actions\/runs\/(\d+)\)/
395+
)
396+
if (!match) throw new Error(`Unhandled 'text' attribute of git-artifacts run ${latest2.id}: ${latest2.url}`)
397+
const owner = match[1]
398+
const repo = match[2]
399+
workFlowRunIDs[architecture] = match[3]
400+
if (owner !== 'git-for-windows' || repo !== 'git-for-windows-automation') {
401+
throw new Error(`Unexpected repository ${owner}/${repo} for git-artifacts run ${latest2.id}: ${latest2.url}`)
402+
}
403+
} else {
404+
return `Won't trigger 'upload-snapshot' on pushing ${commit} because the '${workflowName}' run does not exist.`
405+
}
406+
}
407+
408+
const answer = await triggerWorkflowDispatch(
409+
context,
410+
gitForWindowsAutomationToken,
411+
pushRepo,
412+
'git-for-windows-automation',
413+
'upload-snapshot.yml',
414+
'main', {
415+
git_artifacts_x86_64_workflow_run_id: workFlowRunIDs['x86_64'],
416+
git_artifacts_i686_workflow_run_id: workFlowRunIDs['i686'],
417+
git_artifacts_aarch64_workflow_run_id: workFlowRunIDs['aarch64'],
418+
}
419+
)
420+
421+
return `The 'upload-snapshot' workflow run was started at ${answer.html_url}`
422+
} catch (e) {
423+
await updateCheckRun(
424+
context,
425+
gitForWindowsAutomationToken,
426+
pushOwner,
427+
pushRepo,
428+
tagGitCheckRunId, {
429+
status: 'completed',
430+
conclusion: 'failure',
431+
output: {
432+
title: tagGitCheckRunTitle,
433+
summary: tagGitCheckRunTitle,
434+
text: e.message || JSON.stringify(e, null, 2)
435+
}
436+
}
437+
)
438+
throw e
439+
}
440+
}
441+
141442
module.exports = {
142443
triggerGitArtifactsRuns,
143-
cascadingRuns
444+
cascadingRuns,
445+
handlePush
144446
}

GitForWindowsHelper/index.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,13 @@ module.exports = async function (context, req) {
6262
}
6363

6464
try {
65-
const { cascadingRuns } = require('./cascading-runs.js')
65+
const { cascadingRuns, handlePush } = require('./cascading-runs.js')
6666
if (req.headers['x-github-event'] === 'check_run'
6767
&& req.body.repository.full_name === 'git-for-windows/git'
6868
&& req.body.action === 'completed') return ok(await cascadingRuns(context, req))
69+
70+
if (req.headers['x-github-event'] === 'push'
71+
&& req.body.repository.full_name === 'git-for-windows/git') return ok(await handlePush(context, req))
6972
} catch (e) {
7073
context.log(e)
7174
return withStatus(500, undefined, e.message || JSON.stringify(e, null, 2))

0 commit comments

Comments
 (0)