Skip to content

Commit 323bb7d

Browse files
authored
Merge pull request #2 from gitgitgadget/trigger-sync-ref-workflow-runs
Automatically trigger the `sync-ref` workflow runs on pushes to `git/git`
2 parents 4bd5d1b + 7400342 commit 323bb7d

14 files changed

+474
-66
lines changed

GitGitGadget/gently.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
module.exports = (fn, fallback) => {
2+
try {
3+
return fn()
4+
} catch (e) {
5+
return fallback
6+
}
7+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
const getInstallationAccessToken = async (context, installation_id) => {
2+
const { gitHubAPIRequestAsApp } = require('./github-api-request-as-app')
3+
const answer = await gitHubAPIRequestAsApp(
4+
context,
5+
'POST',
6+
`/app/installations/${installation_id}/access_tokens`)
7+
if (answer.error) throw answer.error
8+
if (answer.token) return answer.token
9+
throw new Error(`Unhandled response:\n${JSON.stringify(answer, null, 2)}`)
10+
}
11+
12+
module.exports = {
13+
getInstallationAccessToken
14+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
const getInstallationIdForRepo = async (context, owner, repo) => {
2+
const { gitHubAPIRequestAsApp } = require('./github-api-request-as-app')
3+
const answer = await gitHubAPIRequestAsApp(
4+
context,
5+
'GET',
6+
`/repos/${owner}/${repo}/installation`
7+
)
8+
if (answer.error) throw answer.error
9+
return answer.id
10+
}
11+
12+
module.exports = {
13+
getInstallationIdForRepo
14+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
const gitHubAPIRequestAsApp = async (context, requestMethod, requestPath, body) => {
2+
const header = {
3+
"alg": "RS256",
4+
"typ": "JWT"
5+
}
6+
7+
const now = Math.floor(new Date().getTime() / 1000)
8+
9+
const payload = {
10+
// issued at time, 60 seconds in the past to allow for clock drift
11+
iat: now - 60,
12+
// JWT expiration time (10 minute maximum)
13+
exp: now + (10 * 60),
14+
// GitHub App's identifier
15+
iss: process.env['GITHUB_APP_ID']
16+
}
17+
18+
const toBase64 = (obj) => Buffer.from(JSON.stringify(obj), "utf-8").toString("base64url")
19+
const headerAndPayload = `${toBase64(header)}.${toBase64(payload)}`
20+
21+
const privateKey = process.env['GITHUB_APP_PRIVATE_KEY'].replaceAll('\\n', '\n')
22+
23+
const crypto = require('crypto')
24+
const signer = crypto.createSign("RSA-SHA256")
25+
signer.update(headerAndPayload)
26+
const signature = signer.sign({
27+
key: privateKey
28+
}, "base64url")
29+
30+
const token = `${headerAndPayload}.${signature}`
31+
32+
const { httpsRequest } = require('./https-request')
33+
return await httpsRequest(
34+
context,
35+
null,
36+
requestMethod,
37+
requestPath,
38+
body,
39+
{
40+
Authorization: `Bearer ${token}`,
41+
}
42+
)
43+
}
44+
45+
module.exports = {
46+
gitHubAPIRequestAsApp
47+
}

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+
}

GitGitGadget/https-request.js

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
const gently = require('./gently')
2+
3+
const httpsRequest = async (context, hostname, method, requestPath, body, headers) => {
4+
headers = {
5+
'User-Agent': 'GitForWindowsHelper/0.0',
6+
Accept: 'application/json',
7+
...headers || {}
8+
}
9+
if (body) {
10+
if (typeof body === 'object') body = JSON.stringify(body)
11+
headers['Content-Type'] = 'application/json'
12+
headers['Content-Length'] = body.length
13+
}
14+
const options = {
15+
port: 443,
16+
hostname: hostname || 'api.github.com',
17+
method: method || 'GET',
18+
path: requestPath,
19+
headers
20+
}
21+
return new Promise((resolve, reject) => {
22+
try {
23+
const https = require('https')
24+
const req = https.request(options, res => {
25+
res.on('error', e => reject(e))
26+
27+
if (res.statusCode === 204) resolve({
28+
statusCode: res.statusCode,
29+
statusMessage: res.statusMessage,
30+
headers: res.headers
31+
})
32+
33+
const chunks = []
34+
res.on('data', data => chunks.push(data))
35+
res.on('end', () => {
36+
const json = Buffer.concat(chunks).toString('utf-8')
37+
if (res.statusCode > 299) {
38+
reject({
39+
statusCode: res.statusCode,
40+
statusMessage: res.statusMessage,
41+
requestMethod: options.method,
42+
requestPath: options.path,
43+
body: json,
44+
json: gently(() => JSON.parse(json))
45+
})
46+
return
47+
}
48+
try {
49+
resolve(JSON.parse(json))
50+
} catch (e) {
51+
reject(`Invalid JSON: ${json}`)
52+
}
53+
})
54+
})
55+
req.on('error', err => reject(err))
56+
if (body) req.write(body)
57+
req.end()
58+
} catch (e) {
59+
reject(e)
60+
}
61+
})
62+
}
63+
64+
const doesURLReturn404 = async url => {
65+
const match = url.match(/^https:\/\/([^/]+?)(:\d+)?(\/.*)?$/)
66+
if (!match) throw new Error(`Could not parse URL ${url}`)
67+
68+
const https = require('https')
69+
const options = {
70+
method: 'HEAD',
71+
host: match[1],
72+
port: Number.parseInt(match[2] || '443'),
73+
path: match[3] || '/'
74+
}
75+
return new Promise((resolve, reject) => {
76+
https.request(options, res => {
77+
if (res.error) reject(res.error)
78+
else if (res.statusCode === 404) resolve(true)
79+
else if (res.statusCode === 200) resolve(false)
80+
else reject(`Unexpected statusCode: ${res.statusCode}`)
81+
}).end()
82+
})
83+
}
84+
85+
module.exports = {
86+
httpsRequest,
87+
doesURLReturn404
88+
}

GitGitGadget/index.js

Lines changed: 19 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -8,73 +8,11 @@
88
* via the "webHookType", starting with v2, we have to do the payload
99
* validation "by hand".
1010
*/
11-
const crypto = require('crypto');
12-
const https = require('https');
11+
const { validateGitHubWebHook } = require('./validate-github-webhook');
1312

14-
const validateGitHubWebHook = (context) => {
15-
const secret = process.env['GITHUB_WEBHOOK_SECRET'];
16-
if (!secret) {
17-
throw new Error('Webhook secret not configured');
18-
}
19-
if (context.req.headers['content-type'] !== 'application/json') {
20-
throw new Error('Unexpected content type: ' + context.req.headers['content-type']);
21-
}
22-
const signature = context.req.headers['x-hub-signature-256'];
23-
if (!signature) {
24-
throw new Error('Missing X-Hub-Signature');
25-
}
26-
const sha256 = signature.match(/^sha256=(.*)/);
27-
if (!sha256) {
28-
throw new Error('Unexpected X-Hub-Signature format: ' + signature);
29-
}
30-
const computed = crypto.createHmac('sha256', secret).update(context.req.rawBody).digest('hex');
31-
if (sha256[1] !== computed) {
32-
throw new Error('Incorrect X-Hub-Signature');
33-
}
34-
}
13+
const { triggerAzurePipeline } = require('./trigger-azure-pipeline');
3514

36-
const triggerAzurePipeline = async (token, organization, project, buildDefinitionId, sourceBranch, parameters) => {
37-
const auth = Buffer.from('PAT:' + token).toString('base64');
38-
const headers = {
39-
'Accept': 'application/json; api-version=5.0-preview.5; excludeUrls=true',
40-
'Authorization': 'Basic ' + auth,
41-
};
42-
const json = JSON.stringify({
43-
'definition': { 'id': buildDefinitionId },
44-
'sourceBranch': sourceBranch,
45-
'parameters': JSON.stringify(parameters),
46-
});
47-
headers['Content-Type'] = 'application/json';
48-
headers['Content-Length'] = Buffer.byteLength(json);
49-
50-
const requestOptions = {
51-
host: 'dev.azure.com',
52-
port: '443',
53-
path: `/${organization}/${project}/_apis/build/builds?ignoreWarnings=false&api-version=5.0-preview.5`,
54-
method: 'POST',
55-
headers: headers
56-
};
57-
58-
return new Promise((resolve, reject) => {
59-
const handleResponse = (res) => {
60-
res.setEncoding('utf8');
61-
var response = '';
62-
res.on('data', (chunk) => {
63-
response += chunk;
64-
});
65-
res.on('end', () => {
66-
resolve(JSON.parse(response));
67-
});
68-
res.on('error', (err) => {
69-
reject(err);
70-
})
71-
};
72-
73-
const request = https.request(requestOptions, handleResponse);
74-
request.write(json);
75-
request.end();
76-
});
77-
}
15+
const { triggerWorkflowDispatch } = require('./trigger-workflow-dispatch')
7816

7917
module.exports = async (context, req) => {
8018
try {
@@ -119,6 +57,22 @@ module.exports = async (context, req) => {
11957
context.res = {
12058
body: `Ignored event type: ${eventType}`,
12159
};
60+
} else if (eventType === 'push') {
61+
if (req.body.repository.full_name !== 'git/git') {
62+
context.res = { body: `Ignoring pushes to ${req.body.repository.full_name}` }
63+
} else {
64+
const run = await triggerWorkflowDispatch(
65+
context,
66+
undefined,
67+
'gitgitgadget',
68+
'gitgitgadget-workflows',
69+
'sync-ref.yml',
70+
'main', {
71+
ref: req.body.ref
72+
}
73+
)
74+
context.res = { body: `push(${req.body.ref}): triggered ${run.html_url}` }
75+
}
12276
} else if (eventType === 'issue_comment') {
12377
const triggerToken = process.env['GITGITGADGET_TRIGGER_TOKEN'];
12478
if (!triggerToken) {
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
const https = require('https');
2+
3+
const triggerAzurePipeline = async (token, organization, project, buildDefinitionId, sourceBranch, parameters) => {
4+
const auth = Buffer.from('PAT:' + token).toString('base64');
5+
const headers = {
6+
'Accept': 'application/json; api-version=5.0-preview.5; excludeUrls=true',
7+
'Authorization': 'Basic ' + auth,
8+
};
9+
const json = JSON.stringify({
10+
'definition': { 'id': buildDefinitionId },
11+
'sourceBranch': sourceBranch,
12+
'parameters': JSON.stringify(parameters),
13+
});
14+
headers['Content-Type'] = 'application/json';
15+
headers['Content-Length'] = Buffer.byteLength(json);
16+
17+
const requestOptions = {
18+
host: 'dev.azure.com',
19+
port: '443',
20+
path: `/${organization}/${project}/_apis/build/builds?ignoreWarnings=false&api-version=5.0-preview.5`,
21+
method: 'POST',
22+
headers: headers
23+
};
24+
25+
return new Promise((resolve, reject) => {
26+
const handleResponse = (res) => {
27+
res.setEncoding('utf8');
28+
var response = '';
29+
res.on('data', (chunk) => {
30+
response += chunk;
31+
});
32+
res.on('end', () => {
33+
resolve(JSON.parse(response));
34+
});
35+
res.on('error', (err) => {
36+
reject(err);
37+
})
38+
};
39+
40+
const request = https.request(requestOptions, handleResponse);
41+
request.write(json);
42+
request.end();
43+
});
44+
}
45+
46+
module.exports = {
47+
triggerAzurePipeline
48+
}

0 commit comments

Comments
 (0)