Skip to content

Commit 7695973

Browse files
committed
Add a function to trigger a GitHub workflow
The GitHub workflow must have a `workflow_dispatch` trigger for this to work. The `workflow_id` is the file name of the workflow, e.g. `sync-ref.yml`. This function is a slightly edited version of a function that has been in use in https://github.com/git-for-windows/gfw-helper-github-app for quite a while. But the test is new. Signed-off-by: Johannes Schindelin <[email protected]>
1 parent f79a22c commit 7695973

File tree

3 files changed

+112
-0
lines changed

3 files changed

+112
-0
lines changed

GitGitGadget/github-api-request.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
const gitHubAPIRequest = async (context, token, method, requestPath, payload) => {
2+
const { httpsRequest } = require('./https-request')
3+
const headers = token ? { Authorization: `Bearer ${token}` } : null
4+
const answer = await httpsRequest(context, null, method, requestPath, payload, headers)
5+
if (answer.error) throw answer.error
6+
return answer
7+
}
8+
9+
module.exports = {
10+
gitHubAPIRequest
11+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
const { gitHubAPIRequest } = require('./github-api-request')
2+
const { gitHubAPIRequestAsApp } = require('./github-api-request-as-app')
3+
4+
const sleep = async (milliseconds) => {
5+
return new Promise((resolve) => {
6+
setTimeout(resolve, milliseconds)
7+
})
8+
}
9+
10+
const getActorForToken = async (context, token) => {
11+
try {
12+
const { login } = await gitHubAPIRequest(context, token, 'GET', '/user')
13+
return login
14+
} catch (e) {
15+
if (e.statusCode !== 403 || e.json?.message !== 'Resource not accessible by integration') throw e
16+
const answer = await gitHubAPIRequestAsApp(context, 'GET', '/app')
17+
return `${answer.slug}[bot]`
18+
}
19+
}
20+
21+
const waitForWorkflowRun = async (context, token, owner, repo, workflow_id, after, actor) => {
22+
if (!actor) actor = await getActorForToken(context, token)
23+
let counter = 0
24+
for (;;) {
25+
const res = await gitHubAPIRequest(
26+
context,
27+
token,
28+
'GET',
29+
`/repos/${owner}/${repo}/actions/runs?actor=${actor}&event=workflow_dispatch&created=>${after}`
30+
)
31+
const filtered = res.workflow_runs.filter(e => e.path === `.github/workflows/${workflow_id}`)
32+
if (filtered.length > 0) return filtered
33+
if (counter++ > 30) throw new Error(`Times out waiting for workflow?`)
34+
await sleep(1000)
35+
}
36+
}
37+
38+
const triggerWorkflowDispatch = async (context, token, owner, repo, workflow_id, ref, inputs) => {
39+
const { headers: { date } } = await gitHubAPIRequest(
40+
context,
41+
token,
42+
'POST',
43+
`/repos/${owner}/${repo}/actions/workflows/${workflow_id}/dispatches`,
44+
{ ref, inputs }
45+
)
46+
47+
const runs = await waitForWorkflowRun(context, token, owner, repo, workflow_id, new Date(date).toISOString())
48+
return runs[0]
49+
}
50+
51+
module.exports = {
52+
triggerWorkflowDispatch,
53+
waitForWorkflowRun
54+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
const mockHTTPSRequest = jest.fn(async (_context, _hostname, method, requestPath) => {
2+
if (method === 'POST' && requestPath === '/repos/hello/world/actions/workflows/the-workflow.yml/dispatches') {
3+
return {
4+
headers: {
5+
date: '2023-01-23T01:23:45Z'
6+
}
7+
}
8+
}
9+
if (method === 'GET' && requestPath === '/user') return { login: 'the actor' }
10+
if (method === 'GET' && requestPath === '/repos/hello/world/actions/runs?actor=the actor&event=workflow_dispatch&created=>2023-01-23T01:23:45.000Z') {
11+
return {
12+
workflow_runs: [
13+
{ path: 'not this one.yml' },
14+
{ path: '.github/workflows/the-workflow.yml', breadcrumb: true },
15+
{ path: 'neither this one.yml' }
16+
]
17+
}
18+
}
19+
throw new Error(`Unexpected requestPath: ${method} '${requestPath}'`)
20+
})
21+
jest.mock('../GitGitGadget/https-request', () => { return { httpsRequest: mockHTTPSRequest } })
22+
23+
const { triggerWorkflowDispatch } = require('../GitGitGadget/trigger-workflow-dispatch')
24+
25+
const { generateKeyPairSync } = require('crypto')
26+
27+
const { privateKey } = generateKeyPairSync('rsa', {
28+
modulusLength: 2048,
29+
publicKeyEncoding: {
30+
type: 'spki',
31+
format: 'pem'
32+
},
33+
privateKeyEncoding: {
34+
type: 'pkcs8',
35+
format: 'pem',
36+
}
37+
})
38+
process.env['GITHUB_APP_PRIVATE_KEY'] = privateKey
39+
40+
test('trigger a workflow_dispatch event and wait for workflow run', async () => {
41+
const context = {}
42+
const run = await triggerWorkflowDispatch(context, 'my-token', 'hello', 'world', 'the-workflow.yml', 'HEAD', { abc: 123 })
43+
expect(run).toEqual({
44+
path: '.github/workflows/the-workflow.yml',
45+
breadcrumb: true
46+
})
47+
})

0 commit comments

Comments
 (0)