UNPKG

pr-view

Version:

Preview deployments for your pull requests

494 lines 20.3 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; result["default"] = mod; return result; }; Object.defineProperty(exports, "__esModule", { value: true }); const config_1 = __importDefault(require("../util/config")); const fs = __importStar(require("fs")); const child_process_1 = require("child_process"); const AWS = require("aws-sdk"); const workflow_1 = require("../util/workflow"); class Deployer { constructor(prViewConfig) { this.prViewConfig = prViewConfig; if (process.env.BRANCH_NAME) { this.branchName = process.env.BRANCH_NAME; } else { console.error("BRANCH_NAME environment variable must be set."); process.exit(1); } } printConfiguration() { console.log("Framework: " + this.prViewConfig.framework); console.log("Memory Size (MB): " + this.prViewConfig.memorySize); console.log("AppName: " + this.prViewConfig.appName); } } exports.Deployer = Deployer; Deployer.PREFIX = "pr-view"; class NextJsServerlessDeployer extends Deployer { constructor(prViewConfig) { super(prViewConfig); let envError = false; if (process.env.AWS_REGION) { this.awsRegion = process.env.AWS_REGION; } else { console.error("AWS_REGION environment variable must be set."); envError = true; } if (!process.env.AWS_ACCESS_KEY_ID) { console.error("AWS_ACCESS_KEY_ID environment variable must be set."); envError = true; } if (!process.env.AWS_SECRET_ACCESS_KEY) { console.error("AWS_SECRET_ACCESS_KEY environment variable must be set."); envError = true; } if (!process.env.BUCKET_PREFIX) { console.error("BUCKET_PREFIX environment variable must be set."); envError = true; } if (envError) { process.exit(1); } AWS.config.update({ region: this.awsRegion, }); this.s3 = new AWS.S3(); this.dynamoDB = new AWS.DynamoDB(); this.documentClient = new AWS.DynamoDB.DocumentClient(); this.deploymentToUse = ""; } getDeploymentUrl() { return this.readCloudFrontUrl(); } readCloudFrontUrl() { let data; try { data = fs.readFileSync(`.serverless/Template.${this.prViewConfig.appName}.CloudFront.json`); } catch (err) { if (err.code === "ENOENT") { console.error("CloudFront JSON file could not be found."); return null; } else { console.error("Error reading CloudFront JSON file."); return null; } } try { const cloudfront = JSON.parse(data.toString()); return cloudfront["url"]; } catch (err) { return null; } } readCloudFrontId() { let data; try { data = fs.readFileSync(`.serverless/Template.${this.prViewConfig.appName}.CloudFront.json`); } catch (err) { if (err.code === "ENOENT") { console.error("CloudFront JSON file could not be found."); return null; } else { console.error("Error reading CloudFront JSON file."); return null; } } try { const cloudfront = JSON.parse(data.toString()); return cloudfront["id"]; } catch (err) { return null; } } createPrViewTable() { return __awaiter(this, void 0, void 0, function* () { const createTableInput = { AttributeDefinitions: [ { AttributeName: "Name", AttributeType: "S", }, ], KeySchema: [ { AttributeName: "Name", KeyType: "HASH", }, ], ProvisionedThroughput: { ReadCapacityUnits: 1, WriteCapacityUnits: 1, }, TableName: "PrView", }; yield this.dynamoDB.createTable(createTableInput, (r) => r).promise(); }); } getPrViewDeploymentStateItem() { return __awaiter(this, void 0, void 0, function* () { const getInput = { TableName: "PrView", Key: { Name: "PrViewDeploymentState", }, ConsistentRead: true, }; let getResponse = yield this.documentClient .get(getInput, (r) => r) .promise(); try { let result = getResponse.$response.httpResponse.body.toString(); let exists = result !== "{}"; if (!exists) { console.error("Item does not exist. Creating item for first time."); const putInput = { TableName: "PrView", Item: { Name: "PrViewDeploymentState", DeploymentState: "{}", }, }; yield this.documentClient.put(putInput, (r) => r).promise(); getResponse = yield this.documentClient .get(getInput, (r) => r) .promise(); } result = getResponse.$response.httpResponse.body.toString(); exists = result !== "{}"; if (!exists) { console.error("Could not retrieve PrViewDeploymentState. Please try again."); process.exit(1); } else { if (getResponse.Item) { console.log("Retrieved PR view item successfully."); return getResponse.Item; } else { console.error("Item is somehow undefined. Please try again."); process.exit(1); } } } catch (err) { console.error("Error: " + err.toString()); process.exit(1); } }); } updatePrViewDeploymentState(deploymentStateItem) { return __awaiter(this, void 0, void 0, function* () { const updateInput = { TableName: "PrView", Item: deploymentStateItem, Key: { Name: "PrViewDeploymentState", }, }; try { yield this.documentClient.put(updateInput, (r) => r).promise(); console.log("Updated PrViewDeploymentState successfully."); } catch (err) { console.error("Error updating PrViewDeploymentState: " + err.toString()); process.exit(1); } }); } generateDeployUrl() { return ("https://" + this.generateSubdomain() + "." + this.prViewConfig.domain); } getBucketPrefix() { const prefix = process.env.BUCKET_PREFIX; if (!prefix) { console.info("BUCKET_PREFIX must be set."); process.exit(1); } return prefix; } getMetaDataBucketName() { return this.getBucketPrefix() + "-deployments"; } retrieveExistingServerlessDeploymentName() { return __awaiter(this, void 0, void 0, function* () { const prViewDeploymentStateItem = yield this.getPrViewDeploymentStateItem(); const deploymentState = JSON.parse(prViewDeploymentStateItem.DeploymentState); let existingDeploymentName = null; for (const key of Object.keys(deploymentState)) { const value = deploymentState[key]; if (value.branchName === process.env.BRANCH_NAME) { existingDeploymentName = key; } } if (existingDeploymentName) { return existingDeploymentName; } else { console.info("No existing deployment name, nothing to do."); process.exit(0); } }); } markDeploymentNameAsFree() { return __awaiter(this, void 0, void 0, function* () { const prViewDeploymentStateItem = yield this.getPrViewDeploymentStateItem(); const deploymentState = JSON.parse(prViewDeploymentStateItem.DeploymentState); for (const key of Object.keys(deploymentState)) { const value = deploymentState[key]; if (key == this.deploymentToUse) { value.inUse = false; value.prId = null; value.branchName = null; } } prViewDeploymentStateItem.DeploymentState = JSON.stringify(deploymentState); yield this.updatePrViewDeploymentState(prViewDeploymentStateItem); }); } generateServerlessDeploymentName() { return __awaiter(this, void 0, void 0, function* () { const prViewDeploymentStateItem = yield this.getPrViewDeploymentStateItem(); const deploymentState = JSON.parse(prViewDeploymentStateItem.DeploymentState); console.log("Current deployment state: " + prViewDeploymentStateItem.DeploymentState); let deploymentToUse = null; for (const key of Object.keys(deploymentState)) { const value = deploymentState[key]; if (value.branchName === process.env.BRANCH_NAME) { console.log("Found existing deployment for this branch: " + value.toString()); deploymentToUse = key; break; } } let maxDeploymentIndex = 0; if (!deploymentToUse) { for (const key of Object.keys(deploymentState)) { const value = deploymentState[key]; if (!value.inUse) { console.log("Found free deployment: " + value.toString()); deploymentToUse = key; break; } const currentIndex = parseInt(key.replace(this.getBucketPrefix() + "-deployment-", "")); maxDeploymentIndex = Math.max(maxDeploymentIndex, currentIndex); } } if (!deploymentToUse) { console.log("No free deployment to use, creating a new one."); deploymentToUse = this.getBucketPrefix() + "-deployment-" + (maxDeploymentIndex + 1); } deploymentState[deploymentToUse] = { inUse: true, branchName: process.env.BRANCH_NAME, prId: workflow_1.getPrNumberFromWorkflow(), }; prViewDeploymentStateItem.DeploymentState = JSON.stringify(deploymentState); yield this.updatePrViewDeploymentState(prViewDeploymentStateItem); return deploymentToUse; }); } generateSubdomain() { return (Deployer.PREFIX + "-" + this.prViewConfig.appName + "-" + this.branchName); } generateServerlessFile(isDeployment) { return __awaiter(this, void 0, void 0, function* () { console.log("Generating serverless.yml file..."); let serverlessDeploymentName; if (isDeployment) { serverlessDeploymentName = yield this.generateServerlessDeploymentName(); } else { serverlessDeploymentName = yield this.retrieveExistingServerlessDeploymentName(); } const subdomain = this.generateSubdomain(); const bucketName = serverlessDeploymentName; const functionName = serverlessDeploymentName + "-lambda"; this.deploymentToUse = serverlessDeploymentName; const component = this.prViewConfig.component || "@sls-next/serverless-component"; let serverless = ""; if (this.prViewConfig.domain) { console.log("Domain specified, will deploy at: " + this.generateDeployUrl()); serverless += `${this.prViewConfig.appName}:\n`; serverless += ` component: "${component}"\n`; serverless += " inputs:\n"; serverless += ` domain: ["${subdomain}", "${this.prViewConfig.domain}"]\n`; serverless += ` memory: ${this.prViewConfig.memorySize}\n`; serverless += ` bucketName: "${bucketName}"\n`; serverless += ` name: "${functionName}"\n`; } else { serverless += `${this.prViewConfig.appName}:\n`; serverless += ` component: "${component}"\n`; serverless += " inputs:\n"; serverless += ` memory: ${this.prViewConfig.memorySize}\n`; serverless += ` bucketName: "${bucketName}"\n`; serverless += ` name: "${functionName}"\n`; } fs.writeFile("serverless.yml", serverless, (err) => { if (err) { console.error("Unable to create serverless.yml file!"); process.exit(1); } }); }); } deployServerless() { console.log("Deploying Next.js preview deployment via Serverless..."); try { child_process_1.execSync("serverless --no-aws-s3-accelerate", { stdio: "inherit" }); const cloudFrontId = this.readCloudFrontId(); child_process_1.execSync(`aws cloudfront create-invalidation --distribution-id ${cloudFrontId} --paths "/*"`, { stdio: "inherit" }); } catch (error) { console.error("Error deploying Serverless preview deployment."); process.exit(1); } } uploadFile(filepath, bucketName) { return __awaiter(this, void 0, void 0, function* () { const data = fs.readFileSync(filepath); const bucketParams = { Bucket: bucketName, Body: data, Key: this.deploymentToUse + filepath.replace("./.serverless", "./.serverless"), }; try { yield this.s3.putObject(bucketParams).promise(); console.log("Successfully added file to existing pr-view metadata S3 bucket."); } catch (err) { console.log("Error putting file to pr-view metadata S3 bucket."); } }); } pullState() { return __awaiter(this, void 0, void 0, function* () { console.log("Pulling existing Serverless state from AWS S3..."); const bucketName = this.getMetaDataBucketName(); const bucketParams = { Bucket: bucketName, }; let bucketExists = true; try { yield this.s3.headBucket(bucketParams).promise(); console.log("Successfully retrieved existing pr-view metadata S3 bucket."); } catch (err) { if (err.statusCode == 404) { bucketExists = false; } } if (!bucketExists) { console.log("pr-view metadata bucket doesn't exist. Creating one now."); try { yield this.s3.createBucket(bucketParams).promise(); console.log("Successfully created pr-view metadata S3 bucket: " + bucketName); } catch (err) { console.error("Error creating pr-view metadata S3 bucket: " + bucketName); } } try { child_process_1.execSync(`aws s3 sync s3://${bucketName}/${this.deploymentToUse}/.serverless .serverless --delete`, { stdio: "inherit" }); } catch (error) { console.error("Error syncing pr-view metadata from S3."); process.exit(1); } }); } pushState() { return __awaiter(this, void 0, void 0, function* () { console.log("Push Serverless state back to AWS S3..."); const bucketName = this.getMetaDataBucketName(); try { child_process_1.execSync(`aws s3 sync .serverless s3://${bucketName}/${this.deploymentToUse}/.serverless --delete`, { stdio: "inherit" }); } catch (error) { console.error("Error syncing pr-view metadata to S3."); process.exit(1); } }); } cleanUpServerless() { return __awaiter(this, void 0, void 0, function* () { console.log("Cleaning up Serverless Preview Deployment..."); console.log("Note: nothing needed to clean up in Serverless as future deployments will reuse this."); }); } removeBucket(bucketName) { return __awaiter(this, void 0, void 0, function* () { try { child_process_1.execSync(`aws s3 rb s3://${bucketName} --force`, { stdio: "inherit" }); } catch (error) { console.warn(`Unable to delete bucket [${bucketName}]. Error: ${error.toString()}`); } }); } printDeploymentDetails() { const cloudFrontUrl = this.readCloudFrontUrl(); console.log(`Deployed at: ${cloudFrontUrl}`); console.log(`Deployed at: ${this.generateDeployUrl()}`); child_process_1.execSync(`echo "::set-env name=PR_VIEW_CLOUD_FRONT_URL::${cloudFrontUrl}"`, { stdio: "inherit" }); } deploy() { return __awaiter(this, void 0, void 0, function* () { this.printConfiguration(); yield this.generateServerlessFile(true); yield this.pullState(); yield this.deployServerless(); yield this.pushState(); this.printDeploymentDetails(); }); } cleanup() { return __awaiter(this, void 0, void 0, function* () { yield this.generateServerlessFile(false); yield this.pullState(); yield this.cleanUpServerless(); yield this.markDeploymentNameAsFree(); }); } } exports.NextJsServerlessDeployer = NextJsServerlessDeployer; function getDeployer() { console.log("Finding the right deployer for your app type..."); const prViewConfig = config_1.default.readFromFile(); if (prViewConfig.framework === "next.js") { console.log("Detected Next.js app."); return new NextJsServerlessDeployer(prViewConfig); } else { return null; } } exports.default = getDeployer; //# sourceMappingURL=deployer.js.map