Skip to content

Commit

Permalink
Merge pull request #2 from gitgitgadget/trigger-sync-ref-workflow-runs
Browse files Browse the repository at this point in the history
Automatically trigger the `sync-ref` workflow runs on pushes to `git/git`
  • Loading branch information
dscho authored Sep 14, 2023
2 parents 4bd5d1b + 7400342 commit 323bb7d
Show file tree
Hide file tree
Showing 14 changed files with 474 additions and 66 deletions.
7 changes: 7 additions & 0 deletions GitGitGadget/gently.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module.exports = (fn, fallback) => {
try {
return fn()
} catch (e) {
return fallback
}
}
14 changes: 14 additions & 0 deletions GitGitGadget/get-installation-access-token.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const getInstallationAccessToken = async (context, installation_id) => {
const { gitHubAPIRequestAsApp } = require('./github-api-request-as-app')
const answer = await gitHubAPIRequestAsApp(
context,
'POST',
`/app/installations/${installation_id}/access_tokens`)
if (answer.error) throw answer.error
if (answer.token) return answer.token
throw new Error(`Unhandled response:\n${JSON.stringify(answer, null, 2)}`)
}

module.exports = {
getInstallationAccessToken
}
14 changes: 14 additions & 0 deletions GitGitGadget/get-installation-id-for-repo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const getInstallationIdForRepo = async (context, owner, repo) => {
const { gitHubAPIRequestAsApp } = require('./github-api-request-as-app')
const answer = await gitHubAPIRequestAsApp(
context,
'GET',
`/repos/${owner}/${repo}/installation`
)
if (answer.error) throw answer.error
return answer.id
}

module.exports = {
getInstallationIdForRepo
}
47 changes: 47 additions & 0 deletions GitGitGadget/github-api-request-as-app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
const gitHubAPIRequestAsApp = async (context, requestMethod, requestPath, body) => {
const header = {
"alg": "RS256",
"typ": "JWT"
}

const now = Math.floor(new Date().getTime() / 1000)

const payload = {
// issued at time, 60 seconds in the past to allow for clock drift
iat: now - 60,
// JWT expiration time (10 minute maximum)
exp: now + (10 * 60),
// GitHub App's identifier
iss: process.env['GITHUB_APP_ID']
}

const toBase64 = (obj) => Buffer.from(JSON.stringify(obj), "utf-8").toString("base64url")
const headerAndPayload = `${toBase64(header)}.${toBase64(payload)}`

const privateKey = process.env['GITHUB_APP_PRIVATE_KEY'].replaceAll('\\n', '\n')

const crypto = require('crypto')
const signer = crypto.createSign("RSA-SHA256")
signer.update(headerAndPayload)
const signature = signer.sign({
key: privateKey
}, "base64url")

const token = `${headerAndPayload}.${signature}`

const { httpsRequest } = require('./https-request')
return await httpsRequest(
context,
null,
requestMethod,
requestPath,
body,
{
Authorization: `Bearer ${token}`,
}
)
}

module.exports = {
gitHubAPIRequestAsApp
}
11 changes: 11 additions & 0 deletions GitGitGadget/github-api-request.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const gitHubAPIRequest = async (context, token, method, requestPath, payload) => {
const { httpsRequest } = require('./https-request')
const headers = token ? { Authorization: `Bearer ${token}` } : null
const answer = await httpsRequest(context, null, method, requestPath, payload, headers)
if (answer.error) throw answer.error
return answer
}

module.exports = {
gitHubAPIRequest
}
88 changes: 88 additions & 0 deletions GitGitGadget/https-request.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
const gently = require('./gently')

const httpsRequest = async (context, hostname, method, requestPath, body, headers) => {
headers = {
'User-Agent': 'GitForWindowsHelper/0.0',
Accept: 'application/json',
...headers || {}
}
if (body) {
if (typeof body === 'object') body = JSON.stringify(body)
headers['Content-Type'] = 'application/json'
headers['Content-Length'] = body.length
}
const options = {
port: 443,
hostname: hostname || 'api.github.com',
method: method || 'GET',
path: requestPath,
headers
}
return new Promise((resolve, reject) => {
try {
const https = require('https')
const req = https.request(options, res => {
res.on('error', e => reject(e))

if (res.statusCode === 204) resolve({
statusCode: res.statusCode,
statusMessage: res.statusMessage,
headers: res.headers
})

const chunks = []
res.on('data', data => chunks.push(data))
res.on('end', () => {
const json = Buffer.concat(chunks).toString('utf-8')
if (res.statusCode > 299) {
reject({
statusCode: res.statusCode,
statusMessage: res.statusMessage,
requestMethod: options.method,
requestPath: options.path,
body: json,
json: gently(() => JSON.parse(json))
})
return
}
try {
resolve(JSON.parse(json))
} catch (e) {
reject(`Invalid JSON: ${json}`)
}
})
})
req.on('error', err => reject(err))
if (body) req.write(body)
req.end()
} catch (e) {
reject(e)
}
})
}

