Skip to content

Commit b095341

Browse files
author
Shaw
committed
fix: harden beta packaging checks
1 parent 41bc782 commit b095341

12 files changed

Lines changed: 304 additions & 44 deletions

File tree

.github/workflows/mobile-build-smoke.yml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,9 @@ jobs:
6767
echo "Built app at: $APP_PATH"
6868
BUNDLE_ID=$(/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "$APP_PATH/Info.plist")
6969
echo "Bundle ID: $BUNDLE_ID"
70-
# iOS project under packages/app-core/platforms/ios is currently
71-
# ai.elizaos.app. Allow a future eliza brand id alongside it.
70+
# Allow the current Capacitor app ID and legacy/beta app IDs.
7271
case "$BUNDLE_ID" in
73-
ai.elizaos.app|ai.elizaos.Eliza) ;;
72+
app.eliza|ai.elizaos.app|ai.elizaos.Eliza) ;;
7473
*)
7574
echo "ERROR: Unexpected bundle ID: $BUNDLE_ID"
7675
exit 1

.github/workflows/test-packaging.yml

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -206,21 +206,33 @@ jobs:
206206
run: |
207207
set -euo pipefail
208208
ARTIFACTS_DIR="artifacts"
209-
for tarball in "$ARTIFACTS_DIR"/*.tgz; do
210-
[ -f "$tarball" ] || continue
211-
echo "::group::Testing $(basename "$tarball")"
212-
TEST_DIR=$(mktemp -d)
213-
cd "$TEST_DIR"
214-
echo '{"name":"tarball-test","private":true,"type":"module"}' > package.json
215-
npm install "$OLDPWD/$tarball" --ignore-scripts 2>&1
216-
# Verify the package has a valid entry point
217-
PKG_MANIFEST=$(find node_modules/@elizaos -mindepth 2 -maxdepth 2 -name package.json | head -n 1)
218-
PKG_NAME=$(node -p "require('$PKG_MANIFEST').name" 2>/dev/null || echo "unknown")
209+
tarballs=("$ARTIFACTS_DIR"/*.tgz)
210+
if [ ! -f "${tarballs[0]}" ]; then
211+
echo "::error::No tarballs found in $ARTIFACTS_DIR"
212+
exit 1
213+
fi
214+
215+
TEST_DIR=$(mktemp -d)
216+
ROOT_DIR="$PWD"
217+
install_paths=()
218+
for tarball in "${tarballs[@]}"; do
219+
install_paths+=("$ROOT_DIR/$tarball")
220+
done
221+
cd "$TEST_DIR"
222+
echo '{"name":"tarball-test","private":true,"type":"module"}' > package.json
223+
npm install "${install_paths[@]}" --ignore-scripts 2>&1
224+
225+
for tarball in "${tarballs[@]}"; do
226+
echo "::group::Checking $(basename "$tarball")"
227+
PKG_NAME=$(tar -xOf "$ROOT_DIR/$tarball" package/package.json | node -e "process.stdin.setEncoding('utf8'); let data = ''; process.stdin.on('data', chunk => data += chunk); process.stdin.on('end', () => console.log(JSON.parse(data).name));")
228+
PKG_MANIFEST="node_modules/${PKG_NAME}/package.json"
229+
test -f "$PKG_MANIFEST"
219230
echo "Installed: $PKG_NAME"
220-
cd "$OLDPWD"
221-
rm -rf "$TEST_DIR"
222231
echo "::endgroup::"
223232
done
233+
234+
cd "$ROOT_DIR"
235+
rm -rf "$TEST_DIR"
224236
echo "All JS tarballs passed installation test"
225237
226238
- name: Upload tarball artifacts

packages/app-core/packaging/snap/snapcraft.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -336,12 +336,12 @@ parts:
336336
node "$APP_CORE_SCRIPTS_DIR/ensure-shared-i18n-data.mjs"
337337
338338
# patch-deps removes @types packages from node_modules and Bun's cache.
339-
# Preserve the two packages needed by declaration builds outside
340-
# node_modules before patch-deps runs.
339+
# Preserve the packages needed by declaration builds outside node_modules
340+
# before patch-deps runs.
341341
SNAP_BUILD_TYPES_DIR=".snap-build-types"
342342
rm -rf "$SNAP_BUILD_TYPES_DIR"
343343
mkdir -p "$SNAP_BUILD_TYPES_DIR/@types"
344-
for type_pkg in node bun; do
344+
for type_pkg in node bun fs-extra fast-redact markdown-it jsonfile linkify-it mdurl; do
345345
type_target=""
346346
if [ -d "node_modules/@types/$type_pkg" ]; then
347347
type_target="node_modules/@types/$type_pkg"
@@ -366,7 +366,7 @@ parts:
366366
# needs for the build, then remove them again before copying node_modules
367367
# into the snap runtime payload.
368368
mkdir -p node_modules/@types
369-
for type_pkg in node bun; do
369+
for type_pkg in node bun fs-extra fast-redact markdown-it jsonfile linkify-it mdurl; do
370370
type_target="$SNAP_BUILD_TYPES_DIR/@types/$type_pkg"
371371
if [ ! -d "$type_target" ]; then
372372
echo "Failed to restore @types/$type_pkg after patch-deps" >&2

packages/app-core/scripts/pack-upstreams.mjs

Lines changed: 220 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@
66
*/
77

