cloudron 5.11.7 → 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.
@@ -34,6 +34,10 @@ program.command('approve')
34
34
  .option('--appstore-id <appid@version>', 'Appstore id and version')
35
35
  .action(appstoreActions.approve);
36
36
 
37
+ program.command('notify')
38
+ .description('Notify forum about successful app submission')
39
+ .action(appstoreActions.notify);
40
+
37
41
  program.command('tag <version>')
38
42
  .description('Tag the repo')
39
43
  .action(appstoreActions.tag);
@@ -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.7",
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
- tag
28
+ tag,
29
+ notify
29
30
  };
30
31
 
31
32
  const NO_MANIFEST_FOUND_ERROR_STRING = 'No CloudronManifest.json found';
@@ -280,8 +281,19 @@ async function upload(localOptions, cmd) {
280
281
  const appConfig = config.getAppConfig(sourceDir);
281
282
 
282
283
  // image can be passed in options for buildbot
283
- const dockerImage = options.image ? options.image : appConfig.dockerImage;
284
- 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
+ }
285
297
 
286
298
  if (!manifest.dockerImage) exit('No docker image found, run `cloudron build` first');
287
299
 
@@ -291,6 +303,10 @@ async function upload(localOptions, cmd) {
291
303
  const error = manifestFormat.checkAppstoreRequirements(manifest);
292
304
  if (error) return exit(error);
293
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
+
294
310
  // ensure the app is known on the appstore side
295
311
  const baseDir = path.dirname(manifestFilePath);
296
312
 
@@ -427,3 +443,61 @@ async function tag(version) {
427
443
 
428
444
  console.log(`Created tag v${version} and pushed branch ${branch}`);
429
445
  }
446
+
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;
452
+
453
+ const manifestFilePath = locateManifest();
454
+ if (!manifestFilePath) return exit('Could not locate CloudronManifest.json');
455
+
456
+ const result = manifestFormat.parseFile(manifestFilePath);
457
+ if (result.error) return exit(new Error(`Invalid CloudronManifest.json: ${result.error.message}`));
458
+ const { manifest } = result;
459
+
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
+ }
494
+
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');
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