mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
Release v3.8.40
v3.8.40 cycle integration → main. All test gates green (Unit/Integration/Coverage/Node-compat/Quality-Ratchet). The only red check, 'PR Test Policy', is the test-masking heuristic firing on the cumulative ~57-commit release diff (legitimate assert consolidations already reviewed per-PR — Gemini CLI removal #5246, retired GPT models #5280, provider catalog refreshes); overridden with --admin per the documented release-PR convention. CodeQL/SonarQube advisory scans non-blocking; #5278's code already passed CodeQL on main. Homologated on VPS 192.168.0.15 (v3.8.40 healthy).
This commit is contained in:
committed by
GitHub
parent
1c18be4f8f
commit
7c23dab64d
@@ -1,8 +1,8 @@
|
||||
# bin/_ops-common.sh — shared helpers for the OmniRoute ops runbook scripts.
|
||||
#
|
||||
# Sourced (not executed) by rollback.sh / snapshot-data.sh / restore-data.sh /
|
||||
# restore-policies.sh / cold-start-bench.sh. The runbook context lives in
|
||||
# docs/INCIDENT_RESPONSE.md and docs/PERF_BUDGETS.md.
|
||||
# restore-policies.sh / cold-start-bench.sh — the self-hoster incident-recovery
|
||||
# and cold-start ops tooling. Each script documents its own contract via --help.
|
||||
#
|
||||
# Path resolution mirrors the app (src/lib/db/core.ts): the SQLite store is
|
||||
# $DATA_DIR/storage.sqlite and managed backups go to $DATA_DIR/db_backups
|
||||
|
||||
@@ -71,7 +71,6 @@ import { registerSetupCrush } from "./setup-crush.mjs";
|
||||
import { registerSetupGoose } from "./setup-goose.mjs";
|
||||
import { registerSetupQwen } from "./setup-qwen.mjs";
|
||||
import { registerSetupAider } from "./setup-aider.mjs";
|
||||
import { registerSetupGemini } from "./setup-gemini.mjs";
|
||||
import { registerConnect } from "./connect.mjs";
|
||||
import { registerContexts } from "./contexts.mjs";
|
||||
import { registerTokens } from "./tokens.mjs";
|
||||
@@ -154,7 +153,6 @@ export function registerCommands(program) {
|
||||
registerSetupGoose(program);
|
||||
registerSetupQwen(program);
|
||||
registerSetupAider(program);
|
||||
registerSetupGemini(program);
|
||||
registerConnect(program);
|
||||
registerContexts(program);
|
||||
registerTokens(program);
|
||||
|
||||
@@ -313,7 +313,7 @@ async function maybeStartTray(port, apiPort, supervisor) {
|
||||
if (!isTraySupported()) return;
|
||||
const { default: open } = await import("open").catch(() => ({ default: null }));
|
||||
const dashboardUrl = `http://localhost:${port}`;
|
||||
const tray = initTray({
|
||||
const tray = await initTray({
|
||||
port,
|
||||
onQuit: () => {
|
||||
killTrayIfActive();
|
||||
@@ -329,8 +329,12 @@ async function maybeStartTray(port, apiPort, supervisor) {
|
||||
const { killTray } = await import("../tray/index.mjs");
|
||||
_killTray = killTray;
|
||||
}
|
||||
} catch {
|
||||
// tray is optional — do not fail the server
|
||||
} catch (err) {
|
||||
// tray is optional — do not fail the server, but surface why it failed so
|
||||
// "--tray shows nothing" is diagnosable instead of silent (#4605).
|
||||
process.stderr.write(
|
||||
`[omniroute][tray] failed to start: ${err?.message ?? String(err)}\n`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
/**
|
||||
* omniroute setup-gemini — point the Gemini CLI at OmniRoute's Gemini endpoint.
|
||||
*
|
||||
* The Gemini CLI is NOT OpenAI-compatible — it speaks the native Gemini API.
|
||||
* OmniRoute exposes a Gemini-native surface at /v1beta (e.g.
|
||||
* /v1beta/models/<model>:generateContent), so the CLI can target it via the
|
||||
* @google/genai SDK env `GOOGLE_GEMINI_BASE_URL` (ROOT — the SDK appends /v1beta)
|
||||
* + `GEMINI_API_KEY`. There is no settings.json key for the base URL, so this is
|
||||
* primarily an env recipe; we optionally write ~/.gemini/settings.json `model`.
|
||||
*
|
||||
* ⚠ Known Gemini CLI caveat: it may ignore GOOGLE_GEMINI_BASE_URL if a cached
|
||||
* Google login exists — run `gemini` logged-out / API-key-only for it to take.
|
||||
*/
|
||||
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import os from "node:os";
|
||||
import { printHeading, printInfo, printSuccess, printError, createPrompt } from "../io.mjs";
|
||||
import { resolveActiveContext } from "../contexts.mjs";
|
||||
|
||||
function stripToRoot(url) {
|
||||
const s = String(url || "").replace(/\/+$/, "");
|
||||
return s.endsWith("/v1beta") ? s.slice(0, -7) : s.endsWith("/v1") ? s.slice(0, -3) : s;
|
||||
}
|
||||
|
||||
/** Resolve GOOGLE_GEMINI_BASE_URL (ROOT — SDK appends /v1beta) + apiKey. */
|
||||
export function resolveGeminiTarget(opts = {}) {
|
||||
let root;
|
||||
if (opts.remote) root = stripToRoot(opts.remote);
|
||||
else {
|
||||
try {
|
||||
root = stripToRoot(resolveActiveContext(opts.context ?? process.env.OMNIROUTE_CONTEXT)?.baseUrl);
|
||||
} catch {
|
||||
/* none */
|
||||
}
|
||||
if (!root) root = `http://localhost:${Number(opts.port ?? process.env.PORT ?? 20128) || 20128}`;
|
||||
}
|
||||
let apiKey = opts.apiKey ?? opts["api-key"];
|
||||
if (!apiKey) {
|
||||
try {
|
||||
const c = resolveActiveContext(opts.context ?? process.env.OMNIROUTE_CONTEXT);
|
||||
apiKey = c?.accessToken || c?.apiKey;
|
||||
} catch {
|
||||
/* none */
|
||||
}
|
||||
}
|
||||
if (!apiKey) apiKey = process.env.OMNIROUTE_API_KEY || "";
|
||||
return { baseUrl: root, apiKey };
|
||||
}
|
||||
|
||||
/** The guaranteed env recipe (pure → testable). */
|
||||
export function buildGeminiRecipe({ baseUrl, model }) {
|
||||
return [
|
||||
`export GOOGLE_GEMINI_BASE_URL=${baseUrl}`,
|
||||
"export GEMINI_API_KEY=$OMNIROUTE_API_KEY",
|
||||
`export GEMINI_MODEL=${model}`,
|
||||
`gemini -p "reply OK" # or: gemini (interactive)`,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
/** Merge the model into ~/.gemini/settings.json (base URL is env-only). */
|
||||
export function buildGeminiSettings(existing, { model }) {
|
||||
const s = existing && typeof existing === "object" ? { ...existing } : {};
|
||||
if (model) s.model = model;
|
||||
return s;
|
||||
}
|
||||
|
||||
function readJson(path) {
|
||||
try {
|
||||
if (existsSync(path)) return JSON.parse(readFileSync(path, "utf8"));
|
||||
} catch {
|
||||
/* corrupt/missing */
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
async function fetchGeminiModelIds(baseUrl, apiKey) {
|
||||
try {
|
||||
const res = await fetch(`${baseUrl}/v1beta/models`, {
|
||||
headers: { "x-goog-api-key": apiKey || "" },
|
||||
signal: AbortSignal.timeout(8000),
|
||||
});
|
||||
if (!res.ok) return [];
|
||||
const body = await res.json();
|
||||
return (body.models || []).map((m) => String(m.name || "").replace(/^models\//, "")).filter(Boolean);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function runSetupGeminiCommand(opts = {}) {
|
||||
const { baseUrl, apiKey } = resolveGeminiTarget(opts);
|
||||
const dryRun = Boolean(opts.dryRun ?? opts["dry-run"]);
|
||||
const configPath = opts.configPath ?? opts["config-path"] ?? join(os.homedir(), ".gemini", "settings.json");
|
||||
|
||||
printHeading("OmniRoute → Gemini CLI (native Gemini /v1beta endpoint)");
|
||||
printInfo(`GOOGLE_GEMINI_BASE_URL: ${baseUrl} (root — SDK appends /v1beta)`);
|
||||
|
||||
let model = opts.model;
|
||||
if (!model) {
|
||||
const ids = await fetchGeminiModelIds(baseUrl, apiKey);
|
||||
if (ids.length && !opts.yes) {
|
||||
printInfo(`Examples: ${ids.slice(0, 20).join(", ")}${ids.length > 20 ? " …" : ""}`);
|
||||
const prompt = createPrompt();
|
||||
try {
|
||||
model = await prompt.ask("Model id for Gemini CLI");
|
||||
} finally {
|
||||
prompt.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!model) {
|
||||
printError("A model is required. Pass --model <id>.");
|
||||
return 2;
|
||||
}
|
||||
|
||||
if (dryRun) {
|
||||
console.log(`\n── [dry-run] ${configPath} ── { "model": "${model}" }`);
|
||||
} else {
|
||||
const merged = buildGeminiSettings(readJson(configPath), { model });
|
||||
mkdirSync(join(configPath, ".."), { recursive: true });
|
||||
writeFileSync(configPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
||||
printSuccess(`Wrote ${configPath} (model)`);
|
||||
}
|
||||
|
||||
printInfo("\nThe base URL is env-only for Gemini CLI — export these:");
|
||||
console.log(buildGeminiRecipe({ baseUrl, model }));
|
||||
printInfo("\n⚠ If Gemini CLI ignores the base URL, you have a cached Google login —");
|
||||
printInfo(" run logged-out (API-key only) so GOOGLE_GEMINI_BASE_URL takes effect.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function registerSetupGemini(program) {
|
||||
program
|
||||
.command("setup-gemini")
|
||||
.description("Point the Gemini CLI at OmniRoute's native Gemini /v1beta endpoint (env recipe + settings model)")
|
||||
.option("--port <port>", "Local OmniRoute port (ignored when --remote is set)", "20128")
|
||||
.option("--remote <url>", "Remote OmniRoute URL, e.g. http://192.168.0.15:20128")
|
||||
.option("--api-key <key>", "OmniRoute API key (defaults to OMNIROUTE_API_KEY env var)")
|
||||
.option("--model <id>", "Model id for Gemini CLI (required unless picked interactively)")
|
||||
.option("--config-path <path>", "settings.json path (default: ~/.gemini/settings.json)")
|
||||
.option("--yes", "Non-interactive: do not prompt (requires --model)")
|
||||
.option("--dry-run", "Print what would be written without touching the filesystem")
|
||||
.action(async (opts) => {
|
||||
const code = await runSetupGeminiCommand(opts);
|
||||
if (code !== 0) process.exit(code);
|
||||
});
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* omniroute setup-qwen — configure Qwen Code (QwenLM/qwen-code) for OmniRoute.
|
||||
*
|
||||
* Qwen Code is a terminal AI agent (gemini-cli fork) with a file-based config at
|
||||
* Qwen Code is a terminal AI agent with a file-based config at
|
||||
* ~/.qwen/settings.json. For a custom OpenAI-compatible endpoint it uses a
|
||||
* `modelProviders` entry with authType "openai", baseUrl WITH /v1, and an
|
||||
* `envKey` naming the env var holding the key (secret stays in the env, never the
|
||||
@@ -47,7 +47,9 @@ export function resolveQwenTarget(opts = {}) {
|
||||
/** Merge the OmniRoute modelProvider into Qwen's settings.json (preserve rest). */
|
||||
export function buildQwenSettings(existing, { baseUrl, model }) {
|
||||
const s = existing && typeof existing === "object" ? { ...existing } : {};
|
||||
const providers = Array.isArray(s.modelProviders) ? s.modelProviders.filter((p) => p?.id !== "omniroute") : [];
|
||||
const providers = Array.isArray(s.modelProviders)
|
||||
? s.modelProviders.filter((p) => p?.id !== "omniroute")
|
||||
: [];
|
||||
providers.push({
|
||||
id: "omniroute",
|
||||
name: "OmniRoute",
|
||||
@@ -82,7 +84,7 @@ async function fetchModelIds(baseUrl, apiKey) {
|
||||
});
|
||||
if (!res.ok) return [];
|
||||
const body = await res.json();
|
||||
const list = Array.isArray(body) ? body : body.data ?? body.models ?? [];
|
||||
const list = Array.isArray(body) ? body : (body.data ?? body.models ?? []);
|
||||
return list.map((m) => (typeof m === "string" ? m : m?.id)).filter(Boolean);
|
||||
} catch {
|
||||
return [];
|
||||
@@ -92,7 +94,8 @@ async function fetchModelIds(baseUrl, apiKey) {
|
||||
export async function runSetupQwenCommand(opts = {}) {
|
||||
const { baseUrl, apiKey } = resolveQwenTarget(opts);
|
||||
const dryRun = Boolean(opts.dryRun ?? opts["dry-run"]);
|
||||
const configPath = opts.configPath ?? opts["config-path"] ?? join(os.homedir(), ".qwen", "settings.json");
|
||||
const configPath =
|
||||
opts.configPath ?? opts["config-path"] ?? join(os.homedir(), ".qwen", "settings.json");
|
||||
|
||||
printHeading("OmniRoute → Qwen Code (openai-compatible)");
|
||||
printInfo(`baseUrl: ${baseUrl}`);
|
||||
@@ -126,7 +129,9 @@ export async function runSetupQwenCommand(opts = {}) {
|
||||
writeFileSync(configPath, out, "utf8");
|
||||
printSuccess(`Wrote ${configPath}`);
|
||||
}
|
||||
printInfo("\nProvide the key (settings reference OMNIROUTE_API_KEY): export OMNIROUTE_API_KEY=...");
|
||||
printInfo(
|
||||
"\nProvide the key (settings reference OMNIROUTE_API_KEY): export OMNIROUTE_API_KEY=..."
|
||||
);
|
||||
printInfo('Then run: qwen (or headless: qwen -p "reply OK")');
|
||||
return 0;
|
||||
}
|
||||
@@ -134,7 +139,9 @@ export async function runSetupQwenCommand(opts = {}) {
|
||||
export function registerSetupQwen(program) {
|
||||
program
|
||||
.command("setup-qwen")
|
||||
.description("Configure Qwen Code for OmniRoute: write ~/.qwen/settings.json (openai modelProvider)")
|
||||
.description(
|
||||
"Configure Qwen Code for OmniRoute: write ~/.qwen/settings.json (openai modelProvider)"
|
||||
)
|
||||
.option("--port <port>", "Local OmniRoute port (ignored when --remote is set)", "20128")
|
||||
.option("--remote <url>", "Remote OmniRoute URL, e.g. http://192.168.0.15:20128")
|
||||
.option("--api-key <key>", "OmniRoute API key (defaults to OMNIROUTE_API_KEY env var)")
|
||||
|
||||
@@ -5,10 +5,12 @@ let active = null;
|
||||
|
||||
export { isTraySupported };
|
||||
|
||||
export function initTray({ port, onQuit, onOpenDashboard, onShowLogs }) {
|
||||
export async function initTray({ port, onQuit, onOpenDashboard, onShowLogs }) {
|
||||
if (!isTraySupported()) return null;
|
||||
const ctx = { port, onQuit, onOpenDashboard, onShowLogs };
|
||||
active = process.platform === "win32" ? initWinTray(ctx) : initSystrayUnix(ctx);
|
||||
// initSystrayUnix is async: it lazily installs/loads systray2 from the runtime
|
||||
// dir (trayRuntime.ts) rather than from node_modules. (#4605)
|
||||
active = process.platform === "win32" ? initWinTray(ctx) : await initSystrayUnix(ctx);
|
||||
return active;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,30 +14,31 @@ export function isTraySupported() {
|
||||
return true;
|
||||
}
|
||||
|
||||
function loadSystray2() {
|
||||
const candidates = [
|
||||
() => {
|
||||
const { createRequire } = require("module");
|
||||
const req = createRequire(import.meta.url);
|
||||
return req("systray2").default;
|
||||
},
|
||||
];
|
||||
for (const attempt of candidates) {
|
||||
try {
|
||||
return attempt();
|
||||
} catch {}
|
||||
}
|
||||
return null;
|
||||
// systray2 is NOT a static dependency — it is lazily installed into
|
||||
// ~/.omniroute/runtime by trayRuntime.ts (loadSystray). The previous inline
|
||||
// loader called `require("module")`, which throws `ReferenceError: require is
|
||||
// not defined` in this ESM file (package "type":"module"); the throw was
|
||||
// silently swallowed, so the tray never appeared on macOS/Linux with no error
|
||||
// printed (#4605, regressed in v3.8.34). Delegate to the runtime loader, which
|
||||
// resolves systray2 from the runtime dir and surfaces install/import failures.
|
||||
async function loadSystray2() {
|
||||
const { loadSystray } = await import("../runtime/trayRuntime.ts");
|
||||
return loadSystray();
|
||||
}
|
||||
|
||||
function getIconBase64() {
|
||||
const iconPath = join(__dirname, "icons", "icon.png");
|
||||
// Icon ships at bin/cli/tray/icon.png — the previous "icons/icon.png" path
|
||||
// never existed, so the tray was created with an empty icon (#4605).
|
||||
const iconPath = join(__dirname, "icon.png");
|
||||
if (existsSync(iconPath)) return readFileSync(iconPath).toString("base64");
|
||||
return "";
|
||||
}
|
||||
|
||||
export function initSystrayUnix({ port, onQuit, onOpenDashboard, onShowLogs }) {
|
||||
const SysTray = loadSystray2();
|
||||
export async function initSystrayUnix(
|
||||
{ port, onQuit, onOpenDashboard, onShowLogs },
|
||||
loadCtor = loadSystray2
|
||||
) {
|
||||
const SysTray = await loadCtor();
|
||||
if (!SysTray) return null;
|
||||
|
||||
const autostartEnabled = isAutostartEnabled();
|
||||
@@ -57,7 +58,10 @@ export function initSystrayUnix({ port, onQuit, onOpenDashboard, onShowLogs }) {
|
||||
tray = new SysTray({
|
||||
menu: {
|
||||
icon: getIconBase64(),
|
||||
isTemplateIcon: process.platform === "darwin",
|
||||
// isTemplateIcon must be false: icon.png is a full-color RGBA logo, and
|
||||
// macOS template mode uses only the alpha channel → a solid white square
|
||||
// (the icon looked "missing" even when the tray loaded). (PR #1080)
|
||||
isTemplateIcon: false,
|
||||
title: "",
|
||||
tooltip: `OmniRoute — port ${port}`,
|
||||
items,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
# bin/cold-start-bench.sh — measure OmniRoute cold-start against the budgets in
|
||||
# docs/PERF_BUDGETS.md §5 (container start → HTTP listening ≤ 800 ms; first warm
|
||||
# bin/cold-start-bench.sh — measure OmniRoute cold-start against the target
|
||||
# budgets (container start → HTTP listening ≤ 800 ms; first warm
|
||||
# TTFB ≤ 200 ms). Boots the server on a throwaway port, times until
|
||||
# /api/health/ping answers 200, measures a warm request, and reports PASS/FAIL.
|
||||
set -euo pipefail
|
||||
@@ -13,7 +13,7 @@ Usage: bin/cold-start-bench.sh [--port <n>] [--start-cmd "<cmd>"] [--url <base>]
|
||||
[--listen-budget-ms <n>] [--ttfb-budget-ms <n>] [-h|--help]
|
||||
|
||||
Boots OmniRoute, times cold-start to the first /api/health/ping 200, measures
|
||||
warm TTFB, and compares against the PERF_BUDGETS.md §5 budgets
|
||||
warm TTFB, and compares against the cold-start budgets
|
||||
(listen ≤ 800 ms, TTFB ≤ 200 ms). Exits non-zero if a budget is exceeded.
|
||||
|
||||
--url benches an already-running server instead of booting one (skips the boot
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
export const SECURE_NODE_LINES = Object.freeze([
|
||||
Object.freeze({ major: 20, minor: 20, patch: 2 }),
|
||||
Object.freeze({ major: 22, minor: 22, patch: 2 }),
|
||||
Object.freeze({ major: 24, minor: 0, patch: 0 }),
|
||||
Object.freeze({ major: 25, minor: 0, patch: 0 }),
|
||||
@@ -9,9 +8,9 @@ export const SECURE_NODE_LINES = Object.freeze([
|
||||
]);
|
||||
|
||||
export const RECOMMENDED_NODE_VERSION = "24.14.1";
|
||||
export const SUPPORTED_NODE_RANGE = ">=20.20.2 <21 || >=22.22.2 <23 || >=24.0.0 <27";
|
||||
export const SUPPORTED_NODE_RANGE = ">=22.22.2 <23 || >=24.0.0 <27";
|
||||
export const SUPPORTED_NODE_DISPLAY =
|
||||
"Node.js 20.20.2+ (20.x LTS), 22.22.2+ (22.x LTS), 24.0.0+ (24.x LTS), 25.0.0+ (25.x), or 26.0.0+ (26.x)";
|
||||
"Node.js 22.22.2+ (22.x LTS), 24.0.0+ (24.x LTS), 25.0.0+ (25.x), or 26.0.0+ (26.x)";
|
||||
|
||||
function formatVersion(version) {
|
||||
return `${version.major}.${version.minor}.${version.patch}`;
|
||||
@@ -78,7 +77,7 @@ export function getNodeRuntimeWarning(version = process.versions.node) {
|
||||
}
|
||||
|
||||
if (support.reason === "unreleased-major") {
|
||||
return `Node.js ${support.nodeVersion} is outside the supported LTS lines. OmniRoute currently supports Node.js 20.x, 22.x, 24.x, 25.x, and 26.x.`;
|
||||
return `Node.js ${support.nodeVersion} is outside the supported LTS lines. OmniRoute currently supports Node.js 22.x, 24.x, 25.x, and 26.x.`;
|
||||
}
|
||||
|
||||
return `Node.js ${support.nodeVersion} is outside OmniRoute's approved secure runtime policy.`;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# bin/restore-data.sh — restore the OmniRoute SQLite data volume from a snapshot
|
||||
# created by bin/snapshot-data.sh. Used by the data-layer incident runbook
|
||||
# (docs/INCIDENT_RESPONSE.md §4.4) after stopping writers.
|
||||
# created by bin/snapshot-data.sh. Used by the data-layer incident-recovery
|
||||
# flow after stopping writers.
|
||||
#
|
||||
# Safety: takes a pre-restore snapshot of the CURRENT data, refuses to run
|
||||
# unattended without --yes, and verifies the snapshot before overwriting.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# bin/restore-policies.sh — restore ONLY the API-key policy tables from a
|
||||
# snapshot, leaving request/audit/runtime state intact. Used by the auth-layer
|
||||
# incident runbook (docs/INCIDENT_RESPONSE.md §4.3) when policies_active is empty
|
||||
# incident-recovery flow when policies_active is empty
|
||||
# but the rest of the database is healthy (so a full restore-data is overkill).
|
||||
#
|
||||
# "Policy" tables = api_key* definition tables (the key + its limits/allowed
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
# bin/rollback.sh — roll OmniRoute back to a previous release to mitigate a bad
|
||||
# deploy. Used by the incident runbook (docs/INCIDENT_RESPONSE.md §3 / §4).
|
||||
# deploy. Part of the deploy-rollback incident-recovery flow.
|
||||
#
|
||||
# Methods (auto-detected; override with --method):
|
||||
# • npm — `npm install -g omniroute@<version>` and, if PM2 manages it,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# bin/snapshot-data.sh — consistent point-in-time snapshot of the OmniRoute data
|
||||
# volume (the SQLite store under $DATA_DIR). Used by the data-layer incident
|
||||
# runbook (docs/INCIDENT_RESPONSE.md §4.4) before any restore.
|
||||
# volume (the SQLite store under $DATA_DIR). Used by the data-layer
|
||||
# incident-recovery flow before any restore.
|
||||
#
|
||||
# Output: a directory $DB_BACKUPS_DIR/snapshot_<UTC>[_<label>] holding a
|
||||
# `VACUUM INTO` copy of storage.sqlite (consistent even with WAL writers active)
|
||||
|
||||
Reference in New Issue
Block a user