@tutti-os/app-release-tools 0.0.78 → 0.0.79
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/README.md +26 -11
- package/bin/build-tutti-app-catalog.mjs +224 -64
- package/bin/build-tutti-app-release.mjs +4 -0
- package/bin/build-tutti-app-versions.mjs +236 -0
- package/bin/merge-tutti-app-latest.mjs +70 -0
- package/bin/publish-tutti-app-catalog.mjs +199 -0
- package/bin/publish-tutti-app-metadata.mjs +365 -0
- package/bin/tutti-app-versioning.mjs +44 -0
- package/bin/verify-tutti-app-release-artifacts.mjs +96 -124
- package/package.json +12 -1
|
@@ -5,100 +5,76 @@ import { readFile } from "node:fs/promises";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
7
|
|
|
8
|
+
import {
|
|
9
|
+
validateCatalog,
|
|
10
|
+
validateCatalogApp
|
|
11
|
+
} from "./build-tutti-app-catalog.mjs";
|
|
8
12
|
import { validateRelease } from "./build-tutti-app-release.mjs";
|
|
9
|
-
|
|
10
|
-
const catalogSchemaVersion = "tutti.app.catalog.v1";
|
|
13
|
+
import { validateVersionsDocument } from "./build-tutti-app-versions.mjs";
|
|
11
14
|
|
|
12
15
|
export async function verifyTuttiAppReleaseArtifacts(options) {
|
|
13
16
|
const releaseFiles = normalizeFiles(options.releaseFiles);
|
|
17
|
+
const versionsFiles = normalizeFiles(options.versionsFiles);
|
|
14
18
|
const catalogFile = options.catalogFile
|
|
15
19
|
? path.resolve(String(options.catalogFile))
|
|
16
20
|
: null;
|
|
17
21
|
const verifyArtifacts = options.verifyArtifacts !== false;
|
|
18
22
|
|
|
19
|
-
if (releaseFiles.length === 0 && !catalogFile) {
|
|
23
|
+
if (releaseFiles.length === 0 && versionsFiles.length === 0 && !catalogFile) {
|
|
20
24
|
throw new Error(
|
|
21
|
-
"at least one --release-file or --catalog-file is required"
|
|
25
|
+
"at least one --release-file, --versions-file, or --catalog-file is required"
|
|
22
26
|
);
|
|
23
27
|
}
|
|
24
28
|
|
|
25
|
-
const
|
|
26
|
-
const
|
|
29
|
+
const releasesByKey = new Map();
|
|
30
|
+
for (const versionsFile of versionsFiles) {
|
|
31
|
+
const document = JSON.parse(await readFile(versionsFile, "utf8"));
|
|
32
|
+
validateVersionsDocument(document);
|
|
33
|
+
for (const record of document.versions) {
|
|
34
|
+
const key = releaseKey(record.release.appId, record.release.version);
|
|
35
|
+
if (releasesByKey.has(key)) {
|
|
36
|
+
throw new Error(`duplicate versions release ${key}`);
|
|
37
|
+
}
|
|
38
|
+
releasesByKey.set(key, record.release);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const checks = new Map();
|
|
27
43
|
for (const releaseFile of releaseFiles) {
|
|
28
44
|
const release = JSON.parse(await readFile(releaseFile, "utf8"));
|
|
29
45
|
validateRelease(release);
|
|
30
|
-
|
|
31
|
-
|
|
46
|
+
const key = releaseKey(release.appId, release.version);
|
|
47
|
+
const existing = releasesByKey.get(key);
|
|
48
|
+
if (existing && JSON.stringify(existing) !== JSON.stringify(release)) {
|
|
49
|
+
throw new Error(`release ${key} does not match versions metadata`);
|
|
32
50
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const checks = [];
|
|
38
|
-
for (const release of releases) {
|
|
39
|
-
checks.push({
|
|
40
|
-
appId: release.appId,
|
|
41
|
-
artifactUrl: release.artifactUrl,
|
|
42
|
-
artifactSha256: release.artifactSha256,
|
|
43
|
-
artifactSizeBytes: release.artifactSizeBytes,
|
|
44
|
-
source: `release ${release.appId}`
|
|
45
|
-
});
|
|
51
|
+
releasesByKey.set(key, release);
|
|
52
|
+
addArtifactCheck(checks, release, `release ${key}`);
|
|
46
53
|
}
|
|
47
54
|
|
|
48
55
|
if (catalogFile) {
|
|
49
56
|
const catalog = JSON.parse(await readFile(catalogFile, "utf8"));
|
|
50
57
|
validateCatalog(catalog);
|
|
51
|
-
const seenCatalogAppIDs = new Set();
|
|
52
58
|
for (const app of catalog.apps) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
addCatalogAppCheck(checks, releasesByKey, app, "catalog legacy app");
|
|
60
|
+
}
|
|
61
|
+
for (const [appId, entries] of Object.entries(
|
|
62
|
+
catalog.compatibility?.apps ?? {}
|
|
63
|
+
)) {
|
|
64
|
+
for (const entry of entries) {
|
|
65
|
+
addCatalogAppCheck(
|
|
66
|
+
checks,
|
|
67
|
+
releasesByKey,
|
|
68
|
+
entry.app,
|
|
69
|
+
`catalog compatibility app ${appId}`
|
|
62
70
|
);
|
|
63
71
|
}
|
|
64
|
-
for (const key of ["artifactUrl", "artifactSha256", "iconUrl"]) {
|
|
65
|
-
if (
|
|
66
|
-
typeof distribution[key] !== "string" ||
|
|
67
|
-
distribution[key].trim() === ""
|
|
68
|
-
) {
|
|
69
|
-
throw new Error(
|
|
70
|
-
`catalog app ${appId} distribution.${key} is required`
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
requireSHA256Hex(
|
|
75
|
-
distribution.artifactSha256,
|
|
76
|
-
`catalog app ${appId} distribution.artifactSha256`
|
|
77
|
-
);
|
|
78
|
-
|
|
79
|
-
const release = releasesByAppID.get(appId);
|
|
80
|
-
if (release) {
|
|
81
|
-
assertCatalogMatchesRelease(app, release);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
checks.push({
|
|
85
|
-
appId,
|
|
86
|
-
artifactUrl: distribution.artifactUrl,
|
|
87
|
-
artifactSha256: distribution.artifactSha256,
|
|
88
|
-
artifactSizeBytes: release?.artifactSizeBytes,
|
|
89
|
-
source: `catalog ${appId}`
|
|
90
|
-
});
|
|
91
72
|
}
|
|
92
73
|
}
|
|
93
74
|
|
|
94
75
|
if (verifyArtifacts) {
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
let digest = artifactDigests.get(check.artifactUrl);
|
|
98
|
-
if (!digest) {
|
|
99
|
-
digest = await digestArtifact(check.artifactUrl);
|
|
100
|
-
artifactDigests.set(check.artifactUrl, digest);
|
|
101
|
-
}
|
|
76
|
+
for (const check of checks.values()) {
|
|
77
|
+
const digest = await digestArtifact(check.artifactUrl);
|
|
102
78
|
if (digest.sha256 !== check.artifactSha256.toLowerCase()) {
|
|
103
79
|
throw new Error(
|
|
104
80
|
`${check.source} artifact sha256 mismatch: want ${check.artifactSha256} got ${digest.sha256}`
|
|
@@ -118,12 +94,49 @@ export async function verifyTuttiAppReleaseArtifacts(options) {
|
|
|
118
94
|
return {
|
|
119
95
|
catalogFile,
|
|
120
96
|
releaseFiles,
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
: 0
|
|
97
|
+
versionsFiles,
|
|
98
|
+
checkedArtifactCount: verifyArtifacts ? checks.size : 0
|
|
124
99
|
};
|
|
125
100
|
}
|
|
126
101
|
|
|
102
|
+
function addCatalogAppCheck(checks, releasesByKey, app, source) {
|
|
103
|
+
const appId = validateCatalogApp(app, source);
|
|
104
|
+
const version = app.manifest.version;
|
|
105
|
+
const release = releasesByKey.get(releaseKey(appId, version));
|
|
106
|
+
if (release) assertCatalogMatchesRelease(app, release);
|
|
107
|
+
addArtifactCheck(
|
|
108
|
+
checks,
|
|
109
|
+
{
|
|
110
|
+
appId,
|
|
111
|
+
version,
|
|
112
|
+
artifactUrl: app.distribution.artifactUrl,
|
|
113
|
+
artifactSha256: app.distribution.artifactSha256,
|
|
114
|
+
artifactSizeBytes: release?.artifactSizeBytes
|
|
115
|
+
},
|
|
116
|
+
`${source} ${appId}@${version}`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function addArtifactCheck(checks, release, source) {
|
|
121
|
+
const existing = checks.get(release.artifactUrl);
|
|
122
|
+
const check = {
|
|
123
|
+
artifactUrl: release.artifactUrl,
|
|
124
|
+
artifactSha256: release.artifactSha256,
|
|
125
|
+
artifactSizeBytes: release.artifactSizeBytes,
|
|
126
|
+
source
|
|
127
|
+
};
|
|
128
|
+
if (
|
|
129
|
+
existing &&
|
|
130
|
+
(existing.artifactSha256 !== check.artifactSha256 ||
|
|
131
|
+
(Number.isSafeInteger(existing.artifactSizeBytes) &&
|
|
132
|
+
Number.isSafeInteger(check.artifactSizeBytes) &&
|
|
133
|
+
existing.artifactSizeBytes !== check.artifactSizeBytes))
|
|
134
|
+
) {
|
|
135
|
+
throw new Error(`artifact metadata conflicts for ${release.artifactUrl}`);
|
|
136
|
+
}
|
|
137
|
+
checks.set(release.artifactUrl, existing ?? check);
|
|
138
|
+
}
|
|
139
|
+
|
|
127
140
|
function assertCatalogMatchesRelease(app, release) {
|
|
128
141
|
const appId = release.appId;
|
|
129
142
|
const distribution = app.distribution;
|
|
@@ -157,31 +170,6 @@ function assertCatalogMatchesRelease(app, release) {
|
|
|
157
170
|
}
|
|
158
171
|
}
|
|
159
172
|
|
|
160
|
-
function validateCatalog(catalog) {
|
|
161
|
-
if (!catalog || typeof catalog !== "object") {
|
|
162
|
-
throw new Error("catalog must be an object");
|
|
163
|
-
}
|
|
164
|
-
if (catalog.schemaVersion !== catalogSchemaVersion) {
|
|
165
|
-
throw new Error(`catalog schemaVersion must be ${catalogSchemaVersion}`);
|
|
166
|
-
}
|
|
167
|
-
if (!Array.isArray(catalog.apps)) {
|
|
168
|
-
throw new Error("catalog apps must be an array");
|
|
169
|
-
}
|
|
170
|
-
for (const [index, app] of catalog.apps.entries()) {
|
|
171
|
-
if (!app || typeof app !== "object") {
|
|
172
|
-
throw new Error(`catalog apps[${index}] must be an object`);
|
|
173
|
-
}
|
|
174
|
-
if (
|
|
175
|
-
!app.manifest ||
|
|
176
|
-
typeof app.manifest !== "object" ||
|
|
177
|
-
typeof app.manifest.appId !== "string" ||
|
|
178
|
-
app.manifest.appId.trim() === ""
|
|
179
|
-
) {
|
|
180
|
-
throw new Error(`catalog apps[${index}].manifest.appId is required`);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
173
|
async function digestArtifact(artifactUrl) {
|
|
186
174
|
const url = String(artifactUrl).trim();
|
|
187
175
|
if (url.startsWith("http://") || url.startsWith("https://")) {
|
|
@@ -200,9 +188,7 @@ async function digestHTTPArtifact(url) {
|
|
|
200
188
|
return await digestHTTPArtifactOnce(url);
|
|
201
189
|
} catch (error) {
|
|
202
190
|
lastError = error;
|
|
203
|
-
if (attempt < 3)
|
|
204
|
-
await delay(1000 * attempt);
|
|
205
|
-
}
|
|
191
|
+
if (attempt < 3) await delay(1000 * attempt);
|
|
206
192
|
}
|
|
207
193
|
}
|
|
208
194
|
throw lastError;
|
|
@@ -222,10 +208,7 @@ async function digestHTTPArtifactOnce(url) {
|
|
|
222
208
|
hash.update(buffer);
|
|
223
209
|
size += buffer.length;
|
|
224
210
|
}
|
|
225
|
-
return {
|
|
226
|
-
sha256: hash.digest("hex"),
|
|
227
|
-
size
|
|
228
|
-
};
|
|
211
|
+
return { sha256: hash.digest("hex"), size };
|
|
229
212
|
}
|
|
230
213
|
|
|
231
214
|
function delay(ms) {
|
|
@@ -244,57 +227,46 @@ async function digestFileArtifact(filePath) {
|
|
|
244
227
|
stream.on("error", reject);
|
|
245
228
|
stream.on("end", resolve);
|
|
246
229
|
});
|
|
247
|
-
return {
|
|
248
|
-
sha256: hash.digest("hex"),
|
|
249
|
-
size
|
|
250
|
-
};
|
|
230
|
+
return { sha256: hash.digest("hex"), size };
|
|
251
231
|
}
|
|
252
232
|
|
|
253
233
|
function normalizeFiles(value) {
|
|
254
234
|
const files = Array.isArray(value)
|
|
255
235
|
? value
|
|
256
236
|
: String(value ?? "")
|
|
257
|
-
.split(/[\n,]/)
|
|
237
|
+
.split(/[\n,]/u)
|
|
258
238
|
.map((file) => file.trim())
|
|
259
239
|
.filter(Boolean);
|
|
260
240
|
return files.map((file) => path.resolve(file));
|
|
261
241
|
}
|
|
262
242
|
|
|
263
|
-
function
|
|
264
|
-
|
|
265
|
-
throw new Error(`${label} must be a sha256 hex digest`);
|
|
266
|
-
}
|
|
243
|
+
function releaseKey(appId, version) {
|
|
244
|
+
return `${appId}@${version}`;
|
|
267
245
|
}
|
|
268
246
|
|
|
269
247
|
function parseArgs(argv) {
|
|
270
248
|
const result = {
|
|
271
249
|
releaseFiles: [],
|
|
250
|
+
versionsFiles: [],
|
|
272
251
|
verifyArtifacts: true
|
|
273
252
|
};
|
|
274
253
|
for (let index = 0; index < argv.length; index += 1) {
|
|
275
254
|
const arg = argv[index];
|
|
276
|
-
if (arg === "--
|
|
277
|
-
|
|
278
|
-
if (!value || value.startsWith("--")) {
|
|
279
|
-
throw new Error("missing value for --release-file");
|
|
280
|
-
}
|
|
281
|
-
result.releaseFiles.push(value);
|
|
282
|
-
index += 1;
|
|
255
|
+
if (arg === "--skip-artifact-download") {
|
|
256
|
+
result.verifyArtifacts = false;
|
|
283
257
|
continue;
|
|
284
258
|
}
|
|
285
|
-
|
|
286
|
-
|
|
259
|
+
const value = argv[index + 1];
|
|
260
|
+
if (["--release-file", "--versions-file", "--catalog-file"].includes(arg)) {
|
|
287
261
|
if (!value || value.startsWith("--")) {
|
|
288
|
-
throw new Error(
|
|
262
|
+
throw new Error(`missing value for ${arg}`);
|
|
289
263
|
}
|
|
290
|
-
result.
|
|
264
|
+
if (arg === "--release-file") result.releaseFiles.push(value);
|
|
265
|
+
if (arg === "--versions-file") result.versionsFiles.push(value);
|
|
266
|
+
if (arg === "--catalog-file") result.catalogFile = value;
|
|
291
267
|
index += 1;
|
|
292
268
|
continue;
|
|
293
269
|
}
|
|
294
|
-
if (arg === "--skip-artifact-download") {
|
|
295
|
-
result.verifyArtifacts = false;
|
|
296
|
-
continue;
|
|
297
|
-
}
|
|
298
270
|
throw new Error(`unexpected argument: ${arg}`);
|
|
299
271
|
}
|
|
300
272
|
return result;
|
package/package.json
CHANGED
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tutti-os/app-release-tools",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.79",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"build-tutti-app-release": "./bin/build-tutti-app-release.mjs",
|
|
8
8
|
"build-tutti-app-catalog": "./bin/build-tutti-app-catalog.mjs",
|
|
9
|
+
"build-tutti-app-versions": "./bin/build-tutti-app-versions.mjs",
|
|
10
|
+
"merge-tutti-app-latest": "./bin/merge-tutti-app-latest.mjs",
|
|
11
|
+
"publish-tutti-app-catalog": "./bin/publish-tutti-app-catalog.mjs",
|
|
12
|
+
"publish-tutti-app-metadata": "./bin/publish-tutti-app-metadata.mjs",
|
|
9
13
|
"verify-tutti-app-release-artifacts": "./bin/verify-tutti-app-release-artifacts.mjs"
|
|
10
14
|
},
|
|
11
15
|
"exports": {
|
|
12
16
|
"./build-tutti-app-release": "./bin/build-tutti-app-release.mjs",
|
|
13
17
|
"./build-tutti-app-catalog": "./bin/build-tutti-app-catalog.mjs",
|
|
18
|
+
"./build-tutti-app-versions": "./bin/build-tutti-app-versions.mjs",
|
|
19
|
+
"./merge-tutti-app-latest": "./bin/merge-tutti-app-latest.mjs",
|
|
20
|
+
"./publish-tutti-app-catalog": "./bin/publish-tutti-app-catalog.mjs",
|
|
21
|
+
"./publish-tutti-app-metadata": "./bin/publish-tutti-app-metadata.mjs",
|
|
14
22
|
"./verify-tutti-app-release-artifacts": "./bin/verify-tutti-app-release-artifacts.mjs"
|
|
15
23
|
},
|
|
16
24
|
"files": [
|
|
@@ -25,6 +33,9 @@
|
|
|
25
33
|
"publishConfig": {
|
|
26
34
|
"access": "public"
|
|
27
35
|
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"semver": "7.8.0"
|
|
38
|
+
},
|
|
28
39
|
"scripts": {
|
|
29
40
|
"build": "node -e \"\"",
|
|
30
41
|
"test": "node --test ../../../tools/scripts/build-tutti-app-release.test.mjs"
|