feat: strengthen backup and mobile workflows
ci / gitleaks (push) Successful in 1m19s
ci / docker (push) Successful in 5m48s

This commit is contained in:
2026-09-16 21:12:52 +08:00
parent 6f38190c92
commit 6c234d7d82
64 changed files with 5059 additions and 635 deletions
@@ -0,0 +1,57 @@
import { spawn } from 'node:child_process'
import { createWriteStream, mkdirSync, writeFileSync } from 'node:fs'
import path from 'node:path'
const frontend = process.cwd()
const root = path.resolve(frontend, '..')
const project = process.env.DODO_E2E_PROJECT
if (!project) throw new Error('DODO_E2E_PROJECT is required')
const runtimeBase = path.join(frontend, 'playwright-runtime', project)
const runtime = path.join(runtimeBase, `run-${new Date().toISOString().replace(/[:.]/g, '-')}-${process.pid}`)
const attachments = path.join(runtime, 'attachments')
const staging = path.join(runtime, 'backup-staging')
mkdirSync(attachments, { recursive: true })
mkdirSync(staging, { recursive: true })
writeFileSync(path.join(runtimeBase, 'latest.json'), JSON.stringify({ runtime, database: path.join(runtime, 'dodo.sqlite3'), attachments, staging }, null, 2))
writeFileSync(path.join(runtime, 'runtime.json'), JSON.stringify({ database: path.join(runtime, 'dodo.sqlite3'), attachments, staging }, null, 2))
const logs = {
backend: createWriteStream(path.join(runtime, 'backend.log'), { flags: 'a' }),
frontend: createWriteStream(path.join(runtime, 'frontend.log'), { flags: 'a' }),
}
const children = []
function start(command, args, options, log) {
const child = spawn(command, args, { ...options, stdio: ['ignore', 'pipe', 'pipe'] })
child.stdout.pipe(log)
child.stderr.pipe(log)
children.push(child)
return child
}
start(path.join(root, '.venv/bin/uvicorn'), ['backend.main:app', '--host', '127.0.0.1', '--port', '8781'], {
cwd: root,
env: {
...process.env,
DODO_DATABASE_URL: `sqlite+aiosqlite:///${path.join(runtime, 'dodo.sqlite3')}`,
DODO_AUTO_CREATE_SCHEMA: 'true',
DODO_COOKIE_SECURE: 'false',
DODO_ATTACHMENT_DIR: attachments,
DODO_BACKUP_STAGING_DIR: staging,
},
}, logs.backend)
start('pnpm', ['exec', 'vite', '--host', '127.0.0.1', '--port', '5173', '--strictPort'], { cwd: frontend, env: process.env }, logs.frontend)
let stopping = false
function stop(signal = 'SIGTERM') {
if (stopping) return
stopping = true
for (const child of children) if (!child.killed) child.kill(signal)
setTimeout(() => { for (const child of children) if (!child.killed) child.kill('SIGKILL') }, 3000).unref()
}
process.on('SIGTERM', () => stop())
process.on('SIGINT', () => stop())
process.on('exit', () => stop())
await Promise.all(children.map(child => new Promise((resolve, reject) => {
child.once('exit', (code, signal) => stopping ? resolve() : reject(new Error(`server exited code=${code} signal=${signal}`)))
})))
+15
View File
@@ -0,0 +1,15 @@
import { spawn } from 'node:child_process'
const projects = ['mobile-390', 'mobile-375']
for (const project of projects) {
const code = await new Promise((resolve, reject) => {
const child = spawn('pnpm', ['exec', 'playwright', 'test', '--project', project], {
cwd: process.cwd(),
env: { ...process.env, DODO_E2E_PROJECT: project },
stdio: 'inherit',
})
child.once('error', reject)
child.once('exit', value => resolve(value ?? 1))
})
if (code !== 0) process.exit(code)
}