@tutti-os/app-release-tools 0.0.79 → 0.0.81
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 +8 -5
- package/bin/build-tutti-app-release.mjs +76 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,11 +13,14 @@ verify-tutti-app-release-artifacts --release-file ./apps/vibe-design/latest.json
|
|
|
13
13
|
verify-tutti-app-release-artifacts --catalog-file ./catalog.json --versions-file ./apps/vibe-design/versions.json
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
The release command validates a complete Tutti app package, creates a
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
The release command validates a complete Tutti app package, creates a
|
|
17
|
+
reproducible zip with stable entry ordering and timestamps, writes immutable
|
|
18
|
+
`release.json`, and writes mutable `latest.json`. Rebuilding the same package
|
|
19
|
+
version produces the same artifact SHA-256, so an interrupted release can
|
|
20
|
+
safely verify and reuse its immutable upload. When the manifest declares
|
|
21
|
+
`localizationInfo`, the release metadata includes the referenced manifest
|
|
22
|
+
locale files so App Center can localize uninstalled remote apps without
|
|
23
|
+
downloading the package.
|
|
21
24
|
|
|
22
25
|
The versions command maintains one mutable `tutti.app.versions.v1` index per
|
|
23
26
|
app. Every catalog-eligible release has an explicit minimum Tutti version and
|
|
@@ -2,7 +2,18 @@
|
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
cp,
|
|
7
|
+
mkdir,
|
|
8
|
+
mkdtemp,
|
|
9
|
+
readFile,
|
|
10
|
+
readdir,
|
|
11
|
+
rm,
|
|
12
|
+
stat,
|
|
13
|
+
utimes,
|
|
14
|
+
writeFile
|
|
15
|
+
} from "node:fs/promises";
|
|
16
|
+
import { tmpdir } from "node:os";
|
|
6
17
|
import path from "node:path";
|
|
7
18
|
|
|
8
19
|
import { requireSemver } from "./tutti-app-versioning.mjs";
|
|
@@ -75,7 +86,7 @@ export async function buildTuttiAppRelease(options) {
|
|
|
75
86
|
|
|
76
87
|
const artifactName = `${appId}-${version}.zip`;
|
|
77
88
|
const artifactPath = path.join(releaseDir, artifactName);
|
|
78
|
-
createZip(packageDir, artifactPath);
|
|
89
|
+
await createZip(packageDir, artifactPath);
|
|
79
90
|
const artifact = await fileDigestAndSize(artifactPath);
|
|
80
91
|
|
|
81
92
|
const iconName = path.basename(sourceIconPath);
|
|
@@ -623,17 +634,71 @@ function validateReleaseLocalizations(localizations, sourceLabel) {
|
|
|
623
634
|
}
|
|
624
635
|
}
|
|
625
636
|
|
|
626
|
-
function createZip(packageDir, artifactPath) {
|
|
627
|
-
const
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
637
|
+
async function createZip(packageDir, artifactPath) {
|
|
638
|
+
const stagingDir = await mkdtemp(
|
|
639
|
+
path.join(tmpdir(), "tutti-app-release-archive-")
|
|
640
|
+
);
|
|
641
|
+
const archiveRoot = path.join(stagingDir, "package");
|
|
642
|
+
try {
|
|
643
|
+
await cp(packageDir, archiveRoot, {
|
|
644
|
+
recursive: true,
|
|
645
|
+
dereference: true
|
|
646
|
+
});
|
|
647
|
+
const entries = await normalizeArchiveEntries(archiveRoot);
|
|
648
|
+
await rm(artifactPath, { force: true });
|
|
649
|
+
const result = spawnSync("zip", ["-X", "-q", artifactPath, "-@"], {
|
|
650
|
+
cwd: archiveRoot,
|
|
651
|
+
encoding: "utf8",
|
|
652
|
+
env: {
|
|
653
|
+
...process.env,
|
|
654
|
+
LC_ALL: "C",
|
|
655
|
+
TZ: "UTC"
|
|
656
|
+
},
|
|
657
|
+
input: `${entries.join("\n")}\n`
|
|
658
|
+
});
|
|
659
|
+
if (result.error) {
|
|
660
|
+
throw result.error;
|
|
661
|
+
}
|
|
662
|
+
if (result.status !== 0) {
|
|
663
|
+
throw new Error(
|
|
664
|
+
result.stderr || `zip exited with status ${result.status}`
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
} finally {
|
|
668
|
+
await rm(stagingDir, { recursive: true, force: true });
|
|
633
669
|
}
|
|
634
|
-
|
|
635
|
-
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
async function normalizeArchiveEntries(rootDir, relativeDir = "") {
|
|
673
|
+
const fixedTimestamp = new Date("1980-01-01T00:00:00.000Z");
|
|
674
|
+
const directoryEntries = await readdir(path.join(rootDir, relativeDir), {
|
|
675
|
+
withFileTypes: true
|
|
676
|
+
});
|
|
677
|
+
directoryEntries.sort((left, right) =>
|
|
678
|
+
left.name < right.name ? -1 : left.name > right.name ? 1 : 0
|
|
679
|
+
);
|
|
680
|
+
|
|
681
|
+
const result = [];
|
|
682
|
+
for (const entry of directoryEntries) {
|
|
683
|
+
if (entry.name.includes("\n") || entry.name.includes("\r")) {
|
|
684
|
+
throw new Error(
|
|
685
|
+
`package path contains an unsupported newline: ${path.join(relativeDir, entry.name)}`
|
|
686
|
+
);
|
|
687
|
+
}
|
|
688
|
+
const relativePath = path.join(relativeDir, entry.name);
|
|
689
|
+
const absolutePath = path.join(rootDir, relativePath);
|
|
690
|
+
await utimes(absolutePath, fixedTimestamp, fixedTimestamp);
|
|
691
|
+
if (entry.isDirectory()) {
|
|
692
|
+
result.push(`${relativePath}/`);
|
|
693
|
+
result.push(...(await normalizeArchiveEntries(rootDir, relativePath)));
|
|
694
|
+
continue;
|
|
695
|
+
}
|
|
696
|
+
if (!entry.isFile()) {
|
|
697
|
+
throw new Error(`unsupported package entry type: ${relativePath}`);
|
|
698
|
+
}
|
|
699
|
+
result.push(relativePath);
|
|
636
700
|
}
|
|
701
|
+
return result;
|
|
637
702
|
}
|
|
638
703
|
|
|
639
704
|
async function fileDigestAndSize(filePath) {
|