Files
OmniRoute/scripts/release/radar-export.mjs
Diego Rodrigues de Sa e Souza 0f13fe4221 feat(radar): stable catalog export workflow with provenance (#10826)
Passo 10 of the Radar go-live: publish the OmniRoute catalog export the Radar
server consumes to a stable URL, so the 1 GB-RAM private server pulls it via
RADAR_EXPORT_URL instead of depending on the deploy-time snapshot.

- scripts/release/radar-export.mjs: emits {geradoEm, budgets, totais, registry,
  provenance} from the catalog config modules. Provenance (sourceCommit,
  sourceRef, runUrl, generatedBy) is never fabricated — unknown fields stay null.
- .github/workflows/radar-export.yml: on main catalog changes / manual dispatch /
  weekly, generates the export and clobbers the stable 'radar-export-latest'
  release asset (gh release, GH_TOKEN — checkout persist-credentials:false).
- tests/unit/radar-export.test.mjs: consumer contract (budgets[] non-empty) +
  provenance null-when-unknown + GitHub-env reflection.

Stable URL for RADAR_EXPORT_URL:
https://github.com/diegosouzapw/OmniRoute/releases/download/radar-export-latest/export-omniroute.json

Re-baselines zizmorFindings 190->192 (+2 unpinned-uses @vN, the repo-wide
deliberate convention; artipacked auto-fixed).

Co-authored-by: Xiangzhe <bakryun0718@proton.me>
2026-08-20 08:09:42 -03:00

94 lines
3.5 KiB
JavaScript

#!/usr/bin/env node
// Gera o export estável do catálogo OmniRoute consumido pelo OmniRoute Radar
// (`RADAR_EXPORT_URL` → `${DATA_DIR}/export-omniroute.json` no servidor privado).
//
// Por que existe: o servidor Radar (1 GB RAM na Akamai) NUNCA clona nem instala
// o OmniRoute; ele só baixa este JSON de uma URL estável. Antes o export vinha
// do snapshot gravado no deploy, preso à máquina do operador. Este script roda
// no CI do OmniRoute (que tem os módulos de catálogo + tsx), emite o export com
// PROVENIÊNCIA e o workflow o publica como asset de release de URL fixa.
//
// Contrato do consumidor (`src/feed/exportSource.ts` no radar-server): exige
// `budgets[]` não-vazio e lê `geradoEm`; chaves extras são ignoradas, então
// `totais`, `registry` e `provenance` viajam junto sem quebrar retrocompat.
//
// Uso (precisa de tsx, pois lê .ts):
// node --import tsx/esm scripts/release/radar-export.mjs [saída.json]
import { execFileSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const DIR = path.dirname(fileURLToPath(import.meta.url));
const REPO = path.resolve(DIR, "../.."); // …/OmniRoute
const SAIDA = process.argv[2] || path.join(REPO, "export-omniroute.json");
const { FREE_MODEL_BUDGETS } = await import(
path.join(REPO, "open-sse/config/freeModelCatalog.data.ts")
);
const { computeFreeModelTotals } = await import(
path.join(REPO, "open-sse/config/freeModelCatalog.ts")
);
const { REGISTRY } = await import(path.join(REPO, "open-sse/config/providerRegistry.ts"));
/**
* Proveniência: quem/quando/de-qual-commit gerou o export. Cada campo é `null`
* quando a origem é desconhecida — NUNCA inventamos um valor (D16: desconhecido
* permanece `null`). No CI o GitHub popula as variáveis; localmente caímos no
* `git` e, sem repositório, em `null`.
*/
function firstEnv(...names) {
for (const name of names) {
const value = process.env[name]?.trim();
if (value) return value;
}
return null;
}
function gitHead() {
try {
return execFileSync("git", ["-C", REPO, "rev-parse", "HEAD"], {
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
}).trim() || null;
} catch {
return null;
}
}
function buildProvenance(geradoEm) {
const sourceCommit = firstEnv("GITHUB_SHA") ?? gitHead();
const sourceRef = firstEnv("GITHUB_REF_NAME", "GITHUB_REF");
const server = firstEnv("GITHUB_SERVER_URL");
const repository = firstEnv("GITHUB_REPOSITORY");
const runId = firstEnv("GITHUB_RUN_ID");
const runUrl = server && repository && runId ? `${server}/${repository}/actions/runs/${runId}` : null;
return {
generatedAt: geradoEm,
generator: "scripts/release/radar-export.mjs",
generatedBy: firstEnv("GITHUB_ACTIONS") ? "github-actions" : "manual",
sourceCommit,
sourceRef,
runUrl,
};
}
const geradoEm = new Date().toISOString();
const dados = {
geradoEm,
budgets: FREE_MODEL_BUDGETS,
totais: computeFreeModelTotals(),
// Só as chaves: o consumidor apenas pergunta "sabemos rotear este provider?".
registry: Object.keys(REGISTRY).sort(),
provenance: buildProvenance(geradoEm),
};
fs.mkdirSync(path.dirname(path.resolve(SAIDA)), { recursive: true });
fs.writeFileSync(SAIDA, JSON.stringify(dados));
console.log(
`catálogo exportado → ${path.basename(SAIDA)}: ` +
`${dados.budgets.length} modelos, ${dados.registry.length} providers no registry` +
` (commit ${dados.provenance.sourceCommit ?? "desconhecido"})`
);