Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,30 @@
"type": "node",
"request": "launch",
"name": "Debug Tests",
"program": "${workspaceRoot}/node_modules/.bin/jest",
"cwd": "${workspaceRoot}",
"program": "${workspaceFolder}/node_modules/.bin/jest",
"cwd": "${workspaceFolder}",
// to restrict running to a particular test case, add something like "--testNamePattern=/deploy"
"args": ["--runInBand", "--config", "jest.config.js"],
"windows": {
"program": "${workspaceFolder}/node_modules/jest/bin/jest"
},
"console": "integratedTerminal"
},
{
"type": "node",
"request": "launch",
"name": "Test event delivery",
"runtimeArgs": [
"--inspect-brk",
],
"stopOnEntry": false,
"cwd": "${workspaceFolder}",
"args": [
"${workspaceFolder}/test-pr-comment-delivery.js",
"${workspaceFolder}/example-check-run-git-artifacts-completed.yml",
],
"console": "integratedTerminal"
},
{
"name": "Attach to Node Functions",
"type": "node",
Expand Down
304 changes: 303 additions & 1 deletion GitForWindowsHelper/cascading-runs.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,12 +133,314 @@ const cascadingRuns = async (context, req) => {

return comment
}
if (checkRunOwner === 'git-for-windows'
&& checkRunRepo === 'git'
&& name.startsWith('git-artifacts-')) {
const output = req.body.check_run.output
const match = output.summary.match(
/Build Git (\S+) artifacts from commit (\S+) \(tag-git run #(\d+)\)/
)
if (!match) throw new Error(
`Could not parse 'summary' attribute of check-run ${req.body.check_run.id}: ${output.summary}`
)
const [, ver, commit, tagGitWorkflowRunID] = match
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've never seen this syntax before (the leading empty ,).. is it intentional?

const [, ver, .....

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this skips the match[0], which would be the entire matched string (I only need the capturing groups).

const snapshotTag = `prerelease-${ver.replace(/^v/, '')}`

// First, verify that the snapshot has not been uploaded yet
const gitSnapshotsToken = await getToken(context, checkRunOwner, 'git-snapshots')
const githubApiRequest = require('./github-api-request')
try {
const releasePath = `${checkRunOwner}/git-snapshots/releases/tags/${snapshotTag}`
await githubApiRequest(
context,
gitSnapshotsToken,
'GET',
`/repos/${releasePath}`,
)
return `Ignoring ${name} check-run because the snapshot for ${commit} was already uploaded`
+ ` to https://github.com/${releasePath}`
} catch(e) {
if (e?.statusCode !== 404) throw e
// The snapshot does not exist yet
}

// Next, check that the commit is on the `main` branch
const gitToken = await getToken(context, checkRunOwner, checkRunRepo)
const { behind_by } = await githubApiRequest(
context,
gitToken,
'GET',
`/repos/${checkRunOwner}/${checkRunRepo}/compare/HEAD...${commit}`,
)
if (behind_by > 0) {
return `Ignoring ${name} check-run because its corresponding commit ${commit} is not on the main branch`
}

const workFlowRunIDs = {}
const { listCheckRunsForCommit, queueCheckRun } = require('./check-runs')
for (const architecture of ['x86_64', 'i686', 'aarch64']) {
const workflowName = `git-artifacts-${architecture}`
const runs = name === workflowName ? [req.body.check_run] : await listCheckRunsForCommit(
context,
gitToken,
checkRunOwner,
checkRunRepo,
commit,
workflowName
)
const needle =
`Build Git ${ver} artifacts from commit ${commit} (tag-git run #${tagGitWorkflowRunID})`
const latest = runs
.filter(run => run.output.summary === needle)
Comment on lines +191 to +194
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This filtering feels fragile. Is this the best (only, I suspect 😢) way to filter runs?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, there is no other way, really. I use the title, summary and output to "store" information that links those workflow runs together.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the record: The summary is specified in the git-artifacts workflow.

.sort((a, b) => a.id - b.id)
.pop()
if (latest) {
if (latest.status !== 'completed') {
return `The '${workflowName}' run at ${latest.html_url} did not complete yet.`
}
if (latest.conclusion !== 'success') {
throw new Error(`The '${workflowName}' run at ${latest.html_url} did not succeed.`)
}

const match = latest.output.text.match(
/For details, see \[this run\]\(https:\/\/github.com\/([^/]+)\/([^/]+)\/actions\/runs\/(\d+)\)/
)
if (!match) throw new Error(`Unhandled 'text' attribute of git-artifacts run ${latest.id}: ${latest.url}`)
const owner = match[1]
const repo = match[2]
workFlowRunIDs[architecture] = match[3]
if (owner !== 'git-for-windows' || repo !== 'git-for-windows-automation') {
throw new Error(`Unexpected repository ${owner}/${repo} for git-artifacts run ${latest.id}: ${latest.url}`)
}
} else {
return `Won't trigger 'upload-snapshot' in reaction to ${name} because the '${workflowName}' run does not exist.`
}
}

const checkRunTitle = `Upload snapshot ${snapshotTag}`
await queueCheckRun(
context,
gitToken,
'git-for-windows',
'git',
commit,
'upload-snapshot',
checkRunTitle,
checkRunTitle
)

const gitForWindowsAutomationToken =
await getToken(context, checkRunOwner, 'git-for-windows-automation')
const triggerWorkflowDispatch = require('./trigger-workflow-dispatch')
const answer = await triggerWorkflowDispatch(
context,
gitForWindowsAutomationToken,
'git-for-windows',
'git-for-windows-automation',
'upload-snapshot.yml',
'main', {
git_artifacts_x86_64_workflow_run_id: workFlowRunIDs['x86_64'],
git_artifacts_i686_workflow_run_id: workFlowRunIDs['i686'],
git_artifacts_aarch64_workflow_run_id: workFlowRunIDs['aarch64'],
}
)

return `The 'upload-snapshot' workflow run was started at ${answer.html_url}`
}
return `Not a cascading run: ${name}; Doing nothing.`
}
return `Unhandled action: ${action}`
}

const handlePush = async (context, req) => {
const pushOwner = req.body.repository.owner.login
const pushRepo = req.body.repository.name
const ref = req.body.ref
const commit = req.body.after

if (pushOwner !== 'git-for-windows' || pushRepo !== 'git') {
throw new Error(`Refusing to handle push to ${pushOwner}/${pushRepo}`)
}

if (ref !== 'refs/heads/main') return `Ignoring push to ${ref}`

// See whether there was are already a `tag-git` check-run for this commit
const { listCheckRunsForCommit, queueCheckRun, updateCheckRun } = require('./check-runs')
const gitToken = await getToken(context, pushOwner, pushRepo)
const runs = await listCheckRunsForCommit(
context,
gitToken,
pushOwner,
pushRepo,
commit,
'tag-git'
)

const latest = runs
.sort((a, b) => a.id - b.id)
.pop()

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}!`)

const gitForWindowsAutomationToken =
await getToken(context, pushOwner, 'git-for-windows-automation')
const triggerWorkflowDispatch = require('./trigger-workflow-dispatch')
if (!latest) {
// There is no `tag-git` workflow run; Trigger it to build a new snapshot
const tagGitCheckRunTitle = `Tag snapshot Git @${commit}`
const tagGitCheckRunId = await queueCheckRun(
context,
gitForWindowsAutomationToken,
pushOwner,
pushRepo,
commit,
'tag-git',
tagGitCheckRunTitle,
tagGitCheckRunTitle
)

try {
const answer = await triggerWorkflowDispatch(
context,
gitForWindowsAutomationToken,
pushOwner,
'git-for-windows-automation',
'tag-git.yml',
'main', {
rev: commit,
owner: pushOwner,
repo: pushRepo,
snapshot: 'true'
}
)
return `The 'tag-git' workflow run was started at ${answer.html_url}`
} catch (e) {
await updateCheckRun(
context,
gitForWindowsAutomationToken,
pushOwner,
pushRepo,
tagGitCheckRunId, {
status: 'completed',
conclusion: 'failure',
output: {
title: tagGitCheckRunTitle,
summary: tagGitCheckRunTitle,
text: e.message || JSON.stringify(e, null, 2)
}
}
)
throw e
}
}

if (latest.conclusion !== 'success') throw new Error(
`The 'tag-git' run at ${latest.html_url} did not succeed (conclusion = ${latest.conclusion}).`
)

// There is already a `tag-git` workflow run; Is there already an `upload-snapshot` run?
const latestUploadSnapshotRun = (await listCheckRunsForCommit(
context,
gitToken,
pushOwner,
pushRepo,
commit,
'upload-snapshot'
)).pop()
if (latestUploadSnapshotRun) return `The 'upload-snapshot' check-run already exists for ${commit}: ${latestUploadSnapshotRun.html_url}`

// Trigger the `upload-snapshot` run directly
const tagGitCheckRunTitle = `Upload snapshot Git @${commit}`
const tagGitCheckRunId = await queueCheckRun(
context,
await getToken(),
pushOwner,
pushRepo,
commit,
'tag-git',
tagGitCheckRunTitle,
tagGitCheckRunTitle
)

const match = latest.output.summary.match(/^Tag Git (\S+) @([0-9a-f]+)$/)
if (!match) throw new Error(`Unexpected summary '${latest.output.summary}' of tag-git run: ${latest.html_url}`)
if (!match[2] === commit) throw new Error(`Unexpected revision ${match[2]} '${latest.output.summary}' of tag-git run: ${latest.html_url}`)
const ver = match[1]

try {
const workFlowRunIDs = {}
for (const architecture of ['x86_64', 'i686', 'aarch64']) {
const workflowName = `git-artifacts-${architecture}`
const runs = await listCheckRunsForCommit(
context,
gitToken,
pushOwner,
pushRepo,
commit,
workflowName
)
const needle =
`Build Git ${ver} artifacts from commit ${commit} (tag-git run #${latest.id})`
const latest2 = runs
.filter(run => run.output.summary === needle)
.sort((a, b) => a.id - b.id)
.pop()
if (latest2) {
if (latest2.status !== 'completed' || latest2.conclusion !== 'success') {
throw new Error(`The '${workflowName}' run at ${latest2.html_url} did not succeed.`)
}

const match = latest2.output.text.match(
/For details, see \[this run\]\(https:\/\/github.com\/([^/]+)\/([^/]+)\/actions\/runs\/(\d+)\)/
)
if (!match) throw new Error(`Unhandled 'text' attribute of git-artifacts run ${latest2.id}: ${latest2.url}`)
const owner = match[1]
const repo = match[2]
workFlowRunIDs[architecture] = match[3]
if (owner !== 'git-for-windows' || repo !== 'git-for-windows-automation') {
throw new Error(`Unexpected repository ${owner}/${repo} for git-artifacts run ${latest2.id}: ${latest2.url}`)
}
} else {
return `Won't trigger 'upload-snapshot' on pushing ${commit} because the '${workflowName}' run does not exist.`
}
}

const answer = await triggerWorkflowDispatch(
context,
gitForWindowsAutomationToken,
pushRepo,
'git-for-windows-automation',
'upload-snapshot.yml',
'main', {
git_artifacts_x86_64_workflow_run_id: workFlowRunIDs['x86_64'],
git_artifacts_i686_workflow_run_id: workFlowRunIDs['i686'],
git_artifacts_aarch64_workflow_run_id: workFlowRunIDs['aarch64'],
}
)

return `The 'upload-snapshot' workflow run was started at ${answer.html_url}`
} catch (e) {
await updateCheckRun(
context,
gitForWindowsAutomationToken,
pushOwner,
pushRepo,
tagGitCheckRunId, {
status: 'completed',
conclusion: 'failure',
output: {
title: tagGitCheckRunTitle,
summary: tagGitCheckRunTitle,
text: e.message || JSON.stringify(e, null, 2)
}
}
)
throw e
}
}

module.exports = {
triggerGitArtifactsRuns,
cascadingRuns
cascadingRuns,
handlePush
}
5 changes: 4 additions & 1 deletion GitForWindowsHelper/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,13 @@ module.exports = async function (context, req) {
}

try {
const { cascadingRuns } = require('./cascading-runs.js')
const { cascadingRuns, handlePush } = require('./cascading-runs.js')
if (req.headers['x-github-event'] === 'check_run'
&& req.body.repository.full_name === 'git-for-windows/git'
&& req.body.action === 'completed') return ok(await cascadingRuns(context, req))

if (req.headers['x-github-event'] === 'push'
&& req.body.repository.full_name === 'git-for-windows/git') return ok(await handlePush(context, req))
} catch (e) {
context.log(e)
return withStatus(500, undefined, e.message || JSON.stringify(e, null, 2))
Expand Down
Loading