1+ /**
2+ * This function should be installed as an Azure Function with a HTTP trigger and configured as a GitHub webhook.
3+ * It expects the following environment variables to be set:
4+ * - GITHUB_APP_ID: the ID of the GitHub App used to authenticate
5+ * - GITHUB_APP_INSTALLATION_ID: the ID of the GitHub App installation
6+ * - GITHUB_APP_PRIVATE_KEY: the private key of the GitHub App
7+ * - GITHUB_WEBHOOK_SECRET: the secret used to sign the webhook
8+ * - GITHUB_WORKFLOW_ID: the ID of the workflow to trigger, this should be the id of the workflow `update-release-status.yml`
9+ */
10+ const crypto = require ( 'crypto' ) ;
11+ const { Buffer } = require ( 'buffer' ) ;
12+ const https = require ( 'https' ) ;
13+
14+ function encode ( obj ) {
15+ return Buffer . from ( JSON . stringify ( obj ) ) . toString ( 'base64url' ) ;
16+ }
17+
18+ function createJwtToken ( ) {
19+
20+ const signingKey = crypto . createPrivateKey ( Buffer . from ( process . env [ 'GITHUB_APP_PRIVATE_KEY' ] , 'base64' ) ) ;
21+
22+ const claims = {
23+ // Issue 60 seconds in the past to account for clock drift.
24+ iat : Math . floor ( Date . now ( ) / 1000 ) - 60 ,
25+ // The token is valid for 1 minute(s).
26+ exp : Math . floor ( Date . now ( ) / 1000 ) + ( 1 * 60 ) ,
27+ iss : process . env [ "GITHUB_APP_ID" ]
28+ } ;
29+
30+ const header = {
31+ alg : "RS256" ,
32+ typ : "JWT"
33+ } ;
34+
35+ const payload = `${ encode ( header ) } .${ encode ( claims ) } ` ;
36+ const signer = crypto . createSign ( 'RSA-SHA256' ) ;
37+ const signature = ( signer . update ( payload ) , signer . sign ( signingKey , 'base64url' ) ) ;
38+
39+ return `${ payload } .${ signature } ` ;
40+ }
41+
42+ function createAccessToken ( context ) {
43+ return new Promise ( ( resolve , reject ) => {
44+ const options = {
45+ hostname : 'api.github.com' ,
46+ path : `/app/installations/${ process . env [ "GITHUB_APP_INSTALLATION_ID" ] } /access_tokens` ,
47+ method : 'POST'
48+ } ;
49+
50+ const req = https . request ( options , ( res ) => {
51+ res . on ( 'data' , ( data ) => {
52+ const body = JSON . parse ( data . toString ( 'utf8' ) ) ;
53+ access_token = body . token ;
54+ //context.log(access_token);
55+ resolve ( access_token ) ;
56+ } ) ;
57+
58+ res . on ( 'error' , ( error ) => {
59+ reject ( error ) ;
60+ } )
61+ } ) ;
62+
63+ req . setHeader ( 'Accept' , 'application/vnd.github+json' ) ;
64+ const token = createJwtToken ( ) ;
65+ //context.log(`JWT Token ${token}`);
66+ req . setHeader ( 'Authorization' , `Bearer ${ token } ` ) ;
67+ req . setHeader ( 'X-GitHub-Api-Version' , '2022-11-28' ) ;
68+ req . setHeader ( 'User-Agent' , 'CodeQL Coding Standards Automation' ) ;
69+
70+ req . end ( ) ;
71+ } ) ;
72+ }
73+
74+ function triggerReleaseUpdate ( context , access_token , head_sha ) {
75+ context . log ( `Triggering release update for head sha ${ head_sha } ` )
76+ return new Promise ( ( resolve , reject ) => {
77+ const options = {
78+ hostname : 'api.github.com' ,
79+ path : `/repos/github/codeql-coding-standards/actions/workflows/${ process . env [ "GITHUB_WORKFLOW_ID" ] } /dispatches` ,
80+ method : 'POST'
81+ } ;
82+
83+ const req = https . request ( options , ( res ) => {
84+ res . on ( 'error' , ( error ) => {
85+ reject ( error ) ;
86+ } )
87+ } ) ;
88+
89+ req . setHeader ( 'Accept' , 'application/vnd.github+json' ) ;
90+ req . setHeader ( 'Authorization' , `Bearer ${ access_token } ` ) ;
91+ req . setHeader ( 'X-GitHub-Api-Version' , '2022-11-28' ) ;
92+ req . setHeader ( 'User-Agent' , 'CodeQL Coding Standards Automation' ) ;
93+
94+ const params = {
95+ ref : 'main' ,
96+ inputs : {
97+ "head-sha" : head_sha
98+ }
99+ } ;
100+ req . on ( 'response' , ( response ) => {
101+ context . log ( `Received status code ${ response . statusCode } with message ${ response . statusMessage } ` ) ;
102+ resolve ( ) ;
103+ } ) ;
104+ req . end ( JSON . stringify ( params ) ) ;
105+ } ) ;
106+ }
107+
108+ function listCheckRunsForRefPerPage ( context , access_token , ref , page = 1 ) {
109+ context . log ( `Listing check runs for ${ ref } ` )
110+ return new Promise ( ( resolve , reject ) => {
111+ const options = {
112+ hostname : 'api.github.com' ,
113+ path : `/repos/github/codeql-coding-standards/commits/${ ref } /check-runs?page=${ page } &per_page=100` ,
114+ method : 'GET' ,
115+ headers : {
116+ 'Accept' : 'application/vnd.github+json' ,
117+ 'Authorization' : `Bearer ${ access_token } ` ,
118+ 'X-GitHub-Api-Version' : '2022-11-28' ,
119+ 'User-Agent' : 'CodeQL Coding Standards Automation'
120+ }
121+ } ;
122+
123+ const req = https . request ( options , ( res ) => {
124+ if ( res . statusCode != 200 ) {
125+ reject ( `Received status code ${ res . statusCode } with message ${ res . statusMessage } ` ) ;
126+ } else {
127+ var body = [ ] ;
128+ res . on ( 'data' , ( chunk ) => {
129+ body . push ( chunk ) ;
130+ } ) ;
131+ res . on ( 'end' , ( ) => {
132+ try {
133+ body = JSON . parse ( Buffer . concat ( body ) . toString ( 'utf8' ) ) ;
134+ resolve ( body ) ;
135+ } catch ( error ) {
136+ reject ( error ) ;
137+ }
138+ } ) ;
139+ }
140+ } ) ;
141+ req . on ( 'error' , ( error ) => {
142+ reject ( error ) ;
143+ } ) ;
144+
145+ req . end ( ) ;
146+ } ) ;
147+ }
148+
149+ async function listCheckRunsForRef ( context , access_token , ref ) {
150+ let page = 1 ;
151+ let check_runs = [ ] ;
152+ const first_page = await listCheckRunsForRefPerPage ( context , access_token , ref , page ) ;
153+ check_runs = check_runs . concat ( first_page . check_runs ) ;
154+ while ( first_page . total_count > check_runs . length ) {
155+ page ++ ;
156+ const next_page = await listCheckRunsForRefPerPage ( context , access_token , ref , page ) ;
157+ check_runs = check_runs . concat ( next_page . check_runs ) ;
158+ }
159+ return check_runs ;
160+ }
161+
162+ function hasReleaseStatusCheckRun ( check_runs ) {
163+ return check_runs . some ( check_run => check_run . name == 'release-status' ) ;
164+ }
165+
166+ function isValidSignature ( req ) {
167+ const hmac = crypto . createHmac ( "sha256" , process . env [ "GITHUB_WEBHOOK_SECRET" ] ) ;
168+ const signature = hmac . update ( JSON . stringify ( req . body ) ) . digest ( 'hex' ) ;
169+ const shaSignature = `sha256=${ signature } ` ;
170+ const gitHubSignature = req . headers [ 'x-hub-signature-256' ] ;
171+
172+ return ! shaSignature . localeCompare ( gitHubSignature ) ;
173+ }
174+
175+ module . exports = async function ( context , req ) {
176+ context . log ( 'Webhook received.' ) ;
177+
178+ if ( isValidSignature ( req ) ) {
179+ const event = req . headers [ 'x-github-event' ] ;
180+
181+ if ( event == 'check_run' ) {
182+ webhook = req . body ;
183+
184+ // To avoid infinite loops, we skip triggering the workflow for the following checkruns.
185+ const check_runs_to_skip = [
186+ // check run created by manual dispatch of Update Release workflow
187+ 'Update release' ,
188+ // check runs created by job in Update release status workflow
189+ 'update-release' ,
190+ // when update-release calls reusable workflow Update release
191+ 'update-release / Update release' ,
192+ 'validate-check-runs' ,
193+ // check run that validates the whole release
194+ 'release-status' ] ;
195+ const update_release_actions = [ 'completed' , 'rerequested' ] ;
196+
197+ if ( update_release_actions . includes ( webhook . action ) && ! check_runs_to_skip . includes ( webhook . check_run . name ) ) {
198+ context . log ( `Triggering update release status because ${ webhook . check_run . name } received action ${ webhook . action } ` ) ;
199+
200+ try {
201+ const access_token = await createAccessToken ( context ) ;
202+ const check_runs = await listCheckRunsForRef ( context , access_token , webhook . check_run . head_sha ) ;
203+ if ( hasReleaseStatusCheckRun ( check_runs ) ) {
204+ context . log ( `Release status check run found for ${ webhook . check_run . head_sha } ` ) ;
205+ await triggerReleaseUpdate ( context , access_token , webhook . check_run . head_sha ) ;
206+ } else {
207+ context . log ( `Skippping, no release status check run found for ${ webhook . check_run . head_sha } ` ) ;
208+ }
209+ } catch ( error ) {
210+ context . log ( `Failed with error: ${ error } ` ) ;
211+ }
212+ } else {
213+ context . log ( `Skipping action ${ webhook . action } for ${ webhook . check_run . name } ` )
214+ }
215+ } else {
216+ context . log ( `Skipping event: ${ event } ` )
217+ }
218+
219+ context . res = {
220+ status : 200
221+ } ;
222+ } else {
223+ context . log ( 'Received invalid GitHub signature' )
224+ context . res = {
225+ status : 401 ,
226+ body : 'Invalid x-hub-signature-256 value'
227+ } ;
228+ }
229+ }
0 commit comments