cloudron 5.11.3 → 5.11.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/cloudron CHANGED
@@ -22,12 +22,6 @@ if (!semver.satisfies(process.version, require('../package.json').engines.node))
22
22
  }
23
23
 
24
24
  const program = new Command();
25
-
26
- function collectArgs(value, collected) {
27
- collected.push(value);
28
- return collected;
29
- }
30
-
31
25
  program.version(version);
32
26
 
33
27
  // global options
@@ -39,6 +33,7 @@ program.option('--server <server>', 'Cloudron domain')
39
33
  // these are separate binaries since global options are not applicable
40
34
  program.command('appstore', 'Cloudron appstore commands');
41
35
  program.command('repo', 'Cloudron repo commands');
36
+ program.command('build', 'Cloudron build commands');
42
37
 
43
38
  const backupCommand = program.command('backup')
44
39
  .description('App backup commands');
@@ -83,21 +78,6 @@ program.command('completion')
83
78
  .description('Shows completion for your shell')
84
79
  .action(completion);
85
80
 
86
- // should probably move to separate binary since globals don't apply
87
- program.command('build')
88
- .description('Build an app')
89
- .option('--build-arg <namevalue>', 'Build arg passed to docker. Can be used multiple times', collectArgs, [])
90
- .option('--build-service-token <token>', 'Build service token')
91
- .option('-f, --file <dockerfile>', 'Name of the Dockerfile')
92
- .option('--set-repository [repository url]', 'Change the repository')
93
- .option('--set-build-service [buildservice url]', 'Set build service app URL')
94
- .option('--local', 'Build docker images locally')
95
- .option('--no-cache', 'Do not use cache')
96
- .option('--no-push', 'Do not push built image to registry')
97
- .option('--raw', 'Raw output build log')
98
- .option('--tag <docker image tag>', 'Docker image tag. Note that this does not include the repository name')
99
- .action(buildActions.build);
100
-
101
81
  program.command('cancel')
102
82
  .description('Cancels any active or pending app task')
