pr-view
Version:
Preview deployments for your pull requests
494 lines • 20.3 kB
JavaScript
"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