Skip to content
1 change: 1 addition & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
{
"type": "node",
"request": "launch",
"console": "integratedTerminal",
"name": "Debug Tests",
"program": "${workspaceRoot}/node_modules/.bin/jest",
"cwd": "${workspaceRoot}",
Expand Down
88 changes: 49 additions & 39 deletions GitGitGadget/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@
*/
const { validateGitHubWebHook } = require('./validate-github-webhook');

const { triggerAzurePipeline } = require('./trigger-azure-pipeline');

const { triggerWorkflowDispatch } = require('./trigger-workflow-dispatch')
const { triggerWorkflowDispatch, listWorkflowRuns } = require('./trigger-workflow-dispatch')

module.exports = async (context, req) => {
try {
Expand All @@ -29,56 +27,74 @@ module.exports = async (context, req) => {

try {
/*
* The Azure Pipeline needs to be installed as a PR build on _the very
* same_ repository that triggers this function. That is, when the
* Azure Function triggers GitGitGadget for gitgitgadget/git, it needs
* to know that pipelineId 3 is installed on gitgitgadget/git, and
* trigger that very pipeline.
*
* So whenever we extend GitGitGadget to handle another repository, we
* will have to add an Azure Pipeline, install it on that repository as
* a PR build, and add the information here.
* For various reasons, the GitGitGadget GitHub App can be installed
* on any random repository. However, GitGitGadget only wants to support
* the `gitgitgadget/git` and the `git/git` repository (with the
* `dscho/git` one thrown in for debugging purposes).
*/
const pipelines = {
'dscho': 12,
'git': 13,
'gitgitgadget': 3,
};
const orgs = ['gitgitgadget', 'git', 'dscho']
const a = [context, undefined, 'gitgitgadget-workflows', 'gitgitgadget-workflows']

const eventType = context.req.headers['x-github-event'];
context.log(`Got eventType: ${eventType}`);
const repositoryOwner = req.body.repository.owner.login;
if (pipelines[repositoryOwner] === undefined) {
if (!orgs.includes(repositoryOwner)) {
context.res = {
status: 403,
body: 'Refusing to work on a repository other than gitgitgadget/git or git/git'
};
} else if (eventType === 'pull_request') {
if (req.body.action !== 'opened' && req.body.action !== 'synchronize') {
context.res = {
body: `Ignoring pull request action: ${req.body.action}`,
};
} else {
const run = await triggerWorkflowDispatch(...a, 'handle-pr-push.yml', 'main', {
'pr-url': req.body.pull_request.html_url
})
context.res = { body: `Okay, triggered ${run.html_url}!` };
}
} else if ((new Set(['check_run', 'status']).has(eventType))) {
context.res = {
body: `Ignored event type: ${eventType}`,
};
} else if (eventType === 'push') {
if (req.body.repository.full_name !== 'git/git') {
if (req.body.repository.full_name ==='gitgitgadget/git-mailing-list-mirror') {
context.res = { body: `push(${req.body.ref} in ${req.body.repository.full_name}): ` }
if (req.body.ref === 'refs/heads/lore-1') {
const queued = await listWorkflowRuns(...a, 'handle-new-mails.yml', 'queued')
if (queued.length) {
context.res.body += [
`skip triggering handle-new-emails, ${queued} already queued:`,
queued.map(e => `- ${e.html_url}`)
].join('\n')
} else {
const run = await triggerWorkflowDispatch(...a, 'handle-new-mails.yml', 'main')
context.res.body += `triggered ${run.html_url}`
}
} else context.res.body += `Ignoring non-default branches`
} else if (req.body.repository.full_name !== 'git/git') {
context.res = { body: `Ignoring pushes to ${req.body.repository.full_name}` }
} else {
const run = await triggerWorkflowDispatch(
context,
undefined,
'gitgitgadget-workflows',
'gitgitgadget-workflows',
...a,
'sync-ref.yml',
'main', {
ref: req.body.ref
}
)
context.res = { body: `push(${req.body.ref}): triggered ${run.html_url}` }
const extra = []
if (req.body.ref === 'refs/heads/seen') {
for (const workflow of ['update-prs.yml', 'update-mail-to-commit-notes.yml']) {
if ((await listWorkflowRuns(...a, workflow, 'main', 'queued')).length === 0) {
const run = await triggerWorkflowDispatch(...a, workflow, 'main')
extra.push(` and ${run.html_url}`)
}
}
}
context.res = { body: `push(${req.body.ref}): triggered ${run.html_url}${extra.join('')}` }
}
} else if (eventType === 'issue_comment') {
const triggerToken = process.env['GITGITGADGET_TRIGGER_TOKEN'];
if (!triggerToken) {
throw new Error('No configured trigger token');
}

const comment = req.body.comment;
const prNumber = req.body.issue.number;
if (!comment || !comment.id || !prNumber) {
Expand All @@ -100,19 +116,13 @@ module.exports = async (context, req) => {
return;
}

const sourceBranch = `refs/pull/${prNumber}/head`;
const parameters = {
'pr.comment.id': comment.id,
};
const pipelineId = pipelines[repositoryOwner];
if (!pipelineId || pipelineId < 1)
throw new Error(`No pipeline set up for org ${repositoryOwner}`);
context.log(`Queuing with branch ${sourceBranch} and parameters ${JSON.stringify(parameters)}`);
await triggerAzurePipeline(triggerToken, 'gitgitgadget', 'git', pipelineId, sourceBranch, parameters);
const run = await triggerWorkflowDispatch(...a, 'handle-pr-comment.yml', 'main', {
'pr-comment-url': comment.html_url
})

context.res = {
// status: 200, /* Defaults to 200 */
body: 'Okay!',
body: `Okay, triggered ${run.html_url}!`,
};
} else {
context.log(`Unhandled request:\n${JSON.stringify(req, null, 4)}`);
Expand Down
48 changes: 0 additions & 48 deletions GitGitGadget/trigger-azure-pipeline.js

This file was deleted.

29 changes: 28 additions & 1 deletion GitGitGadget/trigger-workflow-dispatch.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,34 @@ const triggerWorkflowDispatch = async (context, token, owner, repo, workflow_id,
return runs[0]
}

const listWorkflowRuns = async (context, token, owner, repo, workflow_id, branch, status) => {
if (token === undefined) {
const { getInstallationIdForRepo } = require('./get-installation-id-for-repo')
const installationID = await getInstallationIdForRepo(context, owner, repo)

const { getInstallationAccessToken } = require('./get-installation-access-token')
token = await getInstallationAccessToken(context, installationID)
}

const query = [
branch && `branch=${branch}`,
status && `status=${status}`,
]
.filter((e) => e)
.map((e, i) => `${i === 0 ? '?' : '&'}${e}`)
.join('')

const result = await gitHubAPIRequest(
context,
token,
'GET',
`/repos/${owner}/${repo}/actions/workflows/${workflow_id}/runs${query}`,
)
return result.workflow_runs
}

module.exports = {
triggerWorkflowDispatch,
waitForWorkflowRun
waitForWorkflowRun,
listWorkflowRuns,
}
131 changes: 113 additions & 18 deletions __tests__/index.test.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
const mockTriggerWorkflowDispatch = jest.fn(async (_context, _token, owner, repo, workflow_id, ref, inputs) => {
expect(`${owner}/${repo}`).toEqual('gitgitgadget-workflows/gitgitgadget-workflows')
expect(workflow_id).toEqual('sync-ref.yml')
expect(ref).toEqual('main')
expect(inputs).toEqual({ ref: 'refs/heads/next' })
return { html_url: '<the URL to the workflow run>'}
const mockTriggerWorkflowDispatch = jest.fn(async (_context, _token, _owner, _repo, workflow_id, ref, inputs) => {
return { html_url: `<the URL to the workflow ${workflow_id} run on ${ref} with inputs ${JSON.stringify(inputs)}>` }
})
jest.mock('../GitGitGadget/trigger-workflow-dispatch', () => ({
triggerWorkflowDispatch: mockTriggerWorkflowDispatch
triggerWorkflowDispatch: mockTriggerWorkflowDispatch,
listWorkflowRuns: jest.fn(async (_context, _token, _owner, _repo, workflow_id, branch, status) => {
if (workflow_id === 'update-prs.yml' && branch === 'main' && status === 'queued') {
// pretend that `update-prs` is clogged up, for whatever reason
return [
{ id: 1, head_branch: 'main', status: 'queued', html_url: '<the URL to the workflow run>' },
{ id: 2, head_branch: 'main', status: 'queued', html_url: '<another URL to the workflow run>' }
]
}
return []
})
}))

const index = require('../GitGitGadget/index')
Expand Down Expand Up @@ -147,17 +153,10 @@ const testIssueComment = (comment, repoOwner, fn) => {
testIssueComment('/test', async (context) => {
expect(context.done).toHaveBeenCalledTimes(1)
expect(context.res).toEqual({
body: 'Okay!'
})
expect(mockRequest.write).toHaveBeenCalledTimes(1)
expect(JSON.parse(mockRequest.write.mock.calls[0][0])).toEqual({
definition: {
id: 3
},
sourceBranch: 'refs/pull/1886743660/head',
parameters: '{"pr.comment.id":27988538471837300}'
body: 'Okay, triggered <the URL to the workflow handle-pr-comment.yml run on main with inputs {"pr-comment-url":"https://github.com/gitgitgadget/git/pull/1886743660"}>!'
})
expect(mockRequest.end).toHaveBeenCalledTimes(1)
expect(mockRequest.write).not.toHaveBeenCalled()
expect(mockRequest.end).not.toHaveBeenCalled()
})

testIssueComment('/verify-repository', 'nope', (context) => {
Expand Down Expand Up @@ -197,7 +196,7 @@ testWebhookPayload('react to `next` being pushed to git/git', 'push', {
}
}, (context) => {
expect(context.res).toEqual({
body: 'push(refs/heads/next): triggered <the URL to the workflow run>'
body: 'push(refs/heads/next): triggered <the URL to the workflow sync-ref.yml run on main with inputs {"ref":"refs/heads/next"}>'
})
expect(mockTriggerWorkflowDispatch).toHaveBeenCalledTimes(1)
expect(mockTriggerWorkflowDispatch.mock.calls[0]).toEqual([
Expand All @@ -210,4 +209,100 @@ testWebhookPayload('react to `next` being pushed to git/git', 'push', {
ref: 'refs/heads/next'
}
])
})

testWebhookPayload('react to `seen` being pushed to git/git', 'push', {
ref: 'refs/heads/seen',
repository: {
full_name: 'git/git',
owner: {
login: 'git'
}
}
}, (context) => {
expect(context.res).toEqual({
body: [
'push(refs/heads/seen):',
'triggered <the URL to the workflow sync-ref.yml run on main with inputs {"ref":"refs/heads/seen"}>',
'and <the URL to the workflow update-mail-to-commit-notes.yml run on main with inputs undefined>'
].join(' ')
})
// we expect `update-prs` _not_ to be triggered here because we pretend that it is already queued
expect(mockTriggerWorkflowDispatch).toHaveBeenCalledTimes(2)
expect(mockTriggerWorkflowDispatch.mock.calls[0]).toEqual([
context,
undefined,
'gitgitgadget-workflows',
'gitgitgadget-workflows',
'sync-ref.yml',
'main', {
ref: 'refs/heads/seen'
}
])
expect(mockTriggerWorkflowDispatch.mock.calls[1]).toEqual([
context,
undefined,
'gitgitgadget-workflows',
'gitgitgadget-workflows',
'update-mail-to-commit-notes.yml',
'main',
undefined
])
})

testWebhookPayload('react to `lore-1` being pushed to https://github.com/gitgitgadget/git-mailing-list-mirror', 'push', {
ref: 'refs/heads/lore-1',
repository: {
full_name: 'gitgitgadget/git-mailing-list-mirror',
owner: {
login: 'gitgitgadget'
}
}
}, (context) => {
expect(context.res).toEqual({
body: [
'push(refs/heads/lore-1 in gitgitgadget/git-mailing-list-mirror):',
'triggered <the URL to the workflow handle-new-mails.yml run on main with inputs undefined>'
].join(' ')
})
expect(mockTriggerWorkflowDispatch).toHaveBeenCalledTimes(1)
expect(mockTriggerWorkflowDispatch.mock.calls[0]).toEqual([
context,
undefined,
'gitgitgadget-workflows',
'gitgitgadget-workflows',
'handle-new-mails.yml',
'main',
undefined
])
})
testWebhookPayload('react to PR push', 'pull_request', {
action: 'synchronize',
pull_request: {
html_url: 'https://github.com/gitgitgadget/git/pull/1956',
},
repository: {
full_name: 'gitgitgadget/git',
owner: {
login: 'gitgitgadget'
}
}
}, (context) => {
expect(context.res).toEqual({
body: [
'Okay, triggered <the URL to the workflow handle-pr-push.yml run on main with inputs',
'{"pr-url":"https://github.com/gitgitgadget/git/pull/1956"}>!'
].join(' ')
})
expect(mockTriggerWorkflowDispatch).toHaveBeenCalledTimes(1)
expect(mockTriggerWorkflowDispatch.mock.calls[0]).toEqual([
context,
undefined,
'gitgitgadget-workflows',
'gitgitgadget-workflows',
'handle-pr-push.yml',
'main', {
'pr-url': 'https://github.com/gitgitgadget/git/pull/1956',
}
])
})
Loading