cloudron 5.11.6 → 5.11.8

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.
@@ -2,10 +2,17 @@
2
2
 
3
3
  'use strict';
4
4
 
5
- const { program } = require('commander'),
6
- appstoreActions = require('../src/appstore-actions.js');
5
+ const appstoreActions = require('../src/appstore-actions.js'),
6
+ Command = require('commander').Command;
7
7
 
8
- program.version(require('../package.json').version);
8
+ const version = require('../package.json').version;
9
+
10
+ const program = new Command();
11
+ program.version(version);
12
+
13
+ // global options. IMPORTANT: These cannot conflict with global options!
14
+ program
15
+ .option('--appstore-token <token>', 'AppStore token');
9
16
 
10
17
  program.command('login')
11
18
  .description('Login to the appstore')
@@ -27,9 +34,13 @@ program.command('approve')
27
34
  .option('--appstore-id <appid@version>', 'Appstore id and version')
28
35
  .action(appstoreActions.approve);
29
36
 
30
- program.command('publish <version>')
31
- .description('Tag the repo and publish')
32
- .action(appstoreActions.publish);
37
+ program.command('notify')
38
+ .description('Notify forum about successful app submission')
39
+ .action(appstoreActions.notify);
40
+
41
+ program.command('tag <version>')
42
+ .description('Tag the repo')
43
+ .action(appstoreActions.tag);
33
44
 
34
45
  program.command('revoke')
35
46
  .description('Revoke a published app version')
@@ -47,6 +58,7 @@ program.command('upload')
47
58
  .action(appstoreActions.upload);
48
59
 
49
60
  program.command('versions')
61
+ .alias('list')
50
62
  .description('List published versions')
51
63
  .option('--appstore-id <id>', 'Appstore id')
52
64
  .option('--raw', 'Dump versions as json')
@@ -12,9 +12,9 @@ function collectArgs(value, collected) {
12
12
  return collected;
13
13
  }
14
14
 
15
- // global options
15
+ // global options. IMPORTANT: These cannot conflict with global options!
16
16
  program.option('--server <server>', 'Cloudron domain')
17
- .option('--token, --build-service-token <token>', 'Build service token')
17
+ .option('--build-token, --build-service-token <token>', 'Build service token')
18
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
19
 
20
20
  program.command('build', { isDefault: true })
@@ -45,6 +45,7 @@ program.command('push')
45
45
  .option('--id <buildid>', 'Build ID')
46
46
  .option('--repository [repository url]', 'Set repository to push to. e.g registry/username/projectname')
47
47
  .option('--tag <docker image tag>', 'Docker image tag. Note that this does not include the repository name')
48
+ .option('--image <docker image>', 'Docker image of the form registry/repo:tag')
48
49
  .action(buildActions.push);
49
50
 
50
51
  program.command('status')
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "cloudron",
3
- "version": "5.11.6",
3
+ "version": "5.11.8",
4
4
  "license": "MIT",
5
5
  "description": "Cloudron Commandline Tool",
6
6
  "main": "main.js",
7
- "homepage": "https://git.cloudron.io/cloudron/cloudron-cli",
7
+ "homepage": "https://git.cloudron.io/platform/cloudron-cli",
8
8
  "repository": {
9
9
  "type": "git",
10
- "url": "https://git.cloudron.io/cloudron/cloudron-cli.git"
10
+ "url": "https://git.cloudron.io/platform/cloudron-cli.git"
11
11
  },
