Skip to content

Commit

Permalink
Merge pull request #461 from rvermeulen/rvermeulen/add-webhook-handler
Browse files Browse the repository at this point in the history
Add webhook handler function
  • Loading branch information
rvermeulen authored Nov 12, 2024
2 parents afa7eab + 60c48e6 commit 9fb76d9
Showing 1 changed file with 229 additions and 0 deletions.
229 changes: 229 additions & 0 deletions scripts/release/webhook-handler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
/**
* This function should be installed as an Azure Function with a HTTP trigger and configured as a GitHub webhook.
* It expects the following environment variables to be set:
* - GITHUB_APP_ID: the ID of the GitHub App used to authenticate
* - GITHUB_APP_INSTALLATION_ID: the ID of the GitHub App installation
* - GITHUB_APP_PRIVATE_KEY: the private key of the GitHub App
* - GITHUB_WEBHOOK_SECRET: the secret used to sign the webhook
* - GITHUB_WORKFLOW_ID: the ID of the workflow to trigger, this should be the id of the workflow `update-release-status.yml`
*/
const crypto = require('crypto');
const { Buffer } = require('buffer');
const https = require('https');

function encode(obj) {
return Buffer.from(JSON.stringify(obj)).toString('base64url');
}

function createJwtToken() {

const signingKey = crypto.createPrivateKey(Buffer.from(process.env['GITHUB_APP_PRIVATE_KEY'], 'base64'));

const claims = {
// Issue 60 seconds in the past to account for clock drift.
iat: Math.floor(Date.now() / 1000) - 60,
// The token is valid for 1 minute(s).
exp: Math.floor(Date.now() / 1000) + (1 * 60),
iss: process.env["GITHUB_APP_ID"]
};

const header = {
alg: "RS256",
typ: "JWT"
};

const payload = `${encode(header)}.${encode(claims)}`;
const signer = crypto.createSign('RSA-SHA256');
const signature = (signer.update(payload), signer.sign(signingKey, 'base64url'));

return `${payload}.${signature}`;
}

function createAccessToken(context) {
return new Promise((resolve, reject) => {
const options = {
hostname: 'api.github.com',
path: `/app/installations/${process.env["GITHUB_APP_INSTALLATION_ID"]}/access_tokens`,
method: 'POST'
};

const req = https.request(options, (res) => {
res.on('data', (data) => {
const body = JSON.parse(data.toString('utf8'));
access_token = body.token;
//context.log(access_token);
resolve(access_token);
});

res.on('error', (error) => {
reject(error);
})
});

req.setHeader('Accept', 'application/vnd.github+json');
const token = createJwtToken();
//context.log(`JWT Token ${token}`);
req.setHeader('Authorization', `Bearer ${token}`);
req.setHeader('X-GitHub-Api-Version', '2022-11-28');
req.setHeader('User-Agent', 'CodeQL Coding Standards Automation');

req.end();
});
}

function triggerReleaseUpdate(context, access_token, head_sha) {
context.log(`Triggering release update for head sha ${head_sha}`)
return new Promise((resolve, reject) => {
const options = {
hostname: 'api.github.com',
path: `/repos/github/codeql-coding-standards/actions/workflows/${process.env["GITHUB_WORKFLOW_ID"]}/dispatches`,
method: 'POST'
};

const req = https.request(options, (res) => {
res.on('error', (error) => {
reject(error);
})
});

req.setHeader('Accept', 'application/vnd.github+json');
req.setHeader('Authorization', `Bearer ${access_token}`);
req.setHeader('X-GitHub-Api-Version', '2022-11-28');
req.setHeader('User-Agent', 'CodeQL Coding Standards Automation');

const params = {
ref: 'main',
inputs: {
"head-sha": head_sha
}
};
req.on('response', (response) => {
context.log(`Received status code ${response.statusCode} with message ${response.statusMessage}`);
resolve();
});
req.end(JSON.stringify(params));
});
}

