11import * as c from 'config' ;
2+ import crypto from 'crypto' ;
23import * as mongodb from 'mongodb' ;
34import { IConfig } from 'config' ;
5+ import { APIGatewayEvent , APIGatewayProxyResult } from 'aws-lambda' ;
46import { RepoEntitlementsRepository } from '../../../src/repositories/repoEntitlementsRepository' ;
57import { BranchRepository } from '../../../src/repositories/branchRepository' ;
68import { ConsoleLogger } from '../../../src/services/logger' ;
@@ -13,6 +15,19 @@ import { ECSContainer } from '../../../src/services/containerServices';
1315import { SQSConnector } from '../../../src/services/queue' ;
1416import { Batch } from '../../../src/services/batch' ;
1517
18+ // Although data in payload should always be present, it's not guaranteed from
19+ // external callers
20+ interface SnootyPayload {
21+ jobId ?: string ;
22+ }
23+
24+ // These options should only be defined if the build summary is being called after
25+ // a Gatsby Cloud job
26+ interface BuildSummaryOptions {
27+ mongoClient ?: mongodb . MongoClient ;
28+ previewUrl ?: string ;
29+ }
30+
1631export const TriggerLocalBuild = async ( event : any = { } , context : any = { } ) : Promise < any > => {
1732 const client = new mongodb . MongoClient ( c . get ( 'dbUrl' ) ) ;
1833 await client . connect ( ) ;
@@ -160,9 +175,11 @@ async function retry(message: JobQueueMessage, consoleLogger: ConsoleLogger, url
160175 consoleLogger . error ( message [ 'jobId' ] , err ) ;
161176 }
162177}
163- async function NotifyBuildSummary ( jobId : string ) : Promise < any > {
178+
179+ async function NotifyBuildSummary ( jobId : string , options : BuildSummaryOptions = { } ) : Promise < any > {
180+ const { mongoClient, previewUrl } = options ;
164181 const consoleLogger = new ConsoleLogger ( ) ;
165- const client = new mongodb . MongoClient ( c . get ( 'dbUrl' ) ) ;
182+ const client : mongodb . MongoClient = mongoClient ?? new mongodb . MongoClient ( c . get ( 'dbUrl' ) ) ;
166183 await client . connect ( ) ;
167184 const db = client . db ( c . get ( 'dbName' ) ) ;
168185 const env = c . get < string > ( 'env' ) ;
@@ -187,6 +204,11 @@ async function NotifyBuildSummary(jobId: string): Promise<any> {
187204 const prCommentId = await githubCommenter . getPullRequestCommentId ( fullDocument . payload , pr ) ;
188205 const fullJobDashboardUrl = c . get < string > ( 'dashboardUrl' ) + jobId ;
189206
207+ // We currently avoid posting the Gatsby Cloud preview url on GitHub to avoid
208+ // potentially conflicting behavior with the S3 staging link with parallel
209+ // frontend builds. This is in case the GC build finishing first causes the
210+ // initial comment to be made with a nullish S3 url, while subsequent comment
211+ // updates only append the list of build logs.
190212 if ( prCommentId !== undefined ) {
191213 const ghMessage = prepGithubComment ( fullDocument , fullJobDashboardUrl , true ) ;
192214 await githubCommenter . updateComment ( fullDocument . payload , prCommentId , ghMessage ) ;
@@ -213,7 +235,8 @@ async function NotifyBuildSummary(jobId: string): Promise<any> {
213235 repoName ,
214236 c . get < string > ( 'dashboardUrl' ) ,
215237 jobId ,
216- fullDocument . status == 'failed'
238+ fullDocument . status == 'failed' ,
239+ previewUrl
217240 ) ,
218241 entitlement [ 'slack_user_id' ]
219242 ) ;
@@ -247,22 +270,26 @@ async function prepSummaryMessage(
247270 repoName : string ,
248271 jobUrl : string ,
249272 jobId : string ,
250- failed = false
273+ failed = false ,
274+ previewUrl ?: string
251275) : Promise < string > {
252276 const urls = extractUrlFromMessage ( fullDocument ) ;
253- let mms_urls = [ null , null ] ;
277+ let mms_urls : Array < string | null > = [ null , null ] ;
254278 // mms-docs needs special handling as it builds two sites (cloudmanager & ops manager)
255279 // so we need to extract both URLs
256280 if ( repoName === 'mms-docs' ) {
257281 if ( urls . length >= 2 ) {
258- // TODO: Type 'string[]' is not assignable to type 'null[]'.
259282 mms_urls = urls . slice ( - 2 ) ;
260283 }
261284 }
285+
262286 let url = '' ;
263- if ( urls . length > 0 ) {
287+ if ( previewUrl ) {
288+ url = previewUrl ;
289+ } else if ( urls . length > 0 ) {
264290 url = urls [ urls . length - 1 ] ;
265291 }
292+
266293 let msg = '' ;
267294 if ( failed ) {
268295 msg = `Your Job <${ jobUrl } ${ jobId } |Failed>! Please check the build log for any errors.\n- Repo: *${ repoName } *\n- Branch: *${ fullDocument . payload . branchName } *\n- urlSlug: *${ fullDocument . payload . urlSlug } *\n- Env: *${ env } *\n Check logs for more errors!!\nSorry :disappointed:! ` ;
@@ -385,3 +412,111 @@ async function SubmitArchiveJob(jobId: string) {
385412 consoleLogger . info ( 'submit archive job' , JSON . stringify ( { jobId : jobId , batchJobId : response . jobId } ) ) ;
386413 await client . close ( ) ;
387414}
415+
416+ /**
417+ * Checks the signature payload as a rough validation that the request was made by
418+ * the Snooty frontend.
419+ * @param payload - stringified JSON payload
420+ * @param signature - the Snooty signature included in the header
421+ */
422+ function validateSnootyPayload ( payload : string , signature : string ) {
423+ const secret = c . get < string > ( 'snootySecret' ) ;
424+ const expectedSignature = crypto . createHmac ( 'sha256' , secret ) . update ( payload ) . digest ( 'hex' ) ;
425+ return signature === expectedSignature ;
426+ }
427+
428+ /**
429+ * Performs post-build operations such as notifications and db updates for job ID
430+ * provided in its payload. This is typically expected to only be called by
431+ * Snooty's Gatsby Cloud source plugin.
432+ * @param event
433+ * @returns
434+ */
435+ export async function SnootyBuildComplete ( event : APIGatewayEvent ) : Promise < APIGatewayProxyResult > {
436+ const consoleLogger = new ConsoleLogger ( ) ;
437+ const defaultHeaders = { 'Content-Type' : 'text/plain' } ;
438+
439+ if ( ! event . body ) {
440+ const err = 'SnootyBuildComplete does not have a body in event payload' ;
441+ consoleLogger . error ( 'SnootyBuildCompleteError' , err ) ;
442+ return {
443+ statusCode : 400 ,
444+ headers : defaultHeaders ,
445+ body : err ,
446+ } ;
447+ }
448+
449+ // Keep lowercase in case header is automatically converted to lowercase
450+ // The Snooty frontend should be mindful of using a lowercase header
451+ const snootySignature = event . headers [ 'x-snooty-signature' ] ;
452+ if ( ! snootySignature ) {
453+ const err = 'SnootyBuildComplete does not have a signature in event payload' ;
454+ consoleLogger . error ( 'SnootyBuildCompleteError' , err ) ;
455+ return {
456+ statusCode : 400 ,
457+ headers : defaultHeaders ,
458+ body : err ,
459+ } ;
460+ }
461+
462+ if ( ! validateSnootyPayload ( event . body , snootySignature ) ) {
463+ const errMsg = 'Payload signature is incorrect' ;
464+ consoleLogger . error ( 'SnootyBuildCompleteError' , errMsg ) ;
465+ return {
466+ statusCode : 401 ,
467+ headers : defaultHeaders ,
468+ body : errMsg ,
469+ } ;
470+ }
471+
472+ let payload : SnootyPayload | undefined ;
473+ try {
474+ payload = JSON . parse ( event . body ) as SnootyPayload ;
475+ } catch ( e ) {
476+ const errMsg = 'Payload is not valid JSON' ;
477+ return {
478+ statusCode : 400 ,
479+ headers : defaultHeaders ,
480+ body : errMsg ,
481+ } ;
482+ }
483+
484+ const { jobId } = payload ;
485+ if ( ! jobId ) {
486+ const errMsg = 'Payload missing job ID' ;
487+ consoleLogger . error ( 'SnootyBuildCompleteError' , errMsg ) ;
488+ return {
489+ statusCode : 400 ,
490+ headers : defaultHeaders ,
491+ body : errMsg ,
492+ } ;
493+ }
494+
495+ const client = new mongodb . MongoClient ( c . get ( 'dbUrl' ) ) ;
496+
497+ try {
498+ await client . connect ( ) ;
499+ const db = client . db ( c . get < string > ( 'dbName' ) ) ;
500+ const jobRepository = new JobRepository ( db , c , consoleLogger ) ;
501+ await jobRepository . updateWithCompletionStatus ( jobId , null , false ) ;
502+ // Placeholder preview URL until we iron out the Gatsby Cloud site URLs.
503+ // This would probably involve fetching the URLs in the db on a per project basis
504+ const previewUrl = 'https://www.mongodb.com/docs/' ;
505+ await NotifyBuildSummary ( jobId , { mongoClient : client , previewUrl } ) ;
506+ } catch ( e ) {
507+ consoleLogger . error ( 'SnootyBuildCompleteError' , e ) ;
508+ return {
509+ statusCode : 500 ,
510+ headers : defaultHeaders ,
511+ body : e ,
512+ } ;
513+ } finally {
514+ await client . close ( ) ;
515+ }
516+
517+ return {
518+ statusCode : 200 ,
519+ headers : defaultHeaders ,
520+ body : `Snooty build ${ jobId } completed` ,
521+ } ;
522+ }
0 commit comments