12
12
  "scripts": {
13
13
  "test": "mocha test/test.js"
@@ -25,7 +25,8 @@ exports = module.exports = {
25
25
  revoke,
26
26
  approve,
27
27
 
28
- publish
28
+ tag,
29
+ notify
29
30
  };
30
31
 
31
32
  const NO_MANIFEST_FOUND_ERROR_STRING = 'No CloudronManifest.json found';
@@ -36,10 +37,12 @@ function requestError(response) {
36
37
  return `${response.statusCode} message: ${response.body.message || JSON.stringify(response.body)}`; // body is sometimes just a string like in 401
37
38
  }
38
39
 
39
- function createRequest(method, apiPath) {
40
+ function createRequest(method, apiPath, options) {
41
+ const token = options.appstoreToken || config.appStoreToken();
42
+
40
43
  let url = `${config.appStoreOrigin()}${apiPath}`;
41
44
  if (url.includes('?')) url += '&'; else url += '?';
42
- url += `accessToken=${config.appStoreToken()}`;
45
+ url += `accessToken=${token}`;
43
46
  const request = superagent(method, url);
44
47
  request.retry(3);
45
48
  request.ok(() => true);
@@ -63,7 +66,7 @@ async function getAppstoreId(appstoreId) {
63
66
  return [manifest.id, manifest.version];
64
67
  }
65
68
 
66
- async function authenticate(options) {
69
+ async function authenticate(options) { // maybe we can use options.token to valid using a profile call?
67
70
  if (!options.hideBanner) {
68
71
  const webDomain = config.appStoreOrigin().replace('https://api.', '');
69
72
  console.log(`${webDomain} login` + ` (If you do not have one, sign up at https://${webDomain}/console.html#/register)`);
@@ -101,7 +104,8 @@ async function authenticate(options) {
101
104
  console.log('Login successful.');
102
105
  }
103
106
 
104
- async function login(options) {
107
+ async function login(localOptions, cmd) {
108
+ const options = cmd.optsWithGlobals();
105
109
  await authenticate(options);
106
110
  }
107
111
 
@@ -110,11 +114,12 @@ function logout() {
110
114
  console.log('Done.');
111
115
  }
112
116
 
113
- async function info(options) {
117
+ async function info(localOptions, cmd) {
118
+ const options = cmd.optsWithGlobals();
114
119
  const [id, version] = await getAppstoreId(options.appstoreId);
115
120
 
116
- const response = await createRequest('GET', `/api/v1/developers/apps/${id}/versions/${version}`);
117
- if (response.statusCode !== 200) throw new Error(`Failed to list domains: ${requestError(response)}`);
121
+ const response = await createRequest('GET', `/api/v1/developers/apps/${id}/versions/${version}`, options);
122
+ if (response.statusCode !== 200) return exit(new Error(`Failed to list versions: ${requestError(response)}`));
118
123
 
119
124
  const manifest = response.body.manifest;
120
125
  console.log('id: %s', manifest.id);
@@ -125,11 +130,12 @@ async function info(options) {
125
130
  console.log('contactEmail: %s', manifest.contactEmail);
126
131
  }
127
132
 
128
- async function listVersions(options) {
133
+ async function listVersions(localOptions, cmd) {
134
+ const options = cmd.optsWithGlobals();
129
135
  const [id] = await getAppstoreId(options.appstoreId);
130
136
 
131
- const response = await createRequest('GET', `/api/v1/developers/apps/${id}/versions`);
132
- if (response.statusCode !== 200) throw new Error(`Failed to list domains: ${requestError(response)}`);
137
+ const response = await createRequest('GET', `/api/v1/developers/apps/${id}/versions`, options);
138
+ if (response.statusCode !== 200) return exit(new Error(`Failed to list versions: ${requestError(response)}`));
133
139
 
134
140
  if (response.body.versions.length === 0) return console.log('No versions found.');
135
141
 
@@ -150,17 +156,6 @@ async function listVersions(options) {
150
156
  console.log(t.toString());
151
157
  }
152
158
 
153
- async function addApp(manifest, baseDir) {
154
- assert.strictEqual(typeof manifest, 'object');
155
- assert.strictEqual(typeof baseDir, 'string');
156
-
157
- const request = createRequest('POST', '/api/v1/developers/apps');
158
- request.send({ id: manifest.id });
159
- const response = await request;
160
- if (response.statusCode === 409) return; // already exists
161
- if (response.statusCode !== 201) return exit(`Failed to add app: ${requestError(response)}`);
162
- }
163
-
164
159
  function parseChangelog(file, version) {
165
160
  let changelog = '';
166
161
  const data = safe.fs.readFileSync(file, 'utf8');
@@ -184,9 +179,10 @@ function parseChangelog(file, version) {
184
179
  return changelog;
185
180
  }
186
181
 
187
- async function addVersion(manifest, baseDir) {
182
+ async function addVersion(manifest, baseDir, options) {
188
183
  assert.strictEqual(typeof manifest, 'object');
189
184
  assert.strictEqual(typeof baseDir, 'string');
185
+ assert.strictEqual(typeof options, 'object');
190
186
 
191
187
  let iconFilePath = null;
192
188
  if (manifest.icon) {
@@ -220,7 +216,7 @@ async function addVersion(manifest, baseDir) {
220
216
  if (!manifest.changelog) throw new Error('Bad changelog format or missing changelog for this version');
221
217
  }
222
218
 
223
- const request = createRequest('POST', `/api/v1/developers/apps/${manifest.id}/versions`);
219
+ const request = createRequest('POST', `/api/v1/developers/apps/${manifest.id}/versions`, options);
224
220
  if (iconFilePath) request.attach('icon', iconFilePath);
225
221
  request.attach('manifest', Buffer.from(JSON.stringify(manifest)), 'manifest');
226
222
  const response = await request;
@@ -228,9 +224,10 @@ async function addVersion(manifest, baseDir) {
228
224
  if (response.statusCode !== 204) throw new Error(`Failed to publish version: ${requestError(response)}`);
229
225
  }
230
226
 
231
- async function updateVersion(manifest, baseDir) {
227
+ async function updateVersion(manifest, baseDir, options) {
232
228
  assert.strictEqual(typeof manifest, 'object');
233
229
  assert.strictEqual(typeof baseDir, 'string');
230
+ assert.strictEqual(typeof options, 'object');
234
231
 
235
232
  let iconFilePath = null;
236
233
  if (manifest.icon) {
@@ -262,59 +259,15 @@ async function updateVersion(manifest, baseDir) {
262
259
  if (!manifest.changelog) throw new Error('Could not read changelog or missing version changes');
263
260
  }
264
261
 
265
- const request = createRequest('PUT', `/api/v1/developers/apps/${manifest.id}/versions/${manifest.version}`);
262
+ const request = createRequest('PUT', `/api/v1/developers/apps/${manifest.id}/versions/${manifest.version}`, options);
266
263
  if (iconFilePath) request.attach('icon', iconFilePath);
267
264
  request.attach('manifest', Buffer.from(JSON.stringify(manifest)), 'manifest');
268
265
  const response = await request;
269
266
  if (response.statusCode !== 204) throw new Error(`Failed to publish version: ${requestError(response)}`);
270
267
  }
271
268
 
272
- async function revokeVersion(appstoreId, version) {
273
- assert.strictEqual(typeof appstoreId, 'string');
274
- assert.strictEqual(typeof version, 'string');
275
-
276
- const response = await createRequest('POST', `/api/v1/developers/apps/${appstoreId}/versions/${version}/revoke`);
277
- if (response.statusCode !== 200) return exit(`Failed to revoke version: ${requestError(response)}`);
278
-
279
- console.log('version revoked.');
280
- }
281
-
282
- async function approveVersion(appstoreId, version) {
283
- assert.strictEqual(typeof appstoreId, 'string');
284
- assert.strictEqual(typeof version, 'string');
285
-
286
- const response = await createRequest('POST', `/api/v1/developers/apps/${appstoreId}/versions/${version}/approve`);
287
- if (response.statusCode !== 200) return exit(`Failed to approve version: ${requestError(response)}`);
288
-
289
- console.log('Approved.');
290
- console.log('');
291
-
292
- const response2 = await createRequest('GET', `/api/v1/developers/apps/${appstoreId}/versions/${version}`);
293
- if (response2.statusCode !== 200) return exit(`Failed to list apps: ${requestError(response)}`);
294
-
295
- console.log('Changelog for forum update: ' + response2.body.manifest.forumUrl);
296
- console.log('');
297
- console.log('[' + version + ']');
298
- console.log(response2.body.manifest.changelog);
299
- console.log('');
300
- }
301
-
302
- async function submitAppForReview(manifest) {
303
- assert.strictEqual(typeof manifest, 'object');
304
-
305
- const response = await createRequest('POST', `/api/v1/developers/apps/${manifest.id}/versions/${manifest.version}/submit`);
306
- if (response.statusCode === 404) {
307
- console.log(`No version ${manifest.version} found. Please use 'cloudron apsptore upload' first`);
308
- return exit('Failed to submit app for review.');
309
- }
310
-
311
- if (response.statusCode !== 200) return exit(`Failed to submit app: ${requestError(response)}`);
312
-
313
- console.log('App submitted for review.');
314
- console.log('You will receive an email when approved.');
315
- }
316
-
317
- async function upload(options) {
269
+ async function upload(localOptions, cmd) {
270
+ const options = cmd.optsWithGlobals();
318
271
  // try to find the manifest of this project
319
272
  const manifestFilePath = locateManifest();
320
273
  if (!manifestFilePath) return exit(NO_MANIFEST_FOUND_ERROR_STRING);
@@ -328,8 +281,19 @@ async function upload(options) {
328
281
  const appConfig = config.getAppConfig(sourceDir);
329
282
 
330
283
  // image can be passed in options for buildbot
331
- const dockerImage = options.image ? options.image : appConfig.dockerImage;
332
- manifest.dockerImage = dockerImage;
284
+ if (options.image) {
285
+ manifest.dockerImage = options.image;
286
+ } else {
287
+ const gitCommit = execSync('git rev-parse HEAD', { encoding: 'utf8' }).trim();
288
+ if (appConfig.gitCommit !== gitCommit) {
289
+ console.log(`This build ${appConfig.dockerImage} was made from git hash ${appConfig.gitCommit} but you are now at ${gitCommit}`);
290
+ if (!appConfig.gitCommit) return exit('The build is stale');
291
+ const output = execSync(`git diff ${appConfig.gitCommit}..HEAD --name-only`, { encoding: 'utf8'});
292
+ const changedFiles = output.trim().split('\n').filter(filepath => !filepath.match(/(^CHANGELOG|README|CloudronManifest|test\|.git|screenshots|renovate|LICENSE|POSTINSTALL|.docker|logo)/));
293
+ if (changedFiles.length) return exit(`The build is stale. Changed files: ${changedFiles.join(',')}`);
294
+ }
295
+ manifest.dockerImage = appConfig.dockerImage;
296
+ }
333
297
 
334
298
  if (!manifest.dockerImage) exit('No docker image found, run `cloudron build` first');
335
299
 
@@ -339,16 +303,26 @@ async function upload(options) {
339
303
  const error = manifestFormat.checkAppstoreRequirements(manifest);
340
304
  if (error) return exit(error);
341
305
 
306
+ const [repo, tag] = manifest.dockerImage.split(':');
307
+ const [tagError, tagResponse] = await safe(superagent.get(`https://hub.docker.com/v2/repositories/${repo}/tags/${tag}`).ok(() => true));
308
+ if (tagError || tagResponse.statusCode !== 200) return exit(`Failed to find docker image in dockerhub. check https://hub.docker.com/r/${repo}/tags : ${requestError(tagResponse)}`);
309
+
342
310
  // ensure the app is known on the appstore side
343
311
  const baseDir = path.dirname(manifestFilePath);
344
- await addApp(manifest, baseDir);
312
+
313
+ const request = createRequest('POST', '/api/v1/developers/apps', options);
314
+ request.send({ id: manifest.id });
315
+ const response = await request;
316
+ if (response.statusCode !== 409 && response.statusCode !== 201) return exit(`Failed to add app: ${requestError(response)}`); // 409 means already exists
345
317
  console.log(`Uploading ${manifest.id}@${manifest.version} (dockerImage: ${manifest.dockerImage}) for testing`);
346
318
 
347
- const [error2] = await safe(options.force ? updateVersion(manifest, baseDir) : addVersion(manifest, baseDir));
319
+ const [error2] = await safe(options.force ? updateVersion(manifest, baseDir, options) : addVersion(manifest, baseDir, options));
348
320
  if (error2) return exit(error2);
349
321
  }
350
322
 
351
- function submit() {
323
+ async function submit(localOptions, cmd) {
324
+ const options = cmd.optsWithGlobals();
325
+
352
326
  // try to find the manifest of this project
353
327
  const manifestFilePath = locateManifest();
354
328
  if (!manifestFilePath) return exit(NO_MANIFEST_FOUND_ERROR_STRING);
@@ -358,28 +332,57 @@ function submit() {
358
332
 
359
333
  const manifest = result.manifest;
360
334
 
361
- submitAppForReview(manifest, exit);
335
+ const response = await createRequest('POST', `/api/v1/developers/apps/${manifest.id}/versions/${manifest.version}/submit`, options);
336
+ if (response.statusCode === 404) {
337
+ console.log(`No version ${manifest.version} found. Please use 'cloudron apsptore upload' first`);
338
+ return exit('Failed to submit app for review.');
339
+ }
340
+
341
+ if (response.statusCode !== 200) return exit(`Failed to submit app: ${requestError(response)}`);
342
+
343
+ console.log('App submitted for review.');
344
+ console.log('You will receive an email when approved.');
362
345
  }
363
346
 
364
- async function revoke(options) {
347
+ async function revoke(localOptions, cmd) {
348
+ const options = cmd.optsWithGlobals();
365
349
  const [id, version] = await getAppstoreId(options.appstoreId);
366
350
  if (!version) return exit('--appstore-id must be of the format id@version');
367
351
 
368
352
  console.log(`Revoking ${id}@${version}`);
369
- await revokeVersion(id, version);
353
+
354
+ const response = await createRequest('POST', `/api/v1/developers/apps/${id}/versions/${version}/revoke`, options);
355
+ if (response.statusCode !== 200) return exit(`Failed to revoke version: ${requestError(response)}`);
356
+
357
+ console.log('version revoked.');
370
358
  }
371
359
 
372
- async function approve(options) {
373
- const [id, version] = await getAppstoreId(options.appstoreId);
360
+ async function approve(localOptions, cmd) {
361
+ const options = cmd.optsWithGlobals();
362
+
363
+ const [appstoreId, version] = await getAppstoreId(options.appstoreId);
374
364
 
375
365
  if (!version) return exit('--appstore-id must be of the format id@version');
376
366
 
377
- console.log(`Approving ${id}@${version}`);
367
+ console.log(`Approving ${appstoreId}@${version}`);
368
+
369
+ const response = await createRequest('POST', `/api/v1/developers/apps/${appstoreId}/versions/${version}/approve`, options);
370
+ if (response.statusCode !== 200) return exit(`Failed to approve version: ${requestError(response)}`);
378
371
 
379
- await approveVersion(id, version);
372
+ console.log('Approved.');
373
+ console.log('');
374
+
375
+ const response2 = await createRequest('GET', `/api/v1/developers/apps/${appstoreId}/versions/${version}`, options);
376
+ if (response2.statusCode !== 200) return exit(`Failed to list apps: ${requestError(response)}`);
377
+
378
+ console.log('Changelog for forum update: ' + response2.body.manifest.forumUrl);
379
+ console.log('');
380
+ console.log('[' + version + ']');
381
+ console.log(response2.body.manifest.changelog);
382
+ console.log('');
380
383
  }
381
384
 
382
- async function tagRepository(version) {
385
+ async function tag(version) {
383
386
  const basename = `${path.basename(process.cwd())}`;
384
387
  if (!basename.endsWith('-app')) return exit('Does not look like a app repo. Has to end with -app');
385
388
 
@@ -392,7 +395,7 @@ async function tagRepository(version) {
392
395
  if (!manifestFilePath) return exit('Could not locate CloudronManifest.json');
393
396
 
394
397
  const result = manifestFormat.parseFile(manifestFilePath);
395
- if (result.error) throw new Error(`Invalid CloudronManifest.json: ${result.error.message}`);
398
+ if (result.error) return exit(new Error(`Invalid CloudronManifest.json: ${result.error.message}`));
396
399
  const { manifest } = result;
397
400
 
398
401
  const latestVersion = latestTag.match(/v(.*)/)[1];
@@ -441,22 +444,60 @@ async function tagRepository(version) {
441
444
  console.log(`Created tag v${version} and pushed branch ${branch}`);
442
445
  }
443
446
 
444
- async function publish(version, options) {
445
- await tagRepository(version);
447
+ // https://docs.nodebb.org/api/read/
448
+ // https://docs.nodebb.org/api/write/
449
+ async function notify() {
450
+ if (!process.env.NODEBB_API_TOKEN) return exit('NODEBB_API_TOKEN env var has to be set');
451
+ const apiToken = process.env.NODEBB_API_TOKEN;
446
452
 
447
453
  const manifestFilePath = locateManifest();
448
454
  if (!manifestFilePath) return exit('Could not locate CloudronManifest.json');
449
455
 
450
- const latestRenovateCommit = safe.child_process.execSync('git log -n 1 --committer=renovatebot@cloudron.io --pretty="format:%h,%aI,%s"', { encoding: 'utf8' });
451
- if (!latestRenovateCommit) return exit('Could not find a commit from renovate bot');
456
+ const result = manifestFormat.parseFile(manifestFilePath);
457
+ if (result.error) return exit(new Error(`Invalid CloudronManifest.json: ${result.error.message}`));
458
+ const { manifest } = result;
452
459
 
453
- const [ abbrevHash, commitDate ] = latestRenovateCommit.split(',');
454
- const cleanDate = commitDate.replace(/T.*/, '').replace(/[-]/g, '');
455
- const repoDir = path.dirname(manifestFilePath);
456
- const repoName = path.basename(repoDir).replace('-app', '');
457
- const dockerImage = options.image || `cloudron/${repoName}:${cleanDate}-${abbrevHash}`;
460
+ let postContent = null;
461
+ if (manifest.changelog.slice(0, 7) === 'file://') {
462
+ const baseDir = path.dirname(manifestFilePath);
463
+ let changelogPath = manifest.changelog.slice(7);
464
+ changelogPath = path.isAbsolute(changelogPath) ? changelogPath : path.join(baseDir, changelogPath);
465
+ const changelog = parseChangelog(changelogPath, manifest.version);
466
+ if (!changelog) return exit('Bad changelog format or missing changelog for this version');
467
+ postContent = `[${manifest.version}]\n${changelog}\n`;
468
+ } else {
469
+ postContent = `[${manifest.version}]\n${manifest.changelog}\n`;
470
+ }
471
+
472
+ if (!manifest.forumUrl) return exit(new Error('CloudronManifest.json does not have a forumUrl'));
473
+ const categoryMatch = manifest.forumUrl.match(/category\/(.*)\//);
474
+ if (!categoryMatch) return exit('Unable to detect category id');
475
+ const categoryId = categoryMatch[1];
476
+
477
+ const categoryResponse = await superagent.get(`https://forum.cloudron.io/api/v3/categories/${categoryId}/topics`).set('Authorization', `Bearer ${apiToken}`).ok(() => true);
478
+ if (categoryResponse.statusCode !== 200) return exit(`Unable to get topics of category: ${requestError(categoryResponse)}`);
479
+ const topic = categoryResponse.body.response.topics.find(t => t.title.includes('Package Updates'));
480
+ if (!topic) return exit('Could not find the Package Update topic');
481
+ const topicId = topic.tid;
482
+
483
+ const pageCountResponse = await superagent.get(`https://forum.cloudron.io/api/topic/pagination/${topicId}`).set('Authorization', `Bearer ${apiToken}`).ok(() => true);
484
+ if (pageCountResponse.statusCode !== 200) return exit(`Unable to get page count of topic: ${requestError(pageCountResponse)}`);
485
+ const pageCount = pageCountResponse.body.pagination.pageCount;
486
+
487
+ for (let page = 1; page <= pageCount; page++) {
488
+ const pageResponse = await superagent.get(`https://forum.cloudron.io/api/topic/${topicId}?page=${page}`).set('Authorization', `Bearer ${apiToken}`).ok(() => true);
489
+ if (pageResponse.statusCode !== 200) return exit(`Unable to get topics of category: ${requestError(pageResponse)}`);
490
+ for (const post of pageResponse.body.posts) { // post.content is html!
491
+ if (post.content.includes(`[${manifest.version}]`)) return exit(`Version ${manifest.version} is already on the forum.\n${post.content}`);
492
+ }
493
+ }
458
494
 
459
- await upload({ image: dockerImage, force: false });
460
- await submit();
461
- await approve();
495
+ // https://docs.nodebb.org/api/write/#tag/topics/paths/~1topics~1%7Btid%7D/post
496
+ const postData = {
497
+ content: postContent,
498
+ toPid: 0 // which post is this post a reply to
499
+ };
500
+ const postResponse = await superagent.post(`https://forum.cloudron.io/api/v3/topics/${topicId}`).set('Authorization', `Bearer ${apiToken}`).send(postData).ok(() => true);
501
+ if (postResponse.statusCode !== 200) return exit(`Unable to create changelog post: ${requestError(postResponse)}`);
502
+ console.log('Posted to forum');
462
503
  }
@@ -331,6 +331,7 @@ async function build(localOptions, cmd) {
331
331
  config.setAppConfig(sourceDir, appConfig);
332
332
  }
333
333
 
334
+ appConfig.gitCommit = execSync('git rev-parse HEAD', { encoding: 'utf8' }).trim(); // when the build gets saved, save the gitCommit also
334
335
  if (buildServiceConfig.type === 'local') {
335
336
  await buildLocal(manifest, sourceDir, appConfig, options);
336
337
  } else if (buildServiceConfig.type === 'remote' && buildServiceConfig.url) {
@@ -363,14 +364,21 @@ async function push(localOptions, cmd) {
363
364
  const options = cmd.optsWithGlobals();
364
365
 
365
366
  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');
367
+ let repository, tag;
368
+ if (options.image) {
369
+ [repository, tag] = options.image.split(':');
370
+ } else {
371
+ if (!options.repository) return exit('repository is required');
372
+ if (!options.tag) return exit('tag is required');
373
+ repository = options.repository;
374
+ tag = options.tag;
375
+ }
368
376
 
369
377
  const buildServiceConfig = getBuildServiceConfig(options);
370
378
 
371
379
  const response = await superagent.post(`${buildServiceConfig.url}/api/v1/builds/${options.id}/push`)
372
380
  .query({ accessToken: buildServiceConfig.token })
373
- .send({ dockerImageRepo: options.repository, dockerImageTag: options.tag })
381
+ .send({ dockerImageRepo: repository, dockerImageTag: tag })
374
382
  .ok(() => true);
375
383
  if (response.statusCode !== 201) return exit(`Failed to push: ${requestError(response)}`);
376
384