426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
261 lines
8.4 KiB
JavaScript
261 lines
8.4 KiB
JavaScript
#!/usr/bin/env node
|
|
// Drives repo automation sync artifacts with explicit CLI and CI behavior.
|
|
import { execFileSync } from "node:child_process";
|
|
import { createHash } from "node:crypto";
|
|
import { once } from "node:events";
|
|
/**
|
|
* sync-artifacts.mjs
|
|
*
|
|
* Large generated / downloadable artifacts (benchmark fixtures, CAD exports,
|
|
* build outputs, vendored binaries, media) are NOT committed to this repo —
|
|
* they would make `git clone` slow and heavy. They live as a single bundle on
|
|
* the elizaOS/eliza-archive release and are pulled here on install.
|
|
*
|
|
* This script is idempotent: it no-ops when the on-disk artifact version
|
|
* already matches packages/scripts/artifacts-manifest.json.
|
|
*
|
|
* bun packages/scripts/sync-artifacts.mjs # used by postinstall
|
|
* ELIZA_SKIP_ARTIFACT_SYNC=1 ... # skip (CI lanes that don't need them)
|
|
*
|
|
* On download failure it warns and exits 0 so a network blip never blocks
|
|
* `bun install`; re-run `bun run sync:artifacts` to retry.
|
|
*/
|
|
import {
|
|
createWriteStream,
|
|
existsSync,
|
|
readdirSync,
|
|
readFileSync,
|
|
rmSync,
|
|
statSync,
|
|
writeFileSync,
|
|
} from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { dirname, join } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
const MANIFEST = join(ROOT, "packages", "scripts", "artifacts-manifest.json");
|
|
const MARKER = join(ROOT, ".eliza-artifacts-version");
|
|
const log = (m) => console.log(`[sync-artifacts] ${m}`);
|
|
const warn = (m) => console.warn(`[sync-artifacts] WARNING: ${m}`);
|
|
const PROGRESS_INTERVAL_MS = 5000;
|
|
const STALE_TMP_MAX_AGE_MS = 6 * 60 * 60_000;
|
|
|
|
if (process.env.ELIZA_SKIP_ARTIFACT_SYNC === "1") {
|
|
log("skipped (ELIZA_SKIP_ARTIFACT_SYNC=1)");
|
|
process.exit(0);
|
|
}
|
|
if (!existsSync(MANIFEST)) {
|
|
log("no artifacts-manifest.json; nothing to sync");
|
|
process.exit(0);
|
|
}
|
|
|
|
const m = JSON.parse(readFileSync(MANIFEST, "utf8"));
|
|
const { version, asset } = m;
|
|
cleanupStaleTempArchives();
|
|
|
|
if (existsSync(MARKER) && readFileSync(MARKER, "utf8").trim() === version) {
|
|
log(`artifacts already at ${version}; nothing to do`);
|
|
process.exit(0);
|
|
}
|
|
|
|
const url =
|
|
asset.url ||
|
|
`https://github.com/${asset.repo}/releases/download/${asset.tag}/${asset.name}`;
|
|
const tmp = join(tmpdir(), `eliza-artifacts-${process.pid}.tar.gz`);
|
|
|
|
function formatBytes(bytes) {
|
|
if (!Number.isFinite(bytes) || bytes <= 0) return "0 B";
|
|
const units = ["B", "KiB", "MiB", "GiB"];
|
|
let value = bytes;
|
|
let unit = units[0];
|
|
for (let i = 1; i < units.length && value >= 1024; i++) {
|
|
value /= 1024;
|
|
unit = units[i];
|
|
}
|
|
return `${value >= 10 || unit === "B" ? value.toFixed(0) : value.toFixed(1)} ${unit}`;
|
|
}
|
|
|
|
function formatDuration(ms) {
|
|
if (!Number.isFinite(ms) || ms < 0) return "unknown";
|
|
const seconds = Math.ceil(ms / 1000);
|
|
if (seconds < 60) return `${seconds}s`;
|
|
const minutes = Math.floor(seconds / 60);
|
|
const remainder = seconds % 60;
|
|
return `${minutes}m ${remainder}s`;
|
|
}
|
|
|
|
function progressStatus(received, total, startedAt) {
|
|
const elapsedMs = Math.max(Date.now() - startedAt, 1);
|
|
const bytesPerSecond = received / (elapsedMs / 1000);
|
|
const parts = [`downloaded ${formatBytes(received)}`];
|
|
if (total > 0) {
|
|
const percent = Math.min((received / total) * 100, 100);
|
|
const remainingBytes = Math.max(total - received, 0);
|
|
const etaMs =
|
|
bytesPerSecond > 0 ? (remainingBytes / bytesPerSecond) * 1000 : NaN;
|
|
parts.push(
|
|
`of ${formatBytes(total)}`,
|
|
`(${percent.toFixed(1)}%)`,
|
|
`eta ${formatDuration(etaMs)}`,
|
|
);
|
|
}
|
|
parts.push(`at ${formatBytes(bytesPerSecond)}/s`);
|
|
return parts.join(" ");
|
|
}
|
|
|
|
function cleanupStaleTempArchives() {
|
|
const now = Date.now();
|
|
let removed = 0;
|
|
for (const entry of readdirSync(tmpdir())) {
|
|
if (!/^eliza-artifacts-\d+\.tar\.gz$/.test(entry)) continue;
|
|
const file = join(tmpdir(), entry);
|
|
let stat;
|
|
try {
|
|
stat = statSync(file);
|
|
} catch (err) {
|
|
// error-policy:J6 best-effort temp cleanup; a racing process may remove the file first.
|
|
warn(`could not stat stale temp archive ${file}: ${err.message}`);
|
|
continue;
|
|
}
|
|
if (!stat.isFile()) continue;
|
|
if (now - stat.mtimeMs < STALE_TMP_MAX_AGE_MS) continue;
|
|
try {
|
|
rmSync(file, { force: true });
|
|
removed += 1;
|
|
} catch (err) {
|
|
// error-policy:J6 best-effort temp cleanup; failed cleanup must not block install.
|
|
warn(`could not remove stale temp archive ${file}: ${err.message}`);
|
|
}
|
|
}
|
|
if (removed > 0) {
|
|
log(
|
|
`removed ${removed} stale artifact temp archive${removed === 1 ? "" : "s"}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
async function streamToFileWithProgress(response, dest, expectedBytes) {
|
|
const headerBytes = Number(response.headers.get("content-length")) || 0;
|
|
const totalBytes = headerBytes || expectedBytes || 0;
|
|
const writer = createWriteStream(dest);
|
|
const reader = response.body.getReader();
|
|
const startedAt = Date.now();
|
|
let received = 0;
|
|
let lastLogAt = startedAt;
|
|
let writerError;
|
|
const streamError = new Promise((resolve) => {
|
|
writer.once("error", (err) => {
|
|
writerError = err;
|
|
resolve();
|
|
});
|
|
});
|
|
|
|
if (totalBytes > 0) {
|
|
log(
|
|
`artifact bundle size: ${formatBytes(totalBytes)}${m.fileCount ? ` across ${m.fileCount} files` : ""}`,
|
|
);
|
|
} else {
|
|
log(
|
|
"artifact bundle size unknown; reporting downloaded bytes until complete",
|
|
);
|
|
}
|
|
|
|
try {
|
|
while (true) {
|
|
const { done, value } = await reader.read();
|
|
if (done) break;
|
|
received += value.byteLength;
|
|
if (!writer.write(value)) {
|
|
await Promise.race([once(writer, "drain"), streamError]);
|
|
if (writerError) throw writerError;
|
|
}
|
|
const now = Date.now();
|
|
if (now - lastLogAt >= PROGRESS_INTERVAL_MS) {
|
|
log(progressStatus(received, totalBytes, startedAt));
|
|
lastLogAt = now;
|
|
}
|
|
}
|
|
} finally {
|
|
reader.releaseLock();
|
|
}
|
|
|
|
await Promise.race([
|
|
new Promise((resolve, reject) => {
|
|
writer.end((err) => (err ? reject(err) : resolve()));
|
|
}),
|
|
streamError,
|
|
]);
|
|
if (writerError) throw writerError;
|
|
log(`download complete: ${progressStatus(received, totalBytes, startedAt)}`);
|
|
}
|
|
|
|
async function download(dest) {
|
|
for (let attempt = 1; attempt <= 4; attempt++) {
|
|
try {
|
|
log(`downloading ${url} (attempt ${attempt}/4)`);
|
|
const res = await fetch(url, { redirect: "follow" });
|
|
if (!res.ok || !res.body) throw new Error(`HTTP ${res.status}`);
|
|
await streamToFileWithProgress(res, dest, Number(asset.bytes) || 0);
|
|
return true;
|
|
} catch (err) {
|
|
warn(`download failed: ${err.message}`);
|
|
try {
|
|
rmSync(dest, { force: true });
|
|
} catch {}
|
|
if (attempt < 4) await new Promise((r) => setTimeout(r, attempt * 2000));
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function sha256(file) {
|
|
const h = createHash("sha256");
|
|
h.update(readFileSync(file));
|
|
return h.digest("hex");
|
|
}
|
|
|
|
const ok = await download(tmp);
|
|
if (!ok) {
|
|
warn(`could not download artifact bundle after retries.`);
|
|
warn(`the repo is usable, but large fixtures/binaries are absent.`);
|
|
warn(`retry later with: bun run sync:artifacts`);
|
|
process.exit(0);
|
|
}
|
|
|
|
if (asset.sha256) {
|
|
log(`verifying sha256 for ${formatBytes(asset.bytes || 0)} artifact bundle`);
|
|
const got = sha256(tmp);
|
|
if (got !== asset.sha256) {
|
|
warn(`sha256 mismatch (got ${got}, want ${asset.sha256}); not extracting.`);
|
|
rmSync(tmp, { force: true });
|
|
process.exit(0);
|
|
}
|
|
}
|
|
|
|
log(
|
|
`extracting artifact bundle at repo root${m.fileCount ? ` (${m.fileCount} files)` : ""}…`,
|
|
);
|
|
// Prefer the Windows system bsdtar (System32\tar.exe): a GNU tar that may be
|
|
// first on PATH (Git-for-Windows / MSYS) misreads a `C:\...` archive path as an
|
|
// rsh `host:path` and dies with "Cannot connect to C: resolve failed". bsdtar
|
|
// (shipped with Windows 10 1803+/11) handles drive-letter paths natively.
|
|
// Like the download step above, never let extraction failure block `bun install`.
|
|
const tarBin =
|
|
process.platform === "win32"
|
|
? join(process.env.SystemRoot || "C:\\Windows", "System32", "tar.exe")
|
|
: "tar";
|
|
try {
|
|
execFileSync(tarBin, ["-xzf", tmp, "-C", ROOT], { stdio: "inherit" });
|
|
writeFileSync(MARKER, `${version}\n`);
|
|
log(`done — artifacts synced to ${version}`);
|
|
} catch (err) {
|
|
warn(`extraction failed: ${err.message}`);
|
|
warn(`the repo is usable, but large fixtures/binaries are absent.`);
|
|
warn(`retry later with: bun run sync:artifacts`);
|
|
} finally {
|
|
try {
|
|
rmSync(tmp, { force: true });
|
|
} catch {}
|
|
}
|