@tutti-os/app-release-tools 0.0.92 → 0.0.93
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.
|
@@ -4,6 +4,7 @@ import { pathToFileURL } from "node:url";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
|
|
6
6
|
import {
|
|
7
|
+
normalizeRequiredTuttiCapabilities,
|
|
7
8
|
releaseToCatalogApp,
|
|
8
9
|
validateRelease
|
|
9
10
|
} from "./build-tutti-app-release.mjs";
|
|
@@ -30,6 +31,7 @@ export async function buildTuttiAppCatalog(options) {
|
|
|
30
31
|
|
|
31
32
|
const appsByID = new Map();
|
|
32
33
|
const compatibilityByAppID = new Map();
|
|
34
|
+
const capabilityCompatibilityByAppID = new Map();
|
|
33
35
|
if (existingCatalogPath) {
|
|
34
36
|
const existingCatalog = JSON.parse(
|
|
35
37
|
await readFile(existingCatalogPath, "utf8")
|
|
@@ -43,6 +45,11 @@ export async function buildTuttiAppCatalog(options) {
|
|
|
43
45
|
)) {
|
|
44
46
|
compatibilityByAppID.set(appId, entries);
|
|
45
47
|
}
|
|
48
|
+
for (const [appId, entries] of Object.entries(
|
|
49
|
+
existingCatalog.compatibility?.capabilityApps ?? {}
|
|
50
|
+
)) {
|
|
51
|
+
capabilityCompatibilityByAppID.set(appId, entries);
|
|
52
|
+
}
|
|
46
53
|
}
|
|
47
54
|
|
|
48
55
|
const seenVersionsAppIDs = new Set();
|
|
@@ -53,7 +60,12 @@ export async function buildTuttiAppCatalog(options) {
|
|
|
53
60
|
throw new Error(`duplicate versions appId ${versions.appId}`);
|
|
54
61
|
}
|
|
55
62
|
seenVersionsAppIDs.add(versions.appId);
|
|
56
|
-
applyVersionsDocument(
|
|
63
|
+
applyVersionsDocument(
|
|
64
|
+
appsByID,
|
|
65
|
+
compatibilityByAppID,
|
|
66
|
+
capabilityCompatibilityByAppID,
|
|
67
|
+
versions
|
|
68
|
+
);
|
|
57
69
|
}
|
|
58
70
|
|
|
59
71
|
const seenReleaseAppIDs = new Set();
|
|
@@ -72,7 +84,11 @@ export async function buildTuttiAppCatalog(options) {
|
|
|
72
84
|
appsByID.set(release.appId, releaseToCatalogApp(release));
|
|
73
85
|
}
|
|
74
86
|
|
|
75
|
-
if (
|
|
87
|
+
if (
|
|
88
|
+
appsByID.size === 0 &&
|
|
89
|
+
compatibilityByAppID.size === 0 &&
|
|
90
|
+
capabilityCompatibilityByAppID.size === 0
|
|
91
|
+
) {
|
|
76
92
|
throw new Error(
|
|
77
93
|
"at least one versions file, release file, or existing catalog app is required"
|
|
78
94
|
);
|
|
@@ -86,11 +102,24 @@ export async function buildTuttiAppCatalog(options) {
|
|
|
86
102
|
.filter(([, entries]) => entries.length > 0)
|
|
87
103
|
.sort(([left], [right]) => left.localeCompare(right))
|
|
88
104
|
);
|
|
105
|
+
const capabilityApps = Object.fromEntries(
|
|
106
|
+
[...capabilityCompatibilityByAppID.entries()]
|
|
107
|
+
.filter(([, entries]) => entries.length > 0)
|
|
108
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
109
|
+
);
|
|
89
110
|
const catalog = {
|
|
90
111
|
schemaVersion: catalogSchemaVersion,
|
|
91
112
|
apps,
|
|
92
|
-
...(Object.keys(compatibilityApps).length > 0
|
|
93
|
-
|
|
113
|
+
...(Object.keys(compatibilityApps).length > 0 ||
|
|
114
|
+
Object.keys(capabilityApps).length > 0
|
|
115
|
+
? {
|
|
116
|
+
compatibility: {
|
|
117
|
+
apps: compatibilityApps,
|
|
118
|
+
...(Object.keys(capabilityApps).length > 0
|
|
119
|
+
? { capabilityApps }
|
|
120
|
+
: {})
|
|
121
|
+
}
|
|
122
|
+
}
|
|
94
123
|
: {})
|
|
95
124
|
};
|
|
96
125
|
validateCatalog(catalog);
|
|
@@ -141,7 +170,18 @@ export function validateCatalog(catalog) {
|
|
|
141
170
|
) {
|
|
142
171
|
throw new Error("catalog compatibility.apps must be an object");
|
|
143
172
|
}
|
|
144
|
-
|
|
173
|
+
validateCatalogCompatibilityApps(compatibility.apps);
|
|
174
|
+
if (compatibility.capabilityApps !== undefined) {
|
|
175
|
+
validateCatalogCapabilityApps(
|
|
176
|
+
compatibility.capabilityApps,
|
|
177
|
+
seenLegacyAppIDs
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
return catalog;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function validateCatalogCompatibilityApps(compatibilityApps) {
|
|
184
|
+
for (const [appId, entries] of Object.entries(compatibilityApps)) {
|
|
145
185
|
if (!Array.isArray(entries) || entries.length === 0) {
|
|
146
186
|
throw new Error(`catalog compatibility app ${appId} must be non-empty`);
|
|
147
187
|
}
|
|
@@ -169,7 +209,55 @@ export function validateCatalog(catalog) {
|
|
|
169
209
|
seenVersions.add(version);
|
|
170
210
|
}
|
|
171
211
|
}
|
|
172
|
-
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function validateCatalogCapabilityApps(capabilityApps, legacyAppIDs) {
|
|
215
|
+
if (
|
|
216
|
+
!capabilityApps ||
|
|
217
|
+
typeof capabilityApps !== "object" ||
|
|
218
|
+
Array.isArray(capabilityApps)
|
|
219
|
+
) {
|
|
220
|
+
throw new Error("catalog compatibility.capabilityApps must be an object");
|
|
221
|
+
}
|
|
222
|
+
for (const [rawAppID, entries] of Object.entries(capabilityApps)) {
|
|
223
|
+
const appId = String(rawAppID).trim();
|
|
224
|
+
if (appId === "" || !Array.isArray(entries) || entries.length === 0) {
|
|
225
|
+
throw new Error(
|
|
226
|
+
`catalog capability compatibility app ${rawAppID} must be non-empty`
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
if (!legacyAppIDs.has(appId)) {
|
|
230
|
+
throw new Error(
|
|
231
|
+
`catalog capability compatibility app ${appId} requires a legacy fallback app`
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
const seenCapabilitySets = new Set();
|
|
235
|
+
const seenVersions = new Set();
|
|
236
|
+
for (const [index, entry] of entries.entries()) {
|
|
237
|
+
const label = `catalog capability compatibility app ${appId}[${index}]`;
|
|
238
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
239
|
+
throw new Error(`${label} must be an object`);
|
|
240
|
+
}
|
|
241
|
+
const requiredTuttiCapabilities = normalizeRequiredTuttiCapabilities(
|
|
242
|
+
entry.requiredTuttiCapabilities,
|
|
243
|
+
`${label}.requiredTuttiCapabilities`
|
|
244
|
+
);
|
|
245
|
+
const capabilityKey = requiredTuttiCapabilities.join("\u0000");
|
|
246
|
+
if (seenCapabilitySets.has(capabilityKey)) {
|
|
247
|
+
throw new Error(`${label} has duplicate requiredTuttiCapabilities`);
|
|
248
|
+
}
|
|
249
|
+
seenCapabilitySets.add(capabilityKey);
|
|
250
|
+
const entryAppID = validateCatalogApp(entry.app, `${label}.app`);
|
|
251
|
+
if (entryAppID !== appId) {
|
|
252
|
+
throw new Error(`${label}.app manifest.appId must match map key`);
|
|
253
|
+
}
|
|
254
|
+
const version = entry.app.manifest.version;
|
|
255
|
+
if (seenVersions.has(version)) {
|
|
256
|
+
throw new Error(`${label} has duplicate app version ${version}`);
|
|
257
|
+
}
|
|
258
|
+
seenVersions.add(version);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
173
261
|
}
|
|
174
262
|
|
|
175
263
|
export function validateCatalogApp(app, label) {
|
|
@@ -200,7 +288,12 @@ export function validateCatalogApp(app, label) {
|
|
|
200
288
|
return appId;
|
|
201
289
|
}
|
|
202
290
|
|
|
203
|
-
function applyVersionsDocument(
|
|
291
|
+
function applyVersionsDocument(
|
|
292
|
+
appsByID,
|
|
293
|
+
compatibilityByAppID,
|
|
294
|
+
capabilityCompatibilityByAppID,
|
|
295
|
+
versions
|
|
296
|
+
) {
|
|
204
297
|
const activeRecords = versions.versions.filter(
|
|
205
298
|
(record) => record.status === "active"
|
|
206
299
|
);
|
|
@@ -214,7 +307,9 @@ function applyVersionsDocument(appsByID, compatibilityByAppID, versions) {
|
|
|
214
307
|
appsByID.delete(versions.appId);
|
|
215
308
|
}
|
|
216
309
|
|
|
217
|
-
const entries = compatibilityFrontier(
|
|
310
|
+
const entries = compatibilityFrontier(
|
|
311
|
+
activeRecords.filter((record) => record.minTuttiVersion !== undefined)
|
|
312
|
+
).map((record) => ({
|
|
218
313
|
minTuttiVersion: record.minTuttiVersion,
|
|
219
314
|
app: releaseToCatalogApp(record.release)
|
|
220
315
|
}));
|
|
@@ -223,6 +318,20 @@ function applyVersionsDocument(appsByID, compatibilityByAppID, versions) {
|
|
|
223
318
|
} else {
|
|
224
319
|
compatibilityByAppID.delete(versions.appId);
|
|
225
320
|
}
|
|
321
|
+
|
|
322
|
+
const capabilityEntries = capabilityCompatibilityFrontier(
|
|
323
|
+
activeRecords.filter(
|
|
324
|
+
(record) => record.requiredTuttiCapabilities !== undefined
|
|
325
|
+
)
|
|
326
|
+
).map((record) => ({
|
|
327
|
+
requiredTuttiCapabilities: record.requiredTuttiCapabilities,
|
|
328
|
+
app: releaseToCatalogApp(record.release)
|
|
329
|
+
}));
|
|
330
|
+
if (capabilityEntries.length > 0) {
|
|
331
|
+
capabilityCompatibilityByAppID.set(versions.appId, capabilityEntries);
|
|
332
|
+
} else {
|
|
333
|
+
capabilityCompatibilityByAppID.delete(versions.appId);
|
|
334
|
+
}
|
|
226
335
|
}
|
|
227
336
|
|
|
228
337
|
export function compatibilityFrontier(records) {
|
|
@@ -258,6 +367,27 @@ export function compatibilityFrontier(records) {
|
|
|
258
367
|
return frontier;
|
|
259
368
|
}
|
|
260
369
|
|
|
370
|
+
export function capabilityCompatibilityFrontier(records) {
|
|
371
|
+
const highestByCapabilitySet = new Map();
|
|
372
|
+
for (const record of records) {
|
|
373
|
+
const capabilityKey = record.requiredTuttiCapabilities.join("\u0000");
|
|
374
|
+
const existing = highestByCapabilitySet.get(capabilityKey);
|
|
375
|
+
if (
|
|
376
|
+
!existing ||
|
|
377
|
+
compareReleaseVersions(record.release, existing.release) > 0
|
|
378
|
+
) {
|
|
379
|
+
highestByCapabilitySet.set(capabilityKey, record);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return [...highestByCapabilitySet.values()].sort((left, right) => {
|
|
383
|
+
const leftKey = left.requiredTuttiCapabilities.join("\u0000");
|
|
384
|
+
const rightKey = right.requiredTuttiCapabilities.join("\u0000");
|
|
385
|
+
const keyComparison = leftKey.localeCompare(rightKey);
|
|
386
|
+
if (keyComparison !== 0) return keyComparison;
|
|
387
|
+
return compareReleaseVersions(left.release, right.release);
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
261
391
|
function highestReleaseRecord(records) {
|
|
262
392
|
return [...records]
|
|
263
393
|
.sort((left, right) => compareReleaseVersions(left.release, right.release))
|
|
@@ -167,9 +167,63 @@ export function validateManifest(manifest, sourceLabel = "manifest") {
|
|
|
167
167
|
}
|
|
168
168
|
validateManifestCLI(manifest.cli, sourceLabel);
|
|
169
169
|
validateManifestReferences(manifest.references, sourceLabel);
|
|
170
|
+
requiredTuttiCapabilitiesForManifest(manifest, sourceLabel);
|
|
170
171
|
validateLocalizationInfo(manifest.localizationInfo, sourceLabel);
|
|
171
172
|
}
|
|
172
173
|
|
|
174
|
+
export function requiredTuttiCapabilitiesForManifest(
|
|
175
|
+
manifest,
|
|
176
|
+
sourceLabel = "manifest"
|
|
177
|
+
) {
|
|
178
|
+
const compatibility = manifest.hostCompatibility;
|
|
179
|
+
if (compatibility === undefined) return [];
|
|
180
|
+
if (
|
|
181
|
+
!compatibility ||
|
|
182
|
+
typeof compatibility !== "object" ||
|
|
183
|
+
Array.isArray(compatibility)
|
|
184
|
+
) {
|
|
185
|
+
throw new Error(`${sourceLabel}.hostCompatibility must be an object`);
|
|
186
|
+
}
|
|
187
|
+
const unsupportedKey = Object.keys(compatibility).find(
|
|
188
|
+
(key) => key !== "requiredTuttiCapabilities"
|
|
189
|
+
);
|
|
190
|
+
if (unsupportedKey) {
|
|
191
|
+
throw new Error(
|
|
192
|
+
`${sourceLabel}.hostCompatibility.${unsupportedKey} is unsupported`
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
return normalizeRequiredTuttiCapabilities(
|
|
196
|
+
compatibility.requiredTuttiCapabilities,
|
|
197
|
+
`${sourceLabel}.hostCompatibility.requiredTuttiCapabilities`
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function normalizeRequiredTuttiCapabilities(value, label) {
|
|
202
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
203
|
+
throw new Error(`${label} must be a non-empty array`);
|
|
204
|
+
}
|
|
205
|
+
if (value.length > 32) {
|
|
206
|
+
throw new Error(`${label} must contain at most 32 capabilities`);
|
|
207
|
+
}
|
|
208
|
+
const capabilities = value.map((capability, index) => {
|
|
209
|
+
if (typeof capability !== "string" || capability.trim() !== capability) {
|
|
210
|
+
throw new Error(`${label}[${index}] must be a normalized string`);
|
|
211
|
+
}
|
|
212
|
+
if (!/^[a-z][a-z0-9-]{0,63}$/u.test(capability)) {
|
|
213
|
+
throw new Error(`${label}[${index}] is invalid`);
|
|
214
|
+
}
|
|
215
|
+
return capability;
|
|
216
|
+
});
|
|
217
|
+
const normalized = [...new Set(capabilities)].sort();
|
|
218
|
+
if (
|
|
219
|
+
normalized.length !== capabilities.length ||
|
|
220
|
+
normalized.some((capability, index) => capability !== capabilities[index])
|
|
221
|
+
) {
|
|
222
|
+
throw new Error(`${label} must be sorted and unique`);
|
|
223
|
+
}
|
|
224
|
+
return normalized;
|
|
225
|
+
}
|
|
226
|
+
|
|
173
227
|
function validateManifestCLI(cli, sourceLabel) {
|
|
174
228
|
if (cli === undefined) {
|
|
175
229
|
return;
|
|
@@ -3,7 +3,11 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
normalizeRequiredTuttiCapabilities,
|
|
8
|
+
requiredTuttiCapabilitiesForManifest,
|
|
9
|
+
validateRelease
|
|
10
|
+
} from "./build-tutti-app-release.mjs";
|
|
7
11
|
import {
|
|
8
12
|
compareReleaseVersions,
|
|
9
13
|
requireSemver
|
|
@@ -40,16 +44,32 @@ export async function buildTuttiAppVersions(options) {
|
|
|
40
44
|
}
|
|
41
45
|
|
|
42
46
|
if (releaseFiles.length > 0) {
|
|
43
|
-
const minTuttiVersion =
|
|
44
|
-
options.minTuttiVersion,
|
|
45
|
-
|
|
46
|
-
);
|
|
47
|
+
const minTuttiVersion = options.minTuttiVersion
|
|
48
|
+
? requireSemver(options.minTuttiVersion, "minTuttiVersion")
|
|
49
|
+
: null;
|
|
47
50
|
const status = normalizeStatus(options.status ?? "active");
|
|
48
51
|
for (const releaseFile of releaseFiles) {
|
|
49
52
|
const release = JSON.parse(await readFile(releaseFile, "utf8"));
|
|
50
53
|
validateRelease(release);
|
|
54
|
+
const requiredTuttiCapabilities = requiredTuttiCapabilitiesForManifest(
|
|
55
|
+
release.manifest,
|
|
56
|
+
"release.manifest"
|
|
57
|
+
);
|
|
58
|
+
if (minTuttiVersion && requiredTuttiCapabilities.length > 0) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
"a release cannot declare both minTuttiVersion and requiredTuttiCapabilities"
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
if (!minTuttiVersion && requiredTuttiCapabilities.length === 0) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
"minTuttiVersion or release.manifest.hostCompatibility.requiredTuttiCapabilities is required"
|
|
66
|
+
);
|
|
67
|
+
}
|
|
51
68
|
document = addVersionRecord(document, {
|
|
52
|
-
minTuttiVersion,
|
|
69
|
+
...(minTuttiVersion ? { minTuttiVersion } : {}),
|
|
70
|
+
...(requiredTuttiCapabilities.length > 0
|
|
71
|
+
? { requiredTuttiCapabilities }
|
|
72
|
+
: {}),
|
|
53
73
|
status,
|
|
54
74
|
release
|
|
55
75
|
});
|
|
@@ -104,10 +124,31 @@ export function validateVersionsDocument(document) {
|
|
|
104
124
|
if (!record || typeof record !== "object" || Array.isArray(record)) {
|
|
105
125
|
throw new Error(`${label} must be an object`);
|
|
106
126
|
}
|
|
107
|
-
record.minTuttiVersion
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
)
|
|
127
|
+
const hasMinTuttiVersion = record.minTuttiVersion !== undefined;
|
|
128
|
+
const hasRequiredTuttiCapabilities =
|
|
129
|
+
record.requiredTuttiCapabilities !== undefined;
|
|
130
|
+
if (hasMinTuttiVersion && hasRequiredTuttiCapabilities) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
`${label} cannot declare both minTuttiVersion and requiredTuttiCapabilities`
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
if (!hasMinTuttiVersion && !hasRequiredTuttiCapabilities) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
`${label} must declare minTuttiVersion or requiredTuttiCapabilities`
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
if (hasMinTuttiVersion) {
|
|
141
|
+
record.minTuttiVersion = requireSemver(
|
|
142
|
+
record.minTuttiVersion,
|
|
143
|
+
`${label}.minTuttiVersion`
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
if (hasRequiredTuttiCapabilities) {
|
|
147
|
+
record.requiredTuttiCapabilities = normalizeRequiredTuttiCapabilities(
|
|
148
|
+
record.requiredTuttiCapabilities,
|
|
149
|
+
`${label}.requiredTuttiCapabilities`
|
|
150
|
+
);
|
|
151
|
+
}
|
|
111
152
|
record.status = normalizeStatus(record.status);
|
|
112
153
|
validateRelease(record.release);
|
|
113
154
|
if (record.release.appId !== appId) {
|
|
@@ -7,6 +7,7 @@ import path from "node:path";
|
|
|
7
7
|
|
|
8
8
|
import { buildTuttiAppCatalog } from "./build-tutti-app-catalog.mjs";
|
|
9
9
|
import {
|
|
10
|
+
requiredTuttiCapabilitiesForManifest,
|
|
10
11
|
releaseToCatalogApp,
|
|
11
12
|
validateRelease
|
|
12
13
|
} from "./build-tutti-app-release.mjs";
|
|
@@ -37,10 +38,6 @@ export async function publishTuttiAppMetadata(options) {
|
|
|
37
38
|
if (!catalogOnly && !releasePath) {
|
|
38
39
|
throw new Error("releasePath is required unless catalogOnly is true");
|
|
39
40
|
}
|
|
40
|
-
if (!catalogOnly && !minTuttiVersion) {
|
|
41
|
-
throw new Error("minTuttiVersion is required for app releases");
|
|
42
|
-
}
|
|
43
|
-
|
|
44
41
|
await mkdir(outputDir, { recursive: true });
|
|
45
42
|
const release = releasePath
|
|
46
43
|
? JSON.parse(await readFile(releasePath, "utf8"))
|
|
@@ -51,6 +48,23 @@ export async function publishTuttiAppMetadata(options) {
|
|
|
51
48
|
throw new Error("release appId must match input appId");
|
|
52
49
|
}
|
|
53
50
|
}
|
|
51
|
+
const requiredTuttiCapabilities = release
|
|
52
|
+
? requiredTuttiCapabilitiesForManifest(release.manifest, "release.manifest")
|
|
53
|
+
: [];
|
|
54
|
+
if (
|
|
55
|
+
!catalogOnly &&
|
|
56
|
+
!minTuttiVersion &&
|
|
57
|
+
requiredTuttiCapabilities.length === 0
|
|
58
|
+
) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
"minTuttiVersion or release.manifest.hostCompatibility.requiredTuttiCapabilities is required for app releases"
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
if (minTuttiVersion && requiredTuttiCapabilities.length > 0) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
"an app release cannot declare both minTuttiVersion and requiredTuttiCapabilities"
|
|
66
|
+
);
|
|
67
|
+
}
|
|
54
68
|
|
|
55
69
|
const key = (suffix) => (prefix ? `${prefix}/${suffix}` : suffix);
|
|
56
70
|
const versionsKey = key(`apps/${appId}/versions.json`);
|
|
@@ -78,7 +92,7 @@ export async function publishTuttiAppMetadata(options) {
|
|
|
78
92
|
|
|
79
93
|
if (releasePath) {
|
|
80
94
|
releaseFiles.push(releasePath);
|
|
81
|
-
} else if (!existingPath
|
|
95
|
+
} else if (!existingPath) {
|
|
82
96
|
const latest = await getObject({
|
|
83
97
|
bucket,
|
|
84
98
|
key: latestKey,
|
|
@@ -100,7 +114,9 @@ export async function publishTuttiAppMetadata(options) {
|
|
|
100
114
|
existingVersionsPath: existingPath,
|
|
101
115
|
baselineReleaseFiles,
|
|
102
116
|
releaseFiles,
|
|
103
|
-
...(releaseFiles.length > 0
|
|
117
|
+
...(releaseFiles.length > 0 && minTuttiVersion
|
|
118
|
+
? { minTuttiVersion }
|
|
119
|
+
: {}),
|
|
104
120
|
outputPath
|
|
105
121
|
});
|
|
106
122
|
return outputPath;
|
|
@@ -173,7 +189,10 @@ async function downloadLegacyBaseline(input) {
|
|
|
173
189
|
});
|
|
174
190
|
if (!catalog) return null;
|
|
175
191
|
const document = JSON.parse(await readFile(catalog.path, "utf8"));
|
|
176
|
-
if (
|
|
192
|
+
if (
|
|
193
|
+
(document.compatibility?.apps?.[input.appId] ?? []).length > 0 ||
|
|
194
|
+
(document.compatibility?.capabilityApps?.[input.appId] ?? []).length > 0
|
|
195
|
+
) {
|
|
177
196
|
throw new Error(
|
|
178
197
|
`versions index for ${input.appId} is missing but catalog compatibility history exists; restore versions.json before publishing`
|
|
179
198
|
);
|
|
@@ -70,6 +70,18 @@ export async function verifyTuttiAppReleaseArtifacts(options) {
|
|
|
70
70
|
);
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
|
+
for (const [appId, entries] of Object.entries(
|
|
74
|
+
catalog.compatibility?.capabilityApps ?? {}
|
|
75
|
+
)) {
|
|
76
|
+
for (const entry of entries) {
|
|
77
|
+
addCatalogAppCheck(
|
|
78
|
+
checks,
|
|
79
|
+
releasesByKey,
|
|
80
|
+
entry.app,
|
|
81
|
+
`catalog capability compatibility app ${appId}`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
73
85
|
}
|
|
74
86
|
|
|
75
87
|
if (verifyArtifacts) {
|