UNPKG

7.35 kBJavaScriptView Raw
1var chalk = require('chalk');
2var util = require('util');
3var async = require('async');
4var request = require('request');
5var inquirer = require('inquirer');
6var _ = require('lodash');
7var fs = require('fs');
8var os = require('os');
9var zlib = require('zlib');
10var path = require('path');
11var glob = require("glob");
12var urljoin = require('url-join');
13var open = require('open');
14var shortid = require('shortid');
15var debug = require('debug')('4front:cli:deploy');
16var spawn = require('../lib/spawn');
17var api = require('../lib/api');
18var log = require('../lib/log');
19var basedir = require('../lib/basedir');
20var helper = require('../lib/helper');
21
22require("simple-errors");
23
24var compressExtensions = ['.css', '.js', '.json', '.txt', '.svg'];
25
26module.exports = function(program, done) {
27 // Create a new version object
28 var newVersion, asyncTasks = [], inputAnswers = {};
29
30 // Force buildType to be release
31 program.buildType = 'release';
32
33 // Determine the baseDir
34 asyncTasks.push(function(cb) {
35 basedir(program, function(err, baseDir) {
36 if (err) return cb(err);
37
38 debug('setting baseDir to %s', baseDir);
39 program.baseDir = baseDir;
40 cb();
41 });
42 });
43
44 asyncTasks.push(collectVersionInputs);
45
46 // Run "npm run-script build"
47 asyncTasks.push(function(cb) {
48 if (inputAnswers.runBuildStep === true)
49 spawn('npm', ['run-script', 'build'], cb);
50 else
51 cb();
52 });
53
54 asyncTasks.push(function(cb) {
55 var globOptions = {
56 cwd: program.baseDir,
57 dot: false,
58 nodir: true,
59 ignore: ["node_modules/**/*"]
60 };
61
62 debug('globbing up files');
63 glob("**/*.*", globOptions, function(err, matches) {
64 if (err) return cb(err);
65 deployFiles = matches;
66 cb();
67 });
68 });
69
70 asyncTasks.push(createNewVersion);
71 asyncTasks.push(deployFiles);
72 asyncTasks.push(activateVersion);
73
74 async.series(asyncTasks, function(err) {
75 if (err) return done(err);
76
77 log.success("New version %s deployed and available at: %s", newVersion.versionId, newVersion.previewUrl);
78 if (program.open === true)
79 open(newVersion.previewUrl);
80
81 done();
82 });
83
84 function collectVersionInputs(callback) {
85 // Perform an unattended deployment, possibly from a CI process.
86 if (program.unattended === true) {
87 log.debug("Running in unattended mode");
88 // Assuming that a CI process would have already run the build step.
89 _.extend(inputAnswers, {
90 runBuildStep: false,
91 name: program.versionName,
92 message: program.message
93 });
94
95 return callback();
96 }
97
98 log.messageBox("Deploy a new version of the app.");
99
100 // Use inquirer to collect input.
101 var questions = [
102 {
103 type: 'input',
104 name: 'name',
105 message: 'Version name (leave blank to auto-generate):',
106 validate: validateVersionName
107 },
108 {
109 type: 'input',
110 name: 'message',
111 message: 'Message (optional):'
112 },
113 {
114 type: 'confirm',
115 name: 'runBuildStep',
116 message: 'Run "npm run-script build?"',
117 when: function() {
118 return _.isEmpty(program.virtualAppManifest.scripts.build) === false;
119 },
120 default: true
121 },
122 // TODO: Allow organization to disallow this.
123 {
124 type: 'confirm',
125 name: 'force',
126 message: 'Immediately direct all traffic to this new version?',
127 default: program.virtualApp.trafficControlEnabled === true ? true : false,
128 when: function(answers) {
129 return program.virtualApp.trafficControlEnabled === true;
130 }
131 }
132 ];
133
134 inquirer.prompt(questions, function(answers) {
135 _.extend(inputAnswers, answers);
136 callback();
137 });
138 }
139
140 function validateVersionName(name) {
141 if (_.isEmpty(name))
142 return true;
143
144 if (/^[a-z\.\_\-0-9]{5,20}$/i.test(name) !== true)
145 return "Version " + name + " can only consist of letters, numbers, dashes, periods, or underscores and must be between 5 and 20 characters";
146 return true;
147 }
148
149 function deployFiles(callback) {
150 // PUT each file individually
151 var uploadCount = 0;
152
153 async.each(deployFiles, function(file, cb) {
154 var fullPath = path.join(program.baseDir, file);
155
156 // Ensure the slashes are forward in the relative path
157 var uploadPath = file.replace(/\\/g, '/');
158 uploadCount++;
159
160 var compress = shouldCompress(file);
161 uploadFile(fullPath, uploadPath, compress, cb);
162 }, function(err) {
163 if (err) return callback(err);
164
165 debug('done uploading %s files', uploadCount);
166 callback();
167 });
168 }
169
170 function shouldCompress(filePath) {
171 return _.contains(compressExtensions, path.extname(filePath));
172 }
173
174 function uploadFile(filePath, uploadPath, compress, callback) {
175 var requestOptions = {
176 path: urljoin('apps', program.virtualApp.appId, 'versions',
177 newVersion.versionId, 'deploy', uploadPath),
178 headers: {},
179 method: 'POST'
180 };
181
182 function upload(file) {
183 log.info('Deploying file %s to %s', filePath, uploadPath);
184 fs.stat(file, function(err, stat) {
185 if (err) return callback(err);
186
187 requestOptions.headers['Content-Length'] = stat.size;
188 fs.createReadStream(file)
189 .pipe(api(program, requestOptions, callback));
190 });
191 }
192
193 if (compress === true) {
194 debug('compressing file ' + filePath);
195 requestOptions.headers['Content-Type'] = 'application/gzip';
196
197 // Use a random file name to avoid chance of collisions
198 var gzipFile = path.join(os.tmpdir(), shortid.generate() + path.extname(filePath) + '.gz');
199
200 debug("Writing to gzipFile %s", gzipFile);
201 fs.createReadStream(filePath)
202 .pipe(zlib.createGzip())
203 .pipe(fs.createWriteStream(gzipFile))
204 .on('error', function(err) {
205 return callback(err);
206 })
207 .on('finish', function() {
208 debug('done writing gzip file');
209 return upload(gzipFile);
210 });
211 }
212 else {
213 upload(filePath);
214 }
215 }
216
217 function activateVersion(callback) {
218 var versionData = {};
219
220 if (inputAnswers.force === true) {
221 versionData.forceAllTrafficToNewVersion = true;
222 if (program.virtualApp.trafficControlEnabled === true)
223 log.info(chalk.yellow('Forcing all traffic to the new version.'));
224 }
225
226 var requestOptions = {
227 method: 'PUT',
228 path: '/apps/' + program.virtualApp.appId + '/versions/' + newVersion.versionId + '/complete',
229 json: versionData
230 };
231
232 api(program, requestOptions, function(err, version) {
233 if (err) return callback(err);
234 newVersion = version;
235 callback();
236 });
237 }
238
239 // Create the new version in a non-ready state.
240 function createNewVersion(callback) {
241 var manifest = _.omit(program.virtualAppManifest, 'appId');
242
243 // Create the new version
244 log.info('Creating new version');
245
246 var requestOptions = {
247 method: 'POST',
248 path: '/apps/' + program.virtualApp.appId + '/versions',
249 json: {
250 name: inputAnswers.name,
251 message: inputAnswers.message,
252 manifest: manifest
253 }
254 };
255
256 api(program, requestOptions, function(err, version) {
257 debug("new version created");
258 if (err) return callback(err);
259 newVersion = version;
260 callback();
261 });
262 }
263};