88
import { spawn } from "node:child_process";
9-
import { existsSync, mkdirSync, readFileSync } from "node:fs";
9+
import {
10+
existsSync,
11+
mkdirSync,
12+
readFileSync,
13+
readdirSync,
14+
writeFileSync,
15+
} from "node:fs";
1016
import path from "node:path";
1117
import { resolveRepoRootFromImportMeta } from "./lib/repo-root.mjs";
1218

@@ -18,9 +24,10 @@ const ELIZA_ROOT = existsSync(
1824
? ROOT
1925
: path.join(ROOT, "eliza");
2026

21-
// Target packages to pack. These are the package boundary that Milady consumes
22-
// when it runs without a repo-local eliza checkout.
23-
const TARGETS = [
27+
// Seed packages to pack. Their local workspace dependencies are added
28+
// automatically so PR tarball install tests do not depend on already-published
29+
// beta packages.
30+
const SEED_TARGETS = [
2431
{ label: "@elizaos/core", dir: path.join(ELIZA_ROOT, "packages", "core") },
2532
{
2633
label: "@elizaos/shared",
@@ -78,6 +85,195 @@ function readPackageJson(dir) {
7885
}
7986
}
8087

88+
function collectWorkspacePackages(root) {
89+
const packageRoots = [
90+
path.join(root, "packages"),
91+
path.join(root, "plugins"),
92+
path.join(root, "cloud", "packages"),
93+
];
94+
const packages = new Map();
95+
96+
for (const packageRoot of packageRoots) {
97+
walk(packageRoot, (entryPath) => {
98+
if (path.basename(entryPath) !== "package.json") {
99+
return;
100+
}
101+
const pkgJson = readPackageJson(path.dirname(entryPath));
102+
if (
103+
!pkgJson ||
104+
pkgJson.private === true ||
105+
typeof pkgJson.name !== "string" ||
106+
typeof pkgJson.version !== "string"
107+
) {
108+
return;
109+
}
110+
if (!packages.has(pkgJson.name)) {
111+
packages.set(pkgJson.name, {
112+
label: pkgJson.name,
113+
dir: path.dirname(entryPath),
114+
});
115+
}
116+
});
117+
}
118+
119+
for (const target of SEED_TARGETS) {
120+
packages.set(target.label, target);
121+
}
122+
123+
return packages;
124+
}
125+
126+
function walk(dirPath, visit) {
127+
let entries;
128+
try {
129+
entries = readdirSync(dirPath, { withFileTypes: true });
130+
} catch (error) {
131+
if (
132+
error &&
133+
typeof error === "object" &&
134+
"code" in error &&
135+
error.code === "ENOENT"
136+
) {
137+
return;
138+
}
139+
throw error;
140+
}
141+
142+
for (const entry of entries) {
143+
const entryPath = path.join(dirPath, entry.name);
144+
if (entry.isDirectory()) {
145+
if (
146+
[
147+
"node_modules",
148+
"dist",
149+
".git",
150+
".turbo",
151+
"android",
152+
"ios",
153+
"build",
154+
].includes(entry.name)
155+
) {
156+
continue;
157+
}
158+
walk(entryPath, visit);
159+
continue;
160+
}
161+
visit(entryPath);
162+
}
163+
}
164+
165+
function collectLocalDependencyNames(pkgJson, workspacePackages) {
166+
const names = new Set();
167+
for (const sectionName of ["dependencies", "optionalDependencies"]) {
168+
const section = pkgJson[sectionName];
169+
if (!section || typeof section !== "object") {
170+
continue;
171+
}
172+
for (const [name, spec] of Object.entries(section)) {
173+
if (
174+
typeof spec === "string" &&
175+
spec.startsWith("workspace:") &&
176+
workspacePackages.has(name)
177+
) {
178+
names.add(name);
179+
}
180+
}
181+
}
182+
return names;
183+
}
184+
185+
function resolveTargets() {
186+
const workspacePackages = collectWorkspacePackages(ELIZA_ROOT);
187+
const targets = new Map();
188+
const queue = [...SEED_TARGETS.map((target) => target.label)];
189+
190+
for (let index = 0; index < queue.length; index += 1) {
191+
const label = queue[index];
192+
if (targets.has(label)) {
193+
continue;
194+
}
195+
196+
const target = workspacePackages.get(label);
197+
if (!target) {
198+
throw new Error(`[pack-upstreams] Missing local workspace package ${label}`);
199+
}
200+
201+
const pkgJson = readPackageJson(target.dir);
202+
if (!pkgJson) {
203+
throw new Error(`[pack-upstreams] No package.json found in ${target.dir}`);
204+
}
205+
206+
targets.set(label, target);
207+
for (const dependencyName of collectLocalDependencyNames(
208+
pkgJson,
209+
workspacePackages,
210+
)) {
211+
if (!targets.has(dependencyName)) {
212+
queue.push(dependencyName);
213+
}
214+
}
215+
}
216+
217+
return {
218+
targets: [...targets.values()],
219+
workspacePackages,
220+
};
221+
}
222+
223+
function workspaceSpecToVersion(spec, version) {
224+
if (spec === "workspace:^") {
225+
return `^${version}`;
226+
}
227+
if (spec === "workspace:~") {
228+
return `~${version}`;
229+
}
230+
return version;
231+
}
232+
233+
function rewriteWorkspaceDependencies(packDir, pkgJson, workspacePackages) {
234+
let changed = false;
235+
const nextPkgJson = structuredClone(pkgJson);
236+
for (const sectionName of [
237+
"dependencies",
238+
"optionalDependencies",
239+
"peerDependencies",
240+
"devDependencies",
241+
]) {
242+
const section = nextPkgJson[sectionName];
243+
if (!section || typeof section !== "object") {
244+
continue;
245+
}
246+
for (const [name, spec] of Object.entries(section)) {
247+
if (typeof spec !== "string" || !spec.startsWith("workspace:")) {
248+
continue;
249+
}
250+
const workspacePackage = workspacePackages.get(name);
251+
if (!workspacePackage) {
252+
throw new Error(
253+
`[pack-upstreams] ${pkgJson.name} depends on unknown workspace package ${name}`,
254+
);
255+
}
256+
const dependencyPkgJson = readPackageJson(workspacePackage.dir);
257+
if (!dependencyPkgJson?.version) {
258+
throw new Error(
259+
`[pack-upstreams] Could not resolve version for workspace package ${name}`,
260+
);
261+
}
262+
section[name] = workspaceSpecToVersion(spec, dependencyPkgJson.version);
263+
changed = true;
264+
}
265+
}
266+
267+
if (!changed) {
268+
return null;
269+
}
270+
271+
const packageJsonPath = path.join(packDir, "package.json");
272+
const originalPackageJson = readFileSync(packageJsonPath, "utf8");
273+
writeFileSync(packageJsonPath, `${JSON.stringify(nextPkgJson, null, 2)}\n`);
274+
return () => writeFileSync(packageJsonPath, originalPackageJson);
275+
}
276+
81277
function packageTarballName(pkgJson) {
82278
return `${pkgJson.name.replace(/^@/, "").replace("/", "-")}-${pkgJson.version}.tgz`;
83279
}
@@ -100,7 +296,14 @@ async function packUpstreams() {
100296
mkdirSync(ARTIFACTS_DIR, { recursive: true });
101297
}
102298

103-
for (const target of TARGETS) {
299+
const { targets, workspacePackages } = resolveTargets();
300+
console.log(
301+
`[pack-upstreams] Packing ${targets.length} package(s): ${targets
302+
.map((target) => target.label)
303+
.join(", ")}`,
304+
);
305+
306+
for (const target of targets) {
104307
const pkgDir = target.dir;
105308
if (!existsSync(pkgDir)) {
106309
throw new Error(
@@ -153,11 +356,20 @@ async function packUpstreams() {
153356
console.log(
154357
`[pack-upstreams] Packing ${packPkgJson.name} from ${packDir}...`,
155358
);
156-
await runCommand(
157-
"npm",
158-
["pack", "--pack-destination", ARTIFACTS_DIR],
359+
const restorePackageJson = rewriteWorkspaceDependencies(
159360
packDir,
361+
packPkgJson,
362+
workspacePackages,
160363
);
364+
try {
365+
await runCommand(
366+
"npm",
367+
["pack", "--pack-destination", ARTIFACTS_DIR],
368+
packDir,
369+
);
370+
} finally {
371+
restorePackageJson?.();
372+
}
161373

162374
if (!existsSync(destTarballPath)) {
163375
throw new Error(

packages/app-core/src/components/shell/RuntimeGate.cloud-provisioning.test.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ const {
3030
clientMock: {
3131
getCloudCompatAgents: vi.fn(),
3232
getCloudCompatAgent: vi.fn(),
33+
getCloudCompatAgentStatus: vi.fn(),
3334
createCloudCompatAgent: vi.fn(),
3435
provisionCloudCompatAgent: vi.fn(),
3536
getCloudCompatJobStatus: vi.fn(),
@@ -297,6 +298,18 @@ describe("RuntimeGate cloud provisioning startup handoff", () => {
297298
success: true,
298299
data: RUNNING_AGENT,
299300
});
301+
clientMock.getCloudCompatAgentStatus.mockResolvedValue({
302+
success: true,
303+
data: {
304+
status: "running",
305+
lastHeartbeat: null,
306+
bridgeUrl: "https://agent-1.elizacloud.ai",
307+
webUiUrl: null,
308+
currentNode: null,
309+
suspendedReason: null,
310+
databaseStatus: "ready",
311+
},
312+
});
300313
clientMock.createCloudCompatAgent.mockResolvedValue({
301314
success: true,
302315
data: { agentId: "agent-1" },
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# The compiled collector is intentionally shipped in the npm package.
2+
!activity-collector

0 commit comments

Comments
 (0)