| 
									
										
										
										
											2017-02-06 20:40:28 +02:00
										 |  |  | // Imports
 | 
					
						
							| 
									
										
										
										
											2017-06-27 19:43:02 +03:00
										 |  |  | import * as bodyParser from 'body-parser'; | 
					
						
							| 
									
										
										
										
											2017-02-06 20:40:28 +02:00
										 |  |  | import * as express from 'express'; | 
					
						
							|  |  |  | import * as http from 'http'; | 
					
						
							| 
									
										
										
										
											2018-08-27 13:31:38 +03:00
										 |  |  | import {AddressInfo} from 'net'; | 
					
						
							| 
									
										
										
										
											2018-05-10 13:56:07 +01:00
										 |  |  | import {CircleCiApi} from '../common/circle-ci-api'; | 
					
						
							|  |  |  | import {GithubApi} from '../common/github-api'; | 
					
						
							| 
									
										
										
										
											2017-02-28 21:10:46 +02:00
										 |  |  | import {GithubPullRequests} from '../common/github-pull-requests'; | 
					
						
							| 
									
										
										
										
											2018-05-10 13:56:07 +01:00
										 |  |  | import {GithubTeams} from '../common/github-teams'; | 
					
						
							| 
									
										
										
										
											2018-12-03 23:10:04 +02:00
										 |  |  | import {assert, assertNotMissingOrEmpty, computeShortSha, Logger} from '../common/utils'; | 
					
						
							| 
									
										
										
										
											2017-02-06 20:40:28 +02:00
										 |  |  | import {BuildCreator} from './build-creator'; | 
					
						
							| 
									
										
										
										
											2017-06-19 01:15:07 +03:00
										 |  |  | import {ChangedPrVisibilityEvent, CreatedBuildEvent} from './build-events'; | 
					
						
							| 
									
										
										
										
											2018-05-10 13:56:07 +01:00
										 |  |  | import {BuildRetriever} from './build-retriever'; | 
					
						
							|  |  |  | import {BuildVerifier} from './build-verifier'; | 
					
						
							|  |  |  | import {respondWithError, throwRequestError} from './utils'; | 
					
						
							| 
									
										
										
										
											2017-02-06 20:40:28 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-05-10 13:56:07 +01:00
										 |  |  | const AIO_PREVIEW_JOB = 'aio_preview'; | 
					
						
							| 
									
										
										
										
											2017-02-06 20:40:28 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-02-28 21:10:46 +02:00
										 |  |  | // Interfaces - Types
 | 
					
						
							| 
									
										
										
										
											2018-08-15 13:47:45 +01:00
										 |  |  | export interface PreviewServerConfig { | 
					
						
							| 
									
										
										
										
											2018-05-10 13:56:07 +01:00
										 |  |  |   downloadsDir: string; | 
					
						
							|  |  |  |   downloadSizeLimit: number; | 
					
						
							|  |  |  |   buildArtifactPath: string; | 
					
						
							| 
									
										
										
										
											2017-02-28 21:10:46 +02:00
										 |  |  |   buildsDir: string; | 
					
						
							| 
									
										
										
										
											2017-03-02 00:04:03 +02:00
										 |  |  |   domainName: string; | 
					
						
							| 
									
										
										
										
											2018-05-10 13:56:07 +01:00
										 |  |  |   githubOrg: string; | 
					
						
							|  |  |  |   githubRepo: string; | 
					
						
							| 
									
										
										
										
											2017-02-28 21:10:46 +02:00
										 |  |  |   githubTeamSlugs: string[]; | 
					
						
							| 
									
										
										
										
											2018-05-10 13:56:07 +01:00
										 |  |  |   circleCiToken: string; | 
					
						
							| 
									
										
										
										
											2017-02-28 21:10:46 +02:00
										 |  |  |   githubToken: string; | 
					
						
							| 
									
										
										
										
											2018-05-10 13:56:07 +01:00
										 |  |  |   significantFilesPattern: string; | 
					
						
							| 
									
										
										
										
											2017-06-19 01:15:07 +03:00
										 |  |  |   trustedPrLabel: string; | 
					
						
							| 
									
										
										
										
											2017-02-28 21:10:46 +02:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-08-27 18:07:25 +03:00
										 |  |  | const logger = new Logger('PreviewServer'); | 
					
						
							| 
									
										
										
										
											2018-05-10 13:56:07 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-02-06 20:40:28 +02:00
										 |  |  | // Classes
 | 
					
						
							| 
									
										
										
										
											2018-08-15 13:47:45 +01:00
										 |  |  | export class PreviewServerFactory { | 
					
						
							| 
									
										
										
										
											2017-02-06 20:40:28 +02:00
										 |  |  |   // Methods - Public
 | 
					
						
							| 
									
										
										
										
											2018-08-15 13:47:45 +01:00
										 |  |  |   public static create(cfg: PreviewServerConfig): http.Server { | 
					
						
							| 
									
										
										
										
											2018-05-10 13:56:07 +01:00
										 |  |  |     assertNotMissingOrEmpty('domainName', cfg.domainName); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const circleCiApi = new CircleCiApi(cfg.githubOrg, cfg.githubRepo, cfg.circleCiToken); | 
					
						
							|  |  |  |     const githubApi = new GithubApi(cfg.githubToken); | 
					
						
							|  |  |  |     const prs = new GithubPullRequests(githubApi, cfg.githubOrg, cfg.githubRepo); | 
					
						
							|  |  |  |     const teams = new GithubTeams(githubApi, cfg.githubOrg); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const buildRetriever = new BuildRetriever(circleCiApi, cfg.downloadSizeLimit, cfg.downloadsDir); | 
					
						
							|  |  |  |     const buildVerifier = new BuildVerifier(prs, teams, cfg.githubTeamSlugs, cfg.trustedPrLabel); | 
					
						
							| 
									
										
										
										
											2018-08-15 13:47:45 +01:00
										 |  |  |     const buildCreator = PreviewServerFactory.createBuildCreator(prs, cfg.buildsDir, cfg.domainName); | 
					
						
							| 
									
										
										
										
											2018-05-10 13:56:07 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-08-15 13:47:45 +01:00
										 |  |  |     const middleware = PreviewServerFactory.createMiddleware(buildRetriever, buildVerifier, buildCreator, cfg); | 
					
						
							| 
									
										
										
										
											2017-06-17 21:03:10 +03:00
										 |  |  |     const httpServer = http.createServer(middleware as any); | 
					
						
							| 
									
										
										
										
											2017-02-06 20:40:28 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     httpServer.on('listening', () => { | 
					
						
							| 
									
										
										
										
											2018-08-27 13:31:38 +03:00
										 |  |  |       const info = httpServer.address() as AddressInfo; | 
					
						
							| 
									
										
										
										
											2018-05-10 13:56:07 +01:00
										 |  |  |       logger.info(`Up and running (and listening on ${info.address}:${info.port})...`); | 
					
						
							| 
									
										
										
										
											2017-02-06 20:40:28 +02:00
										 |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return httpServer; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-05-10 13:56:07 +01:00
										 |  |  |   public static createMiddleware(buildRetriever: BuildRetriever, buildVerifier: BuildVerifier, | 
					
						
							| 
									
										
										
										
											2018-08-15 13:47:45 +01:00
										 |  |  |                                  buildCreator: BuildCreator, cfg: PreviewServerConfig): express.Express { | 
					
						
							| 
									
										
										
										
											2018-05-10 13:56:07 +01:00
										 |  |  |     const middleware = express(); | 
					
						
							|  |  |  |     const jsonParser = bodyParser.json(); | 
					
						
							| 
									
										
										
										
											2018-08-26 00:29:14 +03:00
										 |  |  |     const significantFilesRe = new RegExp(cfg.significantFilesPattern); | 
					
						
							| 
									
										
										
										
											2018-05-10 13:56:07 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |     // RESPOND TO IS-ALIVE PING
 | 
					
						
							|  |  |  |     middleware.get(/^\/health-check\/?$/, (_req, res) => res.sendStatus(200)); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-08-26 00:29:14 +03:00
										 |  |  |     // RESPOND TO CAN-HAVE-PUBLIC-PREVIEW CHECK
 | 
					
						
							|  |  |  |     const canHavePublicPreviewRe = /^\/can-have-public-preview\/(\d+)\/?$/; | 
					
						
							|  |  |  |     middleware.get(canHavePublicPreviewRe, async (req, res) => { | 
					
						
							|  |  |  |       try { | 
					
						
							|  |  |  |         const pr = +canHavePublicPreviewRe.exec(req.url)![1]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if (!await buildVerifier.getSignificantFilesChanged(pr, significantFilesRe)) { | 
					
						
							|  |  |  |           // Cannot have preview: PR did not touch relevant files: `aio/` or `packages/` (except for spec files).
 | 
					
						
							|  |  |  |           res.send({canHavePublicPreview: false, reason: 'No significant files touched.'}); | 
					
						
							|  |  |  |           logger.log(`PR:${pr} - Cannot have a public preview, because it did not touch any significant files.`); | 
					
						
							|  |  |  |         } else if (!await buildVerifier.getPrIsTrusted(pr)) { | 
					
						
							|  |  |  |           // Cannot have preview: PR not automatically verifiable as "trusted".
 | 
					
						
							|  |  |  |           res.send({canHavePublicPreview: false, reason: 'Not automatically verifiable as "trusted".'}); | 
					
						
							|  |  |  |           logger.log(`PR:${pr} - Cannot have a public preview, because not automatically verifiable as "trusted".`); | 
					
						
							|  |  |  |         } else { | 
					
						
							|  |  |  |           // Can have preview.
 | 
					
						
							|  |  |  |           res.send({canHavePublicPreview: true, reason: null}); | 
					
						
							|  |  |  |           logger.log(`PR:${pr} - Can have a public preview.`); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       } catch (err) { | 
					
						
							|  |  |  |         logger.error('Previewability check error', err); | 
					
						
							|  |  |  |         respondWithError(res, err); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-05-10 13:56:07 +01:00
										 |  |  |     // CIRCLE_CI BUILD COMPLETE WEBHOOK
 | 
					
						
							|  |  |  |     middleware.post(/^\/circle-build\/?$/, jsonParser, async (req, res) => { | 
					
						
							|  |  |  |       try { | 
					
						
							|  |  |  |         if (!( | 
					
						
							|  |  |  |           req.is('json') && | 
					
						
							|  |  |  |           req.body && | 
					
						
							|  |  |  |           req.body.payload && | 
					
						
							|  |  |  |           req.body.payload.build_num > 0 && | 
					
						
							|  |  |  |           req.body.payload.build_parameters && | 
					
						
							|  |  |  |           req.body.payload.build_parameters.CIRCLE_JOB | 
					
						
							|  |  |  |         )) { | 
					
						
							|  |  |  |           throwRequestError(400, `Incorrect body content. Expected JSON`, req); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         const job = req.body.payload.build_parameters.CIRCLE_JOB; | 
					
						
							|  |  |  |         const buildNum = req.body.payload.build_num; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         logger.log(`Build:${buildNum}, Job:${job} - processing web-hook trigger`); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if (job !== AIO_PREVIEW_JOB) { | 
					
						
							|  |  |  |           res.sendStatus(204); | 
					
						
							|  |  |  |           logger.log(`Build:${buildNum}, Job:${job} -`, | 
					
						
							|  |  |  |                      `Skipping preview processing because this is not the "${AIO_PREVIEW_JOB}" job.`); | 
					
						
							|  |  |  |           return; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         const { pr, sha, org, repo, success } = await buildRetriever.getGithubInfo(buildNum); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if (!success) { | 
					
						
							|  |  |  |           res.sendStatus(204); | 
					
						
							|  |  |  |           logger.log(`PR:${pr}, Build:${buildNum} - Skipping preview processing because this build did not succeed.`); | 
					
						
							|  |  |  |           return; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         assert(cfg.githubOrg === org, | 
					
						
							|  |  |  |           `Invalid webhook: expected "githubOrg" property to equal "${cfg.githubOrg}" but got "${org}".`); | 
					
						
							|  |  |  |         assert(cfg.githubRepo === repo, | 
					
						
							|  |  |  |           `Invalid webhook: expected "githubRepo" property to equal "${cfg.githubRepo}" but got "${repo}".`); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // Do not deploy unless this PR has touched relevant files: `aio/` or `packages/` (except for spec files)
 | 
					
						
							| 
									
										
										
										
											2018-08-26 00:29:14 +03:00
										 |  |  |         if (!await buildVerifier.getSignificantFilesChanged(pr, significantFilesRe)) { | 
					
						
							| 
									
										
										
										
											2018-05-10 13:56:07 +01:00
										 |  |  |           res.sendStatus(204); | 
					
						
							|  |  |  |           logger.log(`PR:${pr}, Build:${buildNum} - ` + | 
					
						
							|  |  |  |                      `Skipping preview processing because this PR did not touch any significant files.`); | 
					
						
							|  |  |  |           return; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         const artifactPath = await buildRetriever.downloadBuildArtifact(buildNum, pr, sha, cfg.buildArtifactPath); | 
					
						
							|  |  |  |         const isPublic = await buildVerifier.getPrIsTrusted(pr); | 
					
						
							|  |  |  |         await buildCreator.create(pr, sha, artifactPath, isPublic); | 
					
						
							| 
									
										
										
										
											2018-12-03 23:10:04 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-05-10 13:56:07 +01:00
										 |  |  |         res.sendStatus(isPublic ? 201 : 202); | 
					
						
							| 
									
										
										
										
											2018-12-03 23:10:04 +02:00
										 |  |  |         logger.log(`PR:${pr}, SHA:${computeShortSha(sha)}, Build:${buildNum} - ` + | 
					
						
							|  |  |  |                    `Successfully created ${isPublic ? 'public' : 'non-public'} preview.`); | 
					
						
							| 
									
										
										
										
											2018-05-10 13:56:07 +01:00
										 |  |  |       } catch (err) { | 
					
						
							|  |  |  |         logger.error('CircleCI webhook error', err); | 
					
						
							|  |  |  |         respondWithError(res, err); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // GITHUB PR UPDATED WEBHOOK
 | 
					
						
							|  |  |  |     middleware.post(/^\/pr-updated\/?$/, jsonParser, async (req, res) => { | 
					
						
							|  |  |  |       const { action, number: prNo }: { action?: string, number?: number } = req.body; | 
					
						
							|  |  |  |       const visMayHaveChanged = !action || (action === 'labeled') || (action === 'unlabeled'); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       try { | 
					
						
							|  |  |  |         if (!visMayHaveChanged) { | 
					
						
							|  |  |  |           res.sendStatus(200); | 
					
						
							|  |  |  |         } else if (!prNo) { | 
					
						
							|  |  |  |           throwRequestError(400, `Missing or empty 'number' field`, req); | 
					
						
							|  |  |  |         } else { | 
					
						
							|  |  |  |           const isPublic = await buildVerifier.getPrIsTrusted(prNo); | 
					
						
							|  |  |  |           await buildCreator.updatePrVisibility(prNo, isPublic); | 
					
						
							|  |  |  |           res.sendStatus(200); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       } catch (err) { | 
					
						
							|  |  |  |         logger.error('PR update hook error', err); | 
					
						
							|  |  |  |         respondWithError(res, err); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // ALL OTHER REQUESTS
 | 
					
						
							|  |  |  |     middleware.all('*', req => throwRequestError(404, 'Unknown resource', req)); | 
					
						
							|  |  |  |     middleware.use((err: any, _req: any, res: express.Response, _next: any) => { | 
					
						
							|  |  |  |       const statusText = http.STATUS_CODES[err.status] || '???'; | 
					
						
							| 
									
										
										
										
											2018-08-15 13:47:45 +01:00
										 |  |  |       logger.error(`Preview server error: ${err.status} - ${statusText}:`, err.message); | 
					
						
							| 
									
										
										
										
											2018-05-10 13:56:07 +01:00
										 |  |  |       respondWithError(res, err); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return middleware; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-08-10 11:10:22 +01:00
										 |  |  |   public static createBuildCreator(prs: GithubPullRequests, buildsDir: string, domainName: string): BuildCreator { | 
					
						
							| 
									
										
										
										
											2017-02-28 21:10:46 +02:00
										 |  |  |     const buildCreator = new BuildCreator(buildsDir); | 
					
						
							| 
									
										
										
										
											2017-06-19 01:15:07 +03:00
										 |  |  |     const postPreviewsComment = (pr: number, shas: string[]) => { | 
					
						
							|  |  |  |       const body = shas. | 
					
						
							|  |  |  |         map(sha => `You can preview ${sha} at https://pr${pr}-${sha}.${domainName}/.`). | 
					
						
							|  |  |  |         join('\n'); | 
					
						
							| 
									
										
										
										
											2017-02-28 21:10:46 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-05-10 13:56:07 +01:00
										 |  |  |       return prs.addComment(pr, body); | 
					
						
							| 
									
										
										
										
											2017-06-19 01:15:07 +03:00
										 |  |  |     }; | 
					
						
							| 
									
										
										
										
											2017-02-28 21:10:46 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-06-19 01:15:07 +03:00
										 |  |  |     buildCreator.on(CreatedBuildEvent.type, ({pr, sha, isPublic}: CreatedBuildEvent) => { | 
					
						
							|  |  |  |       if (isPublic) { | 
					
						
							|  |  |  |         postPreviewsComment(pr, [sha]); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     buildCreator.on(ChangedPrVisibilityEvent.type, ({pr, shas, isPublic}: ChangedPrVisibilityEvent) => { | 
					
						
							|  |  |  |       if (isPublic && shas.length) { | 
					
						
							|  |  |  |         postPreviewsComment(pr, shas); | 
					
						
							|  |  |  |       } | 
					
						
							| 
									
										
										
										
											2017-02-28 21:10:46 +02:00
										 |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return buildCreator; | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2017-02-06 20:40:28 +02:00
										 |  |  | } |