UNPKG

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