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