cloudron 8.1.1 → 8.2.0
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/CHANGELOG.md +31 -0
- package/bin/cloudron +12 -0
- package/package.json +4 -4
- package/src/appstore-actions.js +38 -9
- package/src/versions-actions.js +81 -48
package/CHANGELOG.md
CHANGED
|
@@ -94,3 +94,34 @@
|
|
|
94
94
|
* Add `cloudron sync push` and `cloudron sync pull` subcommands
|
|
95
95
|
* Merge all binaries into single `cloudron` command
|
|
96
96
|
|
|
97
|
+
[7.1.1]
|
|
98
|
+
* Prettier login success page
|
|
99
|
+
* oidc: explicitly listen on ipv4 which is always available
|
|
100
|
+
|
|
101
|
+
[7.1.2]
|
|
102
|
+
* versions: implement revoke --version
|
|
103
|
+
* Make the login page slightly more like the dashboard style and add dark mode
|
|
104
|
+
|
|
105
|
+
[8.0.0]
|
|
106
|
+
* Implement device authorization grant flow
|
|
107
|
+
|
|
108
|
+
[8.0.1]
|
|
109
|
+
* device codes only needs to be confirmed not entered
|
|
110
|
+
* Add fallback in case the overloaded cloudron build --help is called
|
|
111
|
+
|
|
112
|
+
[8.0.2]
|
|
113
|
+
* install/update: -f and --build-arg
|
|
114
|
+
* wait even in debug mode, but just not for healthy
|
|
115
|
+
* add cloudron init --template <template>
|
|
116
|
+
* Fix cloudron pull as newer nodejs versions do not have resume() on stdout anymore
|
|
117
|
+
|
|
118
|
+
[8.1.0]
|
|
119
|
+
* Continue cloudron build even if run outside a git repository
|
|
120
|
+
* Remove old legacy appstore login/logout
|
|
121
|
+
|
|
122
|
+
[8.1.1]
|
|
123
|
+
* token is not required. verify-manifest does not need it
|
|
124
|
+
|
|
125
|
+
[8.1.2]
|
|
126
|
+
* add appstore login/logout and also forum post logs
|
|
127
|
+
|
package/bin/cloudron
CHANGED
|
@@ -29,6 +29,14 @@ program.option('--server <server>', 'Cloudron domain')
|
|
|
29
29
|
const appstoreCommand = program.command('appstore').description('Commands for publishing to the Appstore')
|
|
30
30
|
.option('--appstore-token <token>', 'AppStore token');
|
|
31
31
|
|
|
32
|
+
appstoreCommand.command('login')
|
|
33
|
+
.description('Login to the appstore')
|
|
34
|
+
.action(appstoreActions.login);
|
|
35
|
+
|
|
36
|
+
appstoreCommand.command('logout')
|
|
37
|
+
.description('Logout from the appstore')
|
|
38
|
+
.action(appstoreActions.logout);
|
|
39
|
+
|
|
32
40
|
appstoreCommand.command('info')
|
|
33
41
|
.description('List info of published app')
|
|
34
42
|
.option('--appstore-id <appid@version>', 'Appstore id and version')
|
|
@@ -429,4 +437,8 @@ versionsCommand.command('update')
|
|
|
429
437
|
.option('--state <state>', 'Publish state (published or testing)')
|
|
430
438
|
.action(versionsActions.addOrUpdate);
|
|
431
439
|
|
|
440
|
+
versionsCommand.command('verify')
|
|
441
|
+
.description('Verify if versions file is valid for publishing')
|
|
442
|
+
.action(versionsActions.verify);
|
|
443
|
+
|
|
432
444
|
program.parse(process.argv);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cloudron",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.2.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Cloudron Commandline Tool",
|
|
6
6
|
"type": "module",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"commander": "^14.0.3",
|
|
24
24
|
"debug": "^4.4.3",
|
|
25
25
|
"easy-table": "^1.2.0",
|
|
26
|
-
"ejs": "^5.0.
|
|
26
|
+
"ejs": "^5.0.2",
|
|
27
27
|
"eventsource": "^4.1.0",
|
|
28
28
|
"micromatch": "^4.0.8",
|
|
29
29
|
"open": "^11.0.0",
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@eslint/js": "^10.0.1",
|
|
38
|
-
"eslint": "^10.0
|
|
39
|
-
"globals": "^17.
|
|
38
|
+
"eslint": "^10.2.0",
|
|
39
|
+
"globals": "^17.5.0",
|
|
40
40
|
"mocha": "^11.7.5"
|
|
41
41
|
}
|
|
42
42
|
}
|
package/src/appstore-actions.js
CHANGED
|
@@ -2,7 +2,7 @@ import assert from 'assert';
|
|
|
2
2
|
import * as config from './config.js';
|
|
3
3
|
import { execSync } from 'child_process';
|
|
4
4
|
import fs from 'fs';
|
|
5
|
-
import { exit, locateManifest, parseChangelog } from './helper.js';
|
|
5
|
+
import { exit, locateManifest, parseChangelog, performOidcLogin } from './helper.js';
|
|
6
6
|
import manifestFormat from '@cloudron/manifest-format';
|
|
7
7
|
import path from 'path';
|
|
8
8
|
import safe from '@cloudron/safetydance';
|
|
@@ -14,7 +14,7 @@ const NO_MANIFEST_FOUND_ERROR_STRING = 'No CloudronManifest.json found';
|
|
|
14
14
|
function requestError(response) {
|
|
15
15
|
if (response.status === 401) return 'Invalid token.';
|
|
16
16
|
|
|
17
|
-
return `${response.status} message: ${response.body?.message ||
|
|
17
|
+
return `${response.status} message: ${response.body?.message || JSON.stringify(response.body)}`;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
function createRequest(method, apiPath, options) {
|
|
@@ -392,38 +392,67 @@ async function notify() {
|
|
|
392
392
|
|
|
393
393
|
if (!manifest.forumUrl) return exit(new Error('CloudronManifest.json does not have a forumUrl'));
|
|
394
394
|
const categoryMatch = manifest.forumUrl.match(/category\/(.*)\//);
|
|
395
|
-
if (!categoryMatch) return exit(
|
|
395
|
+
if (!categoryMatch) return exit(`Unable to detect category id from forumUrl: ${manifest.forumUrl}`);
|
|
396
396
|
const categoryId = categoryMatch[1];
|
|
397
397
|
|
|
398
|
+
console.log(`[notify] appId: ${manifest.id} version: ${manifest.version} forumUrl: ${manifest.forumUrl} categoryId: ${categoryId}`);
|
|
399
|
+
|
|
398
400
|
const categoryResponse = await superagent.get(`https://forum.cloudron.io/api/v3/categories/${categoryId}/topics`).set('Authorization', `Bearer ${apiToken}`).ok(() => true);
|
|
399
|
-
if (categoryResponse.status !== 200) return exit(`Unable to get topics of category: ${requestError(categoryResponse)}`);
|
|
400
|
-
|
|
401
|
+
if (categoryResponse.status !== 200) return exit(`Unable to get topics of category (status ${categoryResponse.status}): ${requestError(categoryResponse)}`);
|
|
402
|
+
|
|
403
|
+
const allTopics = categoryResponse.body.response.topics;
|
|
404
|
+
console.log(`[notify] Got ${allTopics.length} topics in category. Titles: ${allTopics.map(t => t.title).join(', ')}`);
|
|
405
|
+
|
|
406
|
+
const topic = allTopics.find(t => t.title.toLowerCase().includes('package updates'));
|
|
401
407
|
if (!topic) return exit('Could not find the Package Update topic');
|
|
402
408
|
const topicId = topic.tid;
|
|
409
|
+
console.log(`[notify] Found "Package Updates" topic tid: ${topicId}`);
|
|
403
410
|
|
|
404
411
|
const pageCountResponse = await superagent.get(`https://forum.cloudron.io/api/topic/pagination/${topicId}`).set('Authorization', `Bearer ${apiToken}`).ok(() => true);
|
|
405
|
-
if (pageCountResponse.status !== 200) return exit(`Unable to get page count of topic: ${requestError(pageCountResponse)}`);
|
|
412
|
+
if (pageCountResponse.status !== 200) return exit(`Unable to get page count of topic (status ${pageCountResponse.status}): ${requestError(pageCountResponse)}`);
|
|
406
413
|
const pageCount = pageCountResponse.body.pagination.pageCount;
|
|
414
|
+
console.log(`[notify] Topic has ${pageCount} pages to scan for duplicates`);
|
|
407
415
|
|
|
408
416
|
for (let page = 1; page <= pageCount; page++) {
|
|
409
417
|
const pageResponse = await superagent.get(`https://forum.cloudron.io/api/topic/${topicId}?page=${page}`).set('Authorization', `Bearer ${apiToken}`).ok(() => true);
|
|
410
|
-
if (pageResponse.status !== 200) return exit(`Unable to get
|
|
411
|
-
|
|
418
|
+
if (pageResponse.status !== 200) return exit(`Unable to get posts of topic page ${page} (status ${pageResponse.status}): ${requestError(pageResponse)}`);
|
|
419
|
+
console.log(`[notify] Page ${page}/${pageCount}: ${pageResponse.body.posts.length} posts`);
|
|
420
|
+
for (const post of pageResponse.body.posts) {
|
|
412
421
|
if (post.content.includes(`[${manifest.version}]`)) return exit(`Version ${manifest.version} is already on the forum.\n${post.content}`);
|
|
413
422
|
}
|
|
414
423
|
}
|
|
415
424
|
|
|
425
|
+
console.log(`[notify] No duplicate found, posting version ${manifest.version}`);
|
|
426
|
+
|
|
416
427
|
// https://docs.nodebb.org/api/write/#tag/topics/paths/~1topics~1%7Btid%7D/post
|
|
417
428
|
const postData = {
|
|
418
429
|
content: postContent,
|
|
419
430
|
toPid: 0 // which post is this post a reply to
|
|
420
431
|
};
|
|
421
432
|
const postResponse = await superagent.post(`https://forum.cloudron.io/api/v3/topics/${topicId}`).set('Authorization', `Bearer ${apiToken}`).send(postData).ok(() => true);
|
|
422
|
-
if (postResponse.status !== 200) return exit(`Unable to create changelog post: ${requestError(postResponse)}`);
|
|
433
|
+
if (postResponse.status !== 200) return exit(`Unable to create changelog post (status ${postResponse.status}): ${requestError(postResponse)}`);
|
|
423
434
|
console.log('Posted to forum');
|
|
424
435
|
}
|
|
425
436
|
|
|
437
|
+
async function login() {
|
|
438
|
+
const origin = config.appStoreOrigin();
|
|
439
|
+
const consoleFqdn = origin.replace('https://', '').replace('api.', 'console.') + '/openid';
|
|
440
|
+
|
|
441
|
+
const token = await performOidcLogin(consoleFqdn);
|
|
442
|
+
if (!token) process.exit(1);
|
|
443
|
+
|
|
444
|
+
config.setAppStoreToken(token);
|
|
445
|
+
console.log('Login successful.');
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function logout() {
|
|
449
|
+
config.setAppStoreToken(null);
|
|
450
|
+
console.log('Logged out.');
|
|
451
|
+
}
|
|
452
|
+
|
|
426
453
|
export default {
|
|
454
|
+
login,
|
|
455
|
+
logout,
|
|
427
456
|
info,
|
|
428
457
|
listVersions,
|
|
429
458
|
submit,
|
package/src/versions-actions.js
CHANGED
|
@@ -31,51 +31,6 @@ async function writeVersions(versionsFilePath, versionsRoot) {
|
|
|
31
31
|
if (error) return exit(`Unable to write to ${path.relative(process.cwd(), versionsFilePath)}: ${error.message}`);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
async function init(/*localOptions, cmd*/) {
|
|
35
|
-
const manifestFilePath = locateManifest();
|
|
36
|
-
if (!manifestFilePath) return exit(`${NO_MANIFEST_FOUND_ERROR_STRING}. You must run this command in the package dir.`);
|
|
37
|
-
|
|
38
|
-
const baseDir = path.dirname(manifestFilePath);
|
|
39
|
-
const versionsFilePath = `${baseDir}/CloudronVersions.json`;
|
|
40
|
-
|
|
41
|
-
if (fs.existsSync(versionsFilePath)) return exit(`${path.relative(process.cwd(), versionsFilePath)} already exists.`);
|
|
42
|
-
|
|
43
|
-
await writeVersions(versionsFilePath, { stable: true, versions: {} });
|
|
44
|
-
console.log(`Created ${path.relative(process.cwd(), versionsFilePath)}.`);
|
|
45
|
-
|
|
46
|
-
const result = manifestFormat.parseFile(manifestFilePath);
|
|
47
|
-
if (!result.error) {
|
|
48
|
-
ensurePublishFields(result.manifest, manifestFilePath);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
console.log('\nUse "cloudron versions add" to add a version.');
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
async function resolveManifest(manifest, baseDir) {
|
|
55
|
-
if (manifest.description.slice(0, 7) === 'file://') {
|
|
56
|
-
let descriptionFilePath = manifest.description.slice(7);
|
|
57
|
-
descriptionFilePath = path.isAbsolute(descriptionFilePath) ? descriptionFilePath : path.join(baseDir, descriptionFilePath);
|
|
58
|
-
manifest.description = safe.fs.readFileSync(descriptionFilePath, 'utf8');
|
|
59
|
-
if (!manifest.description && safe.error) throw(new Error('Could not read/parse description ' + safe.error.message));
|
|
60
|
-
if (!manifest.description) throw new Error('Description cannot be empty');
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (manifest.postInstallMessage && manifest.postInstallMessage.slice(0, 7) === 'file://') {
|
|
64
|
-
let postInstallFilePath = manifest.postInstallMessage.slice(7);
|
|
65
|
-
postInstallFilePath = path.isAbsolute(postInstallFilePath) ? postInstallFilePath : path.join(baseDir, postInstallFilePath);
|
|
66
|
-
manifest.postInstallMessage = safe.fs.readFileSync(postInstallFilePath, 'utf8');
|
|
67
|
-
if (!manifest.postInstallMessage && safe.error) throw(new Error('Could not read/parse postInstall ' + safe.error.message));
|
|
68
|
-
if (!manifest.postInstallMessage) throw new Error('PostInstall file specified but it is empty');
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (manifest.changelog.slice(0, 7) === 'file://') {
|
|
72
|
-
let changelogPath = manifest.changelog.slice(7);
|
|
73
|
-
changelogPath = path.isAbsolute(changelogPath) ? changelogPath : path.join(baseDir, changelogPath);
|
|
74
|
-
manifest.changelog = parseChangelog(changelogPath, manifest.version);
|
|
75
|
-
if (!manifest.changelog) throw new Error('Bad changelog format or missing changelog for this version');
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
34
|
function createStubFile(filePath, content) {
|
|
80
35
|
if (fs.existsSync(filePath)) return false;
|
|
81
36
|
fs.writeFileSync(filePath, content, 'utf8');
|
|
@@ -162,6 +117,55 @@ function ensurePublishFields(manifest, manifestFilePath) {
|
|
|
162
117
|
console.log(' tags, mediaLinks, DESCRIPTION.md, CHANGELOG, POSTINSTALL.md');
|
|
163
118
|
}
|
|
164
119
|
|
|
120
|
+
async function init(/*localOptions, cmd*/) {
|
|
121
|
+
const manifestFilePath = locateManifest();
|
|
122
|
+
if (!manifestFilePath) return exit(`${NO_MANIFEST_FOUND_ERROR_STRING}. You must run this command in the package dir.`);
|
|
123
|
+
|
|
124
|
+
const baseDir = path.dirname(manifestFilePath);
|
|
125
|
+
const versionsFilePath = `${baseDir}/CloudronVersions.json`;
|
|
126
|
+
|
|
127
|
+
if (fs.existsSync(versionsFilePath)) return exit(`${path.relative(process.cwd(), versionsFilePath)} already exists.`);
|
|
128
|
+
|
|
129
|
+
await writeVersions(versionsFilePath, { stable: true, versions: {} });
|
|
130
|
+
console.log(`Created ${path.relative(process.cwd(), versionsFilePath)}.`);
|
|
131
|
+
|
|
132
|
+
const result = manifestFormat.parseFile(manifestFilePath);
|
|
133
|
+
if (!result.error) {
|
|
134
|
+
ensurePublishFields(result.manifest, manifestFilePath);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
console.log('\nUse "cloudron versions add" to add a version.');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function resolveManifest(manifest, baseDir) {
|
|
141
|
+
const resolved = structuredClone(manifest);
|
|
142
|
+
|
|
143
|
+
if (resolved.description.slice(0, 7) === 'file://') {
|
|
144
|
+
let descriptionFilePath = resolved.description.slice(7);
|
|
145
|
+
descriptionFilePath = path.isAbsolute(descriptionFilePath) ? descriptionFilePath : path.join(baseDir, descriptionFilePath);
|
|
146
|
+
resolved.description = safe.fs.readFileSync(descriptionFilePath, 'utf8');
|
|
147
|
+
if (!resolved.description && safe.error) throw(new Error('Could not read/parse description ' + safe.error.message));
|
|
148
|
+
if (!resolved.description) throw new Error('Description cannot be empty');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (resolved.postInstallMessage && resolved.postInstallMessage.slice(0, 7) === 'file://') {
|
|
152
|
+
let postInstallFilePath = resolved.postInstallMessage.slice(7);
|
|
153
|
+
postInstallFilePath = path.isAbsolute(postInstallFilePath) ? postInstallFilePath : path.join(baseDir, postInstallFilePath);
|
|
154
|
+
resolved.postInstallMessage = safe.fs.readFileSync(postInstallFilePath, 'utf8');
|
|
155
|
+
if (!resolved.postInstallMessage && safe.error) throw(new Error('Could not read/parse postInstall ' + safe.error.message));
|
|
156
|
+
if (!resolved.postInstallMessage) throw new Error('PostInstall file specified but it is empty');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (resolved.changelog.slice(0, 7) === 'file://') {
|
|
160
|
+
let changelogPath = resolved.changelog.slice(7);
|
|
161
|
+
changelogPath = path.isAbsolute(changelogPath) ? changelogPath : path.join(baseDir, changelogPath);
|
|
162
|
+
resolved.changelog = parseChangelog(changelogPath, resolved.version);
|
|
163
|
+
if (!resolved.changelog) throw new Error('Bad changelog format or missing changelog for this version');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return resolved;
|
|
167
|
+
}
|
|
168
|
+
|
|
165
169
|
async function addOrUpdate(localOptions, cmd) {
|
|
166
170
|
const isUpdate = cmd.parent.args[0] === 'update';
|
|
167
171
|
const versionsFilePath = await locateVersions();
|
|
@@ -194,7 +198,7 @@ async function addOrUpdate(localOptions, cmd) {
|
|
|
194
198
|
const versionsRoot = await readVersions(versionsFilePath);
|
|
195
199
|
const versions = versionsRoot.versions;
|
|
196
200
|
|
|
197
|
-
await resolveManifest(manifest, path.dirname(manifestFilePath));
|
|
201
|
+
const resolved = await resolveManifest(manifest, path.dirname(manifestFilePath));
|
|
198
202
|
|
|
199
203
|
if (options.state && options.state !== PUBLISH_STATE_PUBLISHED && options.state !== PUBLISH_STATE_TESTING) {
|
|
200
204
|
return exit(`Invalid state "${options.state}". Must be "published" or "testing".`);
|
|
@@ -204,13 +208,13 @@ async function addOrUpdate(localOptions, cmd) {
|
|
|
204
208
|
const targetVersion = options.version || manifest.version;
|
|
205
209
|
if (!(targetVersion in versions)) exit(`${targetVersion} does not exist in ${path.relative(process.cwd(), versionsFilePath)}.`);
|
|
206
210
|
|
|
207
|
-
versions[targetVersion].manifest =
|
|
211
|
+
versions[targetVersion].manifest = resolved;
|
|
208
212
|
versions[targetVersion].ts = (new Date()).toUTCString();
|
|
209
213
|
if (options.state) versions[targetVersion].publishState = options.state;
|
|
210
214
|
} else {
|
|
211
215
|
if (manifest.version in versions) exit(`${manifest.version} already exists in ${path.relative(process.cwd(), versionsFilePath)}.`);
|
|
212
216
|
versions[manifest.version] = {
|
|
213
|
-
manifest,
|
|
217
|
+
manifest: resolved,
|
|
214
218
|
creationDate: (new Date()).toUTCString(),
|
|
215
219
|
ts: (new Date()).toUTCString(),
|
|
216
220
|
publishState: options.state || PUBLISH_STATE_PUBLISHED
|
|
@@ -278,9 +282,38 @@ async function revoke(localOptions, cmd) {
|
|
|
278
282
|
console.log(`Marked ${targetVersion} as revoked in ${path.relative(process.cwd(), versionsFilePath)}`);
|
|
279
283
|
}
|
|
280
284
|
|
|
285
|
+
async function verify(/* localOptions, cmd */) {
|
|
286
|
+
const versionsFilePath = await locateVersions();
|
|
287
|
+
if (!versionsFilePath) return exit(NO_VERSIONS_FOUND_ERROR_STRING);
|
|
288
|
+
|
|
289
|
+
const versionsRoot = await readVersions(versionsFilePath);
|
|
290
|
+
const versions = versionsRoot.versions;
|
|
291
|
+
|
|
292
|
+
const sortedVersions = Object.keys(versions).sort(manifestFormat.packageVersionCompare);
|
|
293
|
+
if (sortedVersions.length === 0) return exit('No versions found in versions file');
|
|
294
|
+
|
|
295
|
+
for (const version of sortedVersions) {
|
|
296
|
+
const manifest = versions[version].manifest;
|
|
297
|
+
|
|
298
|
+
if (!manifest.dockerImage) return exit(`Version ${version}: missing dockerImage`);
|
|
299
|
+
|
|
300
|
+
const error = manifestFormat.checkVersionsRequirements(manifest);
|
|
301
|
+
if (error) return exit(`Version ${version}: ${error}`);
|
|
302
|
+
|
|
303
|
+
try {
|
|
304
|
+
await resolveManifest(manifest, path.dirname(versionsFilePath));
|
|
305
|
+
} catch (e) {
|
|
306
|
+
return exit(`Version ${version}: ${e.message}`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
console.log(`${path.relative(process.cwd(), versionsFilePath)} is valid (${sortedVersions.length} version(s))`);
|
|
311
|
+
}
|
|
312
|
+
|
|
281
313
|
export default {
|
|
282
314
|
init,
|
|
283
315
|
addOrUpdate,
|
|
284
316
|
list,
|
|
285
317
|
revoke,
|
|
318
|
+
verify,
|
|
286
319
|
};
|