const doesURLReturn404 = async url => {
const match = url.match(/^https:\/\/([^/]+?)(:\d+)?(\/.*)?$/)
if (!match) throw new Error(`Could not parse URL ${url}`)

const https = require('https')
const options = {
method: 'HEAD',
host: match[1],
port: Number.parseInt(match[2] || '443'),
path: match[3] || '/'
}
return new Promise((resolve, reject) => {
https.request(options, res => {
if (res.error) reject(res.error)
else if (res.statusCode === 404) resolve(true)
else if (res.statusCode === 200) resolve(false)
else reject(`Unexpected statusCode: ${res.statusCode}`)
}).end()
})
}

module.exports = {
httpsRequest,
doesURLReturn404
}
84 changes: 19 additions & 65 deletions GitGitGadget/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,73 +8,11 @@
* via the "webHookType", starting with v2, we have to do the payload
* validation "by hand".
*/
const crypto = require('crypto');
const https = require('https');
const { validateGitHubWebHook } = require('./validate-github-webhook');

const validateGitHubWebHook = (context) => {
const secret = process.env['GITHUB_WEBHOOK_SECRET'];
if (!secret) {
throw new Error('Webhook secret not configured');
}
if (context.req.headers['content-type'] !== 'application/json') {
throw new Error('Unexpected content type: ' + context.req.headers['content-type']);
}
const signature = context.req.headers['x-hub-signature-256'];
if (!signature) {
throw new Error('Missing X-Hub-Signature');
}
const sha256 = signature.match(/^sha256=(.*)/);
if (!sha256) {
throw new Error('Unexpected X-Hub-Signature format: ' + signature);
}
const computed = crypto.createHmac('sha256', secret).update(context.req.rawBody).digest('hex');
if (sha256[1] !== computed) {
throw new Error('Incorrect X-Hub-Signature');
}
}
const { triggerAzurePipeline } = require('./trigger-azure-pipeline');

const triggerAzurePipeline = async (token, organization, project, buildDefinitionId, sourceBranch, parameters) => {
const auth = Buffer.from('PAT:' + token).toString('base64');
const headers = {
'Accept': 'application/json; api-version=5.0-preview.5; excludeUrls=true',
'Authorization': 'Basic ' + auth,
};
const json = JSON.stringify({
'definition': { 'id': buildDefinitionId },
'sourceBranch': sourceBranch,
'parameters': JSON.stringify(parameters),
});
headers['Content-Type'] = 'application/json';
headers['Content-Length'] = Buffer.byteLength(json);

const requestOptions = {
host: 'dev.azure.com',
port: '443',
path: `/${organization}/${project}/_apis/build/builds?ignoreWarnings=false&api-version=5.0-preview.5`,
method: 'POST',
headers: headers
};

return new Promise((resolve, reject) => {
const handleResponse = (res) => {
res.setEncoding('utf8');
var response = '';
res.on('data', (chunk) => {
response += chunk;
});
res.on('end', () => {
resolve(JSON.parse(response));
});
res.on('error', (err) => {
reject(err);
})
};

const request = https.request(requestOptions, handleResponse);
request.write(json);
request.end();
});
}
const { triggerWorkflowDispatch } = require('./trigger-workflow-dispatch')

module.exports = async (context, req) => {
try {
Expand Down Expand Up @@ -119,6 +57,22 @@ module.exports = async (context, req) => {
context.res = {
body: `Ignored event type: ${eventType}`,
};
} else if (eventType === 'push') {
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',
'gitgitgadget-workflows',
'sync-ref.yml',
'main', {
ref: req.body.ref
}
)
context.res = { body: `push(${req.body.ref}): triggered ${run.html_url}` }
}
} else if (eventType === 'issue_comment') {
const triggerToken = process.env['GITGITGADGET_TRIGGER_TOKEN'];
if (!triggerToken) {
Expand Down
48 changes: 48 additions & 0 deletions GitGitGadget/trigger-azure-pipeline.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
const https = require('https');

const triggerAzurePipeline = async (token, organization, project, buildDefinitionId, sourceBranch, parameters) => {
const auth = Buffer.from('PAT:' + token).toString('base64');
const headers = {
'Accept': 'application/json; api-version=5.0-preview.5; excludeUrls=true',
'Authorization': 'Basic ' + auth,
};
const json = JSON.stringify({
'definition': { 'id': buildDefinitionId },
'sourceBranch': sourceBranch,
'parameters': JSON.stringify(parameters),
});
headers['Content-Type'] = 'application/json';
headers['Content-Length'] = Buffer.byteLength(json);

const requestOptions = {
host: 'dev.azure.com',
port: '443',
path: `/${organization}/${project}/_apis/build/builds?ignoreWarnings=false&api-version=5.0-preview.5`,
method: 'POST',
headers: headers
};

return new Promise((resolve, reject) => {
const handleResponse = (res) => {
res.setEncoding('utf8');
var response = '';
res.on('data', (chunk) => {
response += chunk;
});
res.on('end', () => {
resolve(JSON.parse(response));
});
res.on('error', (err) => {
reject(err);
})
};

const request = https.request(requestOptions, handleResponse);
request.write(json);
request.end();
});
}

module.exports = {
triggerAzurePipeline
}
Loading

0 comments on commit 323bb7d

Please sign in to comment.