103
83
  .option('--app <id/location>', 'App id or location')
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env node
2
+
3
+ 'use strict';
4
+
5
+ const { program } = require('commander'),
6
+ buildActions = require('../src/build-actions.js');
7
+
8
+ program.version(require('../package.json').version);
9
+
10
+ function collectArgs(value, collected) {
11
+ collected.push(value);
12
+ return collected;
13
+ }
14
+
15
+ // global options
16
+ program.option('--server <server>', 'Cloudron domain')
17
+ .option('--token, --build-service-token <token>', 'Build service token')
18
+ .option('--url, --set-build-service [buildservice url]', 'Set build service URL. This build service is automatically used for future calls from this project');
19
+
20
+ program.command('build', { isDefault: true })
21
+ .description('Build an app. This is the default subcommand')
22
+ .option('--build-arg <namevalue>', 'Build arg passed to docker. Can be used multiple times', collectArgs, [])
23
+ .option('-f, --file <dockerfile>', 'Name of the Dockerfile')
24
+ .option('--set-repository [repository url]', 'Change the repository. This url is stored for future builds for this project. e.g registry/username/projectname')
25
+ .option('--local', 'Build docker images locally')
26
+ .option('--no-cache', 'Do not use cache')
27
+ .option('--no-push', 'Do not push built image to registry')
28
+ .option('--raw', 'Raw output build log')
29
+ .option('--tag <docker image tag>', 'Docker image tag. Note that this does not include the repository name')
30
+ .action(buildActions.build);
31
+
32
+ program.command('login')
33
+ .description('Login to the build service')
34
+ .option('-t, --token <token>', 'Build service token')
35
+ .action(buildActions.login);
36
+
37
+ program.command('logs')
38
+ .description('Build logs. This works only when using the Build Service')
39
+ .option('--id <buildid>', 'Build ID')
40
+ .option('--raw', 'Raw output build log')
41
+ .action(buildActions.logs);
42
+
43
+ program.command('push')
44
+ .description('Push the build image')
45
+ .option('--id <buildid>', 'Build ID')
46
+ .option('--repository [repository url]', 'Set repository to push to. e.g registry/username/projectname')
47
+ .option('--tag <docker image tag>', 'Docker image tag. Note that this does not include the repository name')
48
+ .action(buildActions.push);
49
+
50
+ program.command('status')
51
+ .description('Build status. This works only when using the Build Service')
52
+ .option('--id <buildid>', 'Build ID')
53
+ .action(buildActions.status);
54
+
55
+ program.parse(process.argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudron",
3
- "version": "5.11.3",
3
+ "version": "5.11.5",
4
4
  "license": "MIT",
5
5
  "description": "Cloudron Commandline Tool",
6
6
  "main": "main.js",
@@ -25,7 +25,6 @@
25
25
  "ejs": "^3.1.10",
26
26
  "eventsource": "^2.0.2",
27
27
  "micromatch": "^4.0.7",
28
- "once": "^1.4.0",
29
28
  "open": "^8.4.0",
30
29
  "progress": "^2.0.3",
31
30
  "progress-stream": "^2.0.0",
@@ -1,7 +1,11 @@
1
1
  'use strict';
2
2
 
3
3
  exports = module.exports = {
4
- build
4
+ login,
5
+ build,
6
+ logs,
7
+ status,
8
+ push
5
9
  };
6
10
 
7
11
  const assert = require('assert'),
@@ -11,7 +15,6 @@ const assert = require('assert'),
11
15
  execSync = require('child_process').execSync,
12
16
  exit = require('./helper.js').exit,
13
17
  fs = require('fs'),
14
- once = require('once'),
15
18
  helper = require('./helper.js'),
16
19
  manifestFormat = require('cloudron-manifestformat'),
17
20
  micromatch = require('micromatch'),
@@ -20,78 +23,80 @@ const assert = require('assert'),
20
23
  os = require('os'),
21
24
  path = require('path'),
22
25
  safe = require('safetydance'),
26
+ stream = require('stream/promises'),
23
27
  tar = require('tar-fs'),
24
- url = require('url'),
25
- util = require('util');
26
-
27
- function superagentEndBuildService(requestFactory, callback) {
28
- assert.strictEqual(typeof requestFactory, 'function');
29
- assert.strictEqual(typeof callback, 'function');
30
-
31
- requestFactory().end(function (error, result) {
32
- if (error && !error.response) return callback(error);
33
- if (result.statusCode === 400) return authenticateBuildService({ error: true }, superagentEndBuildService.bind(null, requestFactory, callback));
34
- if (result.statusCode === 401) return authenticateBuildService({ error: true }, superagentEndBuildService.bind(null, requestFactory, callback));
35
- if (result.statusCode === 403) return callback(new Error(result.type === 'application/javascript' ? JSON.stringify(result.body) : result.text));
36
- callback(error, result);
37
- });
28
+ url = require('url');
29
+
30
+ function requestError(response) {
31
+ if (response.statusCode === 401 || response.statusCode === 403) return 'Invalid token. Use cloudron build login again.';
32
+
33
+ return `${response.statusCode} message: ${response.body?.message || null}`;
38
34
  }
39
35
 
40
- function authenticateBuildService(options, callback) {
41
- assert.strictEqual(typeof options, 'object');
42
- assert.strictEqual(typeof callback, 'function');
36
+ // analyzes options and merges with any existing build service config
37
+ function getBuildServiceConfig(options) {
38
+ const buildService = config.getBuildServiceConfig();
39
+ if (!buildService.type) buildService.type = 'local'; // default
43
40
 
44
- var buildServiceConfig = config.getBuildServiceConfig();
41
+ if (options.local) {
42
+ buildService.type = 'local';
43
+ } else if (options.setBuildService) { // stash for future use
44
+ buildService.token = null;
45
+ buildService.url = null;
46
+ buildService.type = 'remote';
45
47
 
46
- if (!options.hideBanner) console.log('Build Service login' + ` (${buildServiceConfig.url}):`);
48
+ let url;
49
+ if (typeof options.setBuildService === 'string') {
50
+ url = options.setBuildService;
51
+ } else {
52
+ url = readlineSync.question('Enter build service URL: ', { });
53
+ }
47
54
 
48
- var username = options.username || readlineSync.question('Username: ', {});
49
- var password = options.password || readlineSync.question('Password: ', { noEchoBack: true });
55
+ if (url.indexOf('://') === -1) url = `https://${url}`;
56
+ buildService.url = url;
57
+ }
50
58
 
51
- // reset token
52
- buildServiceConfig.token = null;
53
- config.setBuildServiceConfig(buildServiceConfig);
59
+ if (options.buildServiceToken) buildService.token = options.buildServiceToken;
54
60
 
55
- superagent.post(`${buildServiceConfig.url}/api/v1/login`).send({ username: username, password: password }).end(function (error, result) {
56
- if (error && !error.response) exit(error);
61
+ config.setBuildServiceConfig(buildService);
57
62
 
58
- if (result.statusCode !== 200 && result.statusCode !== 201) {
59
- console.log('Login failed.');
63
+ return buildService;
64
+ }
60
65
 
61
- options.hideBanner = true;
62
- options.username = '';
63
- options.password = '';
66
+ async function login(options) {
67
+ assert.strictEqual(typeof options, 'object');
64
68
 
65
- return authenticateBuildService(options, callback);
66
- }
69
+ const buildServiceConfig = getBuildServiceConfig(options);
67
70
 
68
- buildServiceConfig.token = result.body.accessToken;
69
- config.setBuildServiceConfig(buildServiceConfig);
71
+ console.log('Build Service login' + ` (${buildServiceConfig.url}):`);
70
72
 
71
- console.log('Login successful.');
73
+ const token = options.buildServiceToken || readlineSync.question('Token: ', {});
72
74
 
73
- if (typeof callback === 'function') callback();
74
- });
75
+ const response = await superagent.get(`${buildServiceConfig.url}/api/v1/profile`).query({ accessToken: token }).ok(() => true);
76
+ if (response.statusCode === 401 || response.statusCode === 403) return exit(`Authentication error: ${requestError(response)}`);
77
+ if (response.statusCode !== 200) return exit(`Unexpected response: ${requestError(response)}`);
78
+
79
+ buildServiceConfig.token = token;
80
+ config.setBuildServiceConfig(buildServiceConfig);
81
+
82
+ console.log('Login successful.');
75
83
  }
76
84
 
77
- function followBuildLog(buildId, raw, callback) {
85
+ async function followBuildLog(buildId, raw) {
78
86
  assert.strictEqual(typeof buildId, 'string');
79
87
  assert.strictEqual(typeof raw, 'boolean');
80
88
 
81
- // ensure callback is only ever called once
82
- callback = once(callback);
83
-
84
89
  // EventSource always requires http
85
90
  let tmp = url.parse(config.getBuildServiceConfig().url);
86
91
  if (tmp.protocol !== 'https:' && tmp.protocol !== 'http:') tmp = url.parse('http://' + config.getBuildServiceConfig().url);
87
92
 
88
- var es = new EventSource(`${tmp.href}api/v1/builds/${buildId}/logstream?accessToken=${config.getBuildServiceConfig().token}`);
89
- var prevId = null, prevWasStatus = false;
93
+ const es = new EventSource(`${tmp.href}api/v1/builds/${buildId}/logstream?accessToken=${config.getBuildServiceConfig().token}`);
94
+ let prevId = null, prevWasStatus = false;
90
95
 
91
96
  es.on('message', function (e) {
92
97
  if (raw) return console.dir(e);
93
98
 
94
- var data = safe.JSON.parse(e.data);
99
+ const data = safe.JSON.parse(e.data);
95
100
  if (!data) return; // this is a bug in docker or our build server
96
101
 
97
102
  if (data.status) { // image push log
@@ -131,25 +136,39 @@ function followBuildLog(buildId, raw, callback) {
131
136
  console.error(data.error);
132
137
  }
133
138
  });
134
- es.on('error', function (error) {
135
- if (raw) console.dir(error);
136
139
 
137
- // We sometimes get { type: 'error' } from es module when the server closes the socket. not clear why
138
- if (error && !error.status && error.type === 'error') error = null;
140
+ let didConnect = false;
141
+ es.once('open', () => didConnect = true);
142
+
143
+ return new Promise((resolve, reject) => {
144
+ es.once('error', function (error) { // server close or network error or some interruption
145
+ if (raw) console.dir(error);
139
146
 
140
- callback(error && error.status ? error : null); // eventsource module really needs to give us better errors
147
+ es.close();
148
+ if (didConnect) resolve(); else reject(new Error('Failed to connect'));
149
+ });
141
150
  });
142
151
  }
143
152
 
153
+ async function getStatus(buildId) {
154
+ const buildServiceConfig = config.getBuildServiceConfig();
155
+
156
+ const response2 = await superagent.get(`${buildServiceConfig.url}/api/v1/builds/${buildId}`)
157
+ .query({ accessToken: buildServiceConfig.token })
158
+ .ok(() => true);
159
+ if (response2.statusCode !== 200) throw new Error(`Failed to get status: ${requestError(response2)}`);
160
+ return response2.body.status;
161
+ }
162
+
144
163
  function dockerignoreMatcher(dockerignorePath) {
145
- var patterns = [];
164
+ let patterns = [];
146
165
 
147
166
  if (fs.existsSync(dockerignorePath)) {
148
167
  patterns = fs.readFileSync(dockerignorePath, 'utf8').split('\n');
149
168
 
150
169
  patterns = patterns.filter(function (line) { return line[0] !== '#'; });
151
170
  patterns = patterns.map(function (line) {
152
- var l = line.trim();
171
+ let l = line.trim();
153
172
 
154
173
  while (l[0] === '/') l = l.slice(1);
155
174
  while (l[l.length-1] === '/') l = l.slice(0, -1);
@@ -164,7 +183,7 @@ function dockerignoreMatcher(dockerignorePath) {
164
183
  };
165
184
  }
166
185
 
167
- function buildLocal(manifest, sourceDir, appConfig, options) {
186
+ async function buildLocal(manifest, sourceDir, appConfig, options) {
168
187
  let tag;
169
188
  if (options.tag) {
170
189
  tag = options.tag;
@@ -180,7 +199,7 @@ function buildLocal(manifest, sourceDir, appConfig, options) {
180
199
  console.log('Building locally as %s', dockerImage);
181
200
  console.log();
182
201
 
183
- let buildArgsCmdLine = options.buildArg.map(function (a) { return `--build-arg "${a}"`; }).join(' ');
202
+ const buildArgsCmdLine = options.buildArg.map(function (a) { return `--build-arg "${a}"`; }).join(' ');
184
203
 
185
204
  let dockerfile = 'Dockerfile';
186
205
  if (options.file) dockerfile = options.file;
@@ -196,9 +215,9 @@ function buildLocal(manifest, sourceDir, appConfig, options) {
196
215
  if (safe.error) exit('Failed to push image (are you logged in? if not, use "docker login")');
197
216
  }
198
217
 
199
- let result = safe.child_process.execSync(`docker inspect --format="{{index .RepoDigests 0}}" ${dockerImage}`, { encoding: 'utf8' });
218
+ const result = safe.child_process.execSync(`docker inspect --format="{{index .RepoDigests 0}}" ${dockerImage}`, { encoding: 'utf8' });
200
219
  if (safe.error) exit('Failed to inspect image');
201
- let match = /.*@sha256:(.*)/.exec(result.trim());
220
+ const match = /.*@sha256:(.*)/.exec(result.trim());
202
221
  if (!match) exit('Failed to detect sha256');
203
222
 
204
223
  appConfig.dockerImage = `${dockerImage}`;
@@ -206,7 +225,7 @@ function buildLocal(manifest, sourceDir, appConfig, options) {
206
225
  config.setAppConfig(sourceDir, appConfig);
207
226
  }
208
227
 
209
- function buildRemote(manifest, sourceDir, appConfig, options) {
228
+ async function buildRemote(manifest, sourceDir, appConfig, options) {
210
229
  console.log('Using build service', config.getBuildServiceConfig().url);
211
230
 
212
231
  let tag;
@@ -223,21 +242,21 @@ function buildRemote(manifest, sourceDir, appConfig, options) {
223
242
 
224
243
  console.log('Building %s', dockerImage);
225
244
 
226
- var sourceArchiveFilePath = path.join(os.tmpdir(), path.basename(sourceDir) + '.tar.gz');
227
- var dockerignoreFilePath = path.join(sourceDir, '.dockerignore');
228
- var ignoreMatcher = dockerignoreMatcher(dockerignoreFilePath);
245
+ const sourceArchiveFilePath = path.join(os.tmpdir(), path.basename(sourceDir) + '.tar.gz');
246
+ const dockerignoreFilePath = path.join(sourceDir, '.dockerignore');
247
+ const ignoreMatcher = dockerignoreMatcher(dockerignoreFilePath);
229
248
 
230
249
  console.log('Uploading source tarball...');
231
250
 
232
- var stream = tar.pack(sourceDir, {
251
+ const tarStream = tar.pack(sourceDir, {
233
252
  ignore: function (name) {
234
253
  return ignoreMatcher(name.slice(sourceDir.length + 1)); // make name as relative path
235
254
  }
236
- }).pipe(fs.createWriteStream(sourceArchiveFilePath));
237
-
238
- stream.on('error', function (error) {
239
- exit('Failed to create application source archive: ' + error);
240
255
  });
256
+ const sourceArchiveStream = fs.createWriteStream(sourceArchiveFilePath);
257
+
258
+ const [tarError] = await safe(stream.pipeline(tarStream, sourceArchiveStream));
259
+ if (tarError) return exit(`Could not tar: ${tarError.message}`);
241
260
 
242
261
  let dockerfile = 'Dockerfile';
243
262
  if (options.file) dockerfile = options.file;
@@ -251,49 +270,40 @@ function buildRemote(manifest, sourceDir, appConfig, options) {
251
270
  buildArgsObject[key] = value;
252
271
  });
253
272
 
254
- stream.on('finish', function () {
255
- superagentEndBuildService(function () {
256
- let buildServiceConfig = config.getBuildServiceConfig();
257
- return superagent.post(`${buildServiceConfig.url}/api/v1/builds`)
258
- .query({ accessToken: buildServiceConfig.token, noCache: !options.cache, dockerfile: dockerfile, noPush: !options.push })
259
- .field('dockerImageRepo', appConfig.repository)
260
- .field('dockerImageTag', tag)
261
- .field('buildArgs', JSON.stringify(buildArgsObject))
262
- .attach('sourceArchive', sourceArchiveFilePath);
263
- }, function (error, result) {
264
- if (error && !error.response) return exit(util.format('Failed to build app: %s', error.message));
265
- if (result.statusCode === 413) exit('Failed to build app. The app source is too large.\nPlease adjust your .dockerignore file to only include neccessary files.');
266
- if (result.statusCode !== 201) exit(util.format('Failed to build app (statusCode %s): \n%s', result.statusCode, result.body && result.body.message ? result.body.message : result.text));
267
-
268
- var buildId = result.body.id;
269
-
270
- followBuildLog(buildId, false, function (error) {
271
- if (error) return console.error(error);
272
-
273
- superagentEndBuildService(function () {
274
- let buildServiceConfig = config.getBuildServiceConfig();
275
- return superagent.get(`${buildServiceConfig.url}/api/v1/builds/${buildId}`)
276
- .query({ accessToken: buildServiceConfig.token });
277
- }, function (error, result) {
278
- if (error && !error.response) return exit(util.format('Failed to build app: %s', error.message));
279
- if (result.statusCode !== 200) exit(util.format('Failed to build app (statusCode %s): \n%s', result.statusCode, result.body && result.body.message ? result.body.message : result.text));
280
- if (result.body.status !== 'success') exit('Failed to build app. See log output above.');
281
-
282
- appConfig.dockerImage = dockerImage;
283
- // appConfig.dockerImageSha256 = match[1]; // stash this separately for now
284
- config.setAppConfig(sourceDir, appConfig);
285
-
286
- console.log(dockerImage);
287
- console.log('\nBuild successful');
288
-
289
- exit();
290
- });
291
- });
292
- });
293
- });
273
+ const buildServiceConfig = config.getBuildServiceConfig();
274
+ const response = await superagent.post(`${buildServiceConfig.url}/api/v1/builds`)
275
+ .query({ accessToken: buildServiceConfig.token, noCache: !options.cache, dockerfile: dockerfile, noPush: !options.push })
276
+ .field('dockerImageRepo', appConfig.repository)
277
+ .field('dockerImageTag', tag)
278
+ .field('buildArgs', JSON.stringify(buildArgsObject))
279
+ .attach('sourceArchive', sourceArchiveFilePath)
280
+ .ok(() => true);
281
+ if (response.statusCode === 413) return exit('Failed to build app. The app source is too large.\nPlease adjust your .dockerignore file to only include neccessary files.');
282
+ if (response.statusCode !== 201) return exit(`Failed to upload app for building: ${requestError(response)}`);
283
+
284
+ const buildId = response.body.id;
285
+ console.log(`BuildId: ${buildId}`);
286
+
287
+ const [logsError] = await safe(followBuildLog(buildId, !!options.raw));
288
+ if (logsError) console.log(`Failed to get logs: ${logsError.message}`);
289
+
290
+ const [statusError, status] = await safe(getStatus(buildId));
291
+ if (statusError) return exit(`Failed to get status: ${statusError.message}`);
292
+ if (status !== 'success') return exit('Failed to build app. See log output above.');
293
+
294
+ appConfig.dockerImage = dockerImage;
295
+ // appConfig.dockerImageSha256 = match[1]; // stash this separately for now
296
+ config.setAppConfig(sourceDir, appConfig);
297
+
298
+ console.log(`Docker image: ${dockerImage}`);
299
+ console.log('\nBuild successful');
300
+
301
+ exit();
294
302
  }
295
303
 
296
- function build(options) {
304
+ async function build(localOptions, cmd) {
305
+ const options = cmd.optsWithGlobals();
306
+
297
307
  // try to find the manifest of this project
298
308
  const manifestFilePath = helper.locateManifest();
299
309
  if (!manifestFilePath) return exit('No CloudronManifest.json found');
@@ -301,35 +311,11 @@ function build(options) {
301
311
  const result = manifestFormat.parseFile(manifestFilePath);
302
312
  if (result.error) return exit('Error in CloudronManifest.json: ' + result.error.message);
303
313
 
304
- let manifest = result.manifest;
314
+ const manifest = result.manifest;
305
315
  const sourceDir = path.dirname(manifestFilePath);
306
316
 
307
317
  const appConfig = config.getAppConfig(sourceDir);
308
-
309
- let buildService = config.getBuildServiceConfig();
310
- if (!buildService.type) buildService.type = 'local'; // default
311
-
312
- if (options.local) {
313
- buildService.type = 'local';
314
- } else if (options.setBuildService) {
315
- buildService.token = null;
316
- buildService.url = null;
317
- buildService.type = 'remote';
318
-
319
- let url;
320
- if (typeof options.setBuildService === 'string') {
321
- url = options.setBuildService;
322
- } else {
323
- url = readlineSync.question('Enter build service URL: ', { });
324
- }
325
-
326
- if (url.indexOf('://') === -1) url = `https://${url}`;
327
- buildService.url = url;
328
- }
329
-
330
- if (options.buildServiceToken) buildService.token = options.buildServiceToken;
331
-
332
- config.setBuildServiceConfig(buildService);
318
+ const buildServiceConfig = getBuildServiceConfig(options);
333
319
 
334
320
  let repository = appConfig.repository;
335
321
  if (!repository || options.setRepository) {
@@ -345,11 +331,53 @@ function build(options) {
345
331
  config.setAppConfig(sourceDir, appConfig);
346
332
  }
347
333
 
348
- if (buildService.type === 'local') {
349
- buildLocal(manifest, sourceDir, appConfig, options);
350
- } else if (buildService.type === 'remote' && buildService.url) {
351
- buildRemote(manifest, sourceDir, appConfig, options);
334
+ if (buildServiceConfig.type === 'local') {
335
+ await buildLocal(manifest, sourceDir, appConfig, options);
336
+ } else if (buildServiceConfig.type === 'remote' && buildServiceConfig.url) {
337
+ await buildRemote(manifest, sourceDir, appConfig, options);
352
338
  } else {
353
339
  exit('Unknown build service type or missing build service url. Rerun with --reset-build-service');
354
340
  }
355
341
  }
342
+
343
+ async function logs(localOptions, cmd) {
344
+ const options = cmd.optsWithGlobals();
345
+
346
+ if (!options.id) return exit('buildId is required');
347
+
348
+ const [logsError] = await safe(followBuildLog(options.id, !!options.raw));
349
+ if (logsError) console.log(`Failed to get logs: ${logsError.message}`);
350
+ }
351
+
352
+ async function status(localOptions, cmd) {
353
+ const options = cmd.optsWithGlobals();
354
+
355
+ if (!options.id) return exit('buildId is required');
356
+
357
+ const [statusError, status] = await safe(getStatus(options.id));
358
+ if (statusError) return exit(`Failed to get status: ${statusError.message}`);
359
+ console.log(status);
360
+ }
361
+
362
+ async function push(localOptions, cmd) {
363
+ const options = cmd.optsWithGlobals();
364
+
365
+ if (!options.id) return exit('buildId is required');
366
+ if (!options.repository) return exit('repository is required');
367
+ if (!options.tag) return exit('tag is required');
368
+
369
+ const buildServiceConfig = getBuildServiceConfig(options);
370
+
371
+ const response = await superagent.post(`${buildServiceConfig.url}/api/v1/builds/${options.id}/push`)
372
+ .query({ accessToken: buildServiceConfig.token })
373
+ .send({ dockerImageRepo: options.repository, dockerImageTag: options.tag })
374
+ .ok(() => true);
375
+ if (response.statusCode !== 201) return exit(`Failed to push: ${requestError(response)}`);
376
+
377
+ const [logsError] = await safe(followBuildLog(options.id, !!options.raw));
378
+ if (logsError) console.log(`Failed to get logs: ${logsError.message}`);
379
+
380
+ const [statusError, status] = await safe(getStatus(options.id));
381
+ if (statusError) return exit(`Failed to get status: ${statusError.message}`);
382
+ if (status !== 'success') return exit('Failed to push app. See log output above.');
383
+ }