function listCheckRunsForRefPerPage(context, access_token, ref, page = 1) {
context.log(`Listing check runs for ${ref}`)
return new Promise((resolve, reject) => {
const options = {
hostname: 'api.github.com',
path: `/repos/github/codeql-coding-standards/commits/${ref}/check-runs?page=${page}&per_page=100`,
method: 'GET',
headers: {
'Accept': 'application/vnd.github+json',
'Authorization': `Bearer ${access_token}`,
'X-GitHub-Api-Version': '2022-11-28',
'User-Agent': 'CodeQL Coding Standards Automation'
}
};

const req = https.request(options, (res) => {
if (res.statusCode != 200) {
reject(`Received status code ${res.statusCode} with message ${res.statusMessage}`);
} else {
var body = [];
res.on('data', (chunk) => {
body.push(chunk);
});
res.on('end', () => {
try {
body = JSON.parse(Buffer.concat(body).toString('utf8'));
resolve(body);
} catch (error) {
reject(error);
}
});
}
});
req.on('error', (error) => {
reject(error);
});

req.end();
});
}

async function listCheckRunsForRef(context, access_token, ref) {
let page = 1;
let check_runs = [];
const first_page = await listCheckRunsForRefPerPage(context, access_token, ref, page);
check_runs = check_runs.concat(first_page.check_runs);
while (first_page.total_count > check_runs.length) {
page++;
const next_page = await listCheckRunsForRefPerPage(context, access_token, ref, page);
check_runs = check_runs.concat(next_page.check_runs);
}
return check_runs;
}

function hasReleaseStatusCheckRun(check_runs) {
return check_runs.some(check_run => check_run.name == 'release-status');
}

function isValidSignature(req) {
const hmac = crypto.createHmac("sha256", process.env["GITHUB_WEBHOOK_SECRET"]);
const signature = hmac.update(JSON.stringify(req.body)).digest('hex');
const shaSignature = `sha256=${signature}`;
const gitHubSignature = req.headers['x-hub-signature-256'];

return !shaSignature.localeCompare(gitHubSignature);
}

module.exports = async function (context, req) {
context.log('Webhook received.');

if (isValidSignature(req)) {
const event = req.headers['x-github-event'];

if (event == 'check_run') {
webhook = req.body;

// To avoid infinite loops, we skip triggering the workflow for the following checkruns.
const check_runs_to_skip = [
// check run created by manual dispatch of Update Release workflow
'Update release',
// check runs created by job in Update release status workflow
'update-release',
// when update-release calls reusable workflow Update release
'update-release / Update release',
'validate-check-runs',
// check run that validates the whole release
'release-status'];
const update_release_actions = ['completed', 'rerequested'];

if (update_release_actions.includes(webhook.action) && !check_runs_to_skip.includes(webhook.check_run.name)) {
context.log(`Triggering update release status because ${webhook.check_run.name} received action ${webhook.action}`);

try {
const access_token = await createAccessToken(context);
const check_runs = await listCheckRunsForRef(context, access_token, webhook.check_run.head_sha);
if (hasReleaseStatusCheckRun(check_runs)) {
context.log(`Release status check run found for ${webhook.check_run.head_sha}`);
await triggerReleaseUpdate(context, access_token, webhook.check_run.head_sha);
} else {
context.log(`Skippping, no release status check run found for ${webhook.check_run.head_sha}`);
}
} catch (error) {
context.log(`Failed with error: ${error}`);
}
} else {
context.log(`Skipping action ${webhook.action} for ${webhook.check_run.name}`)
}
} else {
context.log(`Skipping event: ${event}`)
}

context.res = {
status: 200
};
} else {
context.log('Received invalid GitHub signature')
context.res = {
status: 401,
body: 'Invalid x-hub-signature-256 value'
};
}
}

0 comments on commit 9fb76d9

Please sign in to comment.