Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

267 lines
8.0 KiB
TypeScript

// Drives cloud admin cloud admin migrate with diagnostics automation with explicit environment and CI invariants.
import { createHash } from "node:crypto";
import { existsSync } from "node:fs";
import { readFile } from "node:fs/promises";
import path from "node:path";
import { enforceTlsForRemote } from "@elizaos/cloud-shared/db/client";
import pg from "pg";
const { Client } = pg;
const MIGRATIONS_SCHEMA = "drizzle";
const MIGRATIONS_TABLE = "__drizzle_migrations";
const MIGRATIONS_DIR =
[
path.join(process.cwd(), "packages/cloud/shared/src/db/migrations"),
path.join(process.cwd(), "src/db/migrations"),
].find((candidate) =>
existsSync(path.join(candidate, "meta/_journal.json")),
) ?? path.join(process.cwd(), "packages/cloud/shared/src/db/migrations");
const JOURNAL_PATH = path.join(MIGRATIONS_DIR, "meta/_journal.json");
interface JournalEntry {
idx: number;
version: string;
when: number;
tag: string;
breakpoints: boolean;
}
interface Journal {
version: string;
dialect: string;
entries: JournalEntry[];
}
interface Migration {
entry: JournalEntry;
hash: string;
statements: string[];
}
interface AppliedMigration {
id: number;
hash: string;
created_at: string | number | bigint | null;
}
interface DatabaseError extends Error {
code?: string;
detail?: string;
hint?: string;
position?: string;
schema?: string;
table?: string;
column?: string;
constraint?: string;
}
interface MigrationClient {
query<T = unknown>(text: string, params?: unknown[]): Promise<{ rows: T[] }>;
end(): Promise<void>;
}
async function readJournal(): Promise<Journal> {
return JSON.parse(await readFile(JOURNAL_PATH, "utf8")) as Journal;
}
async function readMigration(entry: JournalEntry): Promise<Migration> {
const migrationPath = path.join(MIGRATIONS_DIR, `${entry.tag}.sql`);
const sql = await readFile(migrationPath, "utf8");
return {
entry,
hash: createHash("sha256").update(sql).digest("hex"),
statements: sql
.split("--> statement-breakpoint")
.map((statement) => statement.trim())
.filter(Boolean),
};
}
function createdAtValue(
migration: AppliedMigration | undefined,
): number | null {
if (!migration?.created_at) return null;
const value = Number(migration.created_at);
return Number.isFinite(value) ? value : null;
}
function summarizeStatement(statement: string): string {
return statement.replace(/\s+/g, " ").slice(0, 500);
}
function formatDatabaseError(error: unknown): string {
if (!(error instanceof Error)) {
return String(error);
}
const databaseError = error as DatabaseError;
const details = [
`message=${databaseError.message}`,
databaseError.code ? `code=${databaseError.code}` : null,
databaseError.detail ? `detail=${databaseError.detail}` : null,
databaseError.hint ? `hint=${databaseError.hint}` : null,
databaseError.position ? `position=${databaseError.position}` : null,
databaseError.schema ? `schema=${databaseError.schema}` : null,
databaseError.table ? `table=${databaseError.table}` : null,
databaseError.column ? `column=${databaseError.column}` : null,
databaseError.constraint ? `constraint=${databaseError.constraint}` : null,
].filter(Boolean);
return details.join(" ");
}
async function ensureMigrationsTable(client: MigrationClient): Promise<void> {
await client.query(`CREATE SCHEMA IF NOT EXISTS "${MIGRATIONS_SCHEMA}"`);
await client.query(`
CREATE TABLE IF NOT EXISTS "${MIGRATIONS_SCHEMA}"."${MIGRATIONS_TABLE}" (
id SERIAL PRIMARY KEY,
hash text NOT NULL,
created_at bigint
)
`);
}
async function getLastAppliedMigration(
client: MigrationClient,
): Promise<AppliedMigration | undefined> {
const result = await client.query<AppliedMigration>(`
SELECT id, hash, created_at
FROM "${MIGRATIONS_SCHEMA}"."${MIGRATIONS_TABLE}"
ORDER BY created_at DESC
LIMIT 1
`);
return result.rows[0];
}
async function applyMigration(
client: MigrationClient,
migration: Migration,
): Promise<void> {
const { entry, statements, hash } = migration;
console.log(
`[db:migrate] applying ${entry.tag} (${statements.length} statements)`,
);
await client.query("BEGIN");
try {
for (const [index, statement] of statements.entries()) {
try {
await client.query(statement);
} catch (error) {
console.error(
`[db:migrate] failed ${entry.tag} statement ${index + 1}/${statements.length}`,
);
console.error(`[db:migrate] sql: ${summarizeStatement(statement)}`);
console.error(`[db:migrate] error: ${formatDatabaseError(error)}`);
throw error;
}
}
await client.query(
`INSERT INTO "${MIGRATIONS_SCHEMA}"."${MIGRATIONS_TABLE}" (hash, created_at) VALUES ($1, $2)`,
[hash, entry.when],
);
await client.query("COMMIT");
} catch (error) {
await client.query("ROLLBACK");
throw error;
}
}
async function createPGliteClient(url: string): Promise<MigrationClient> {
const stripped = url.slice("pglite://".length);
const dataDir = !stripped || stripped === "memory" ? undefined : stripped;
const { PGlite } = await import("@electric-sql/pglite");
const { vector } = await import("@electric-sql/pglite/vector");
const db = await PGlite.create({ dataDir, extensions: { vector } });
return {
// Migrations contain multi-statement chunks (drizzle does not split on `;`
// for non-breakpoint segments). PGlite's prepared `query()` rejects those,
// so route parameter-less SQL through `exec()` and bound queries through
// `query()`. Result rows from `exec()` come back as an array per statement;
// the migrate harness only reads rows from the bound queries it issues.
query: async <T>(text: string, params?: unknown[]) => {
if (params && params.length > 0) {
const result = await db.query<T>(text, params as unknown[]);
return { rows: result.rows };
}
const results = await db.exec(text);
const last = results[results.length - 1];
return { rows: (last?.rows as T[] | undefined) ?? [] };
},
end: () => db.close(),
};
}
async function createPgClient(url: string): Promise<MigrationClient> {
const { url: clientUrl, ssl: clientSsl } = enforceTlsForRemote(url);
const client = new Client({
connectionString: clientUrl,
...(clientSsl ? { ssl: clientSsl } : {}),
});
await client.connect();
return {
query: async <T>(text: string, params?: unknown[]) => {
const result = await client.query<Record<string, unknown>>(text, params);
return { rows: result.rows as T[] };
},
end: () => client.end(),
};
}
async function main(): Promise<void> {
const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl) {
throw new Error("DATABASE_URL is required to run database migrations.");
}
const journal = await readJournal();
const migrations = await Promise.all(
journal.entries.map((entry) => readMigration(entry)),
);
const client: MigrationClient = databaseUrl.startsWith("pglite://")
? await createPGliteClient(databaseUrl)
: await createPgClient(databaseUrl);
try {
await ensureMigrationsTable(client);
const lastApplied = await getLastAppliedMigration(client);
const lastAppliedCreatedAt = createdAtValue(lastApplied);
console.log(
`[db:migrate] last applied migration: ${
lastAppliedCreatedAt === null
? "none"
: `${lastAppliedCreatedAt} (${lastApplied?.hash.slice(0, 12)})`
}`,
);
const pending = migrations.filter(
(migration) =>
lastAppliedCreatedAt === null ||
migration.entry.when > lastAppliedCreatedAt,
);
console.log(`[db:migrate] pending migrations: ${pending.length}`);
for (const migration of pending) {
await applyMigration(client, migration);
}
console.log("[db:migrate] migrations complete");
} finally {
await client.end();
}
}
main().catch((error) => {
console.error(`[db:migrate] fatal: ${formatDatabaseError(error)}`);
process.exit(1);
});