Become the data-only repo: site and worker move to private nagi-bench-site
The showcase app and the arena backend now live in the private nagi-bench-site repo and deploy to Cloudflare Pages, fetching this repo's models/, outputs/ and cases.json at build time. CI here is a standalone data validation (same rules as before, no build), plus an optional repository_dispatch that triggers an instant site rebuild when SITE_DISPATCH_TOKEN is configured — otherwise the site's 6-hour cron picks new data up. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ee081a4e34
commit
664a7115c7
5 files changed
+151
-71
No files matched your search
@@ -2,19 +2,39 @@ name: CI
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
validate:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: oven-sh/setup-bun@v2
|
- uses: oven-sh/setup-bun@v2
|
||||||
- name: Install dependencies
|
- name: Validate contribution data
|
||||||
run: bun install
|
run: bun scripts/validate-data.ts
|
||||||
working-directory: site
|
|
||||||
- name: Type-check and build
|
trigger-site-rebuild:
|
||||||
run: bun run build
|
# The site lives in the private nagi-bench-site repo (Cloudflare Pages).
|
||||||
working-directory: site
|
# With SITE_DISPATCH_TOKEN set the rebuild is instant; without it the
|
||||||
|
# site's 6-hour cron picks the new data up anyway.
|
||||||
|
if: github.event_name == 'push'
|
||||||
|
needs: validate
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Dispatch data-updated to nagi-bench-site
|
||||||
|
env:
|
||||||
|
TOKEN: ${{ secrets.SITE_DISPATCH_TOKEN }}
|
||||||
|
run: |
|
||||||
|
if [ -z "$TOKEN" ]; then
|
||||||
|
echo "SITE_DISPATCH_TOKEN not set; relying on the site's scheduled rebuild."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
curl -sf -X POST \
|
||||||
|
-H "Authorization: Bearer $TOKEN" \
|
||||||
|
-H "Accept: application/vnd.github+json" \
|
||||||
|
https://api.github.com/repos/nagi-studio/nagi-bench-site/dispatches \
|
||||||
|
-d '{"event_type":"data-updated"}'
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
name: Deploy site to GitHub Pages
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
pages: write
|
|
||||||
id-token: write
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: pages
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: oven-sh/setup-bun@v2
|
|
||||||
- name: Install dependencies
|
|
||||||
run: bun install --frozen-lockfile
|
|
||||||
working-directory: site
|
|
||||||
- name: Build
|
|
||||||
run: bun run build
|
|
||||||
working-directory: site
|
|
||||||
env:
|
|
||||||
# Optional arena backend (see worker/README.md); empty = local-only votes.
|
|
||||||
VITE_ARENA_API: ${{ vars.VITE_ARENA_API }}
|
|
||||||
VITE_TURNSTILE_SITE_KEY: ${{ vars.VITE_TURNSTILE_SITE_KEY }}
|
|
||||||
- uses: actions/configure-pages@v5
|
|
||||||
- uses: actions/upload-pages-artifact@v3
|
|
||||||
with:
|
|
||||||
path: site/dist
|
|
||||||
|
|
||||||
deploy:
|
|
||||||
needs: build
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
environment:
|
|
||||||
name: github-pages
|
|
||||||
url: ${{ steps.deployment.outputs.page_url }}
|
|
||||||
steps:
|
|
||||||
- id: deployment
|
|
||||||
uses: actions/deploy-pages@v4
|
|
||||||
+7
-9
@@ -49,18 +49,16 @@ Pending: Mistral Medium 3.5 · GLM-5.1 · Qwen3.7-Max (PRs welcome)
|
|||||||
```
|
```
|
||||||
outputs/<model-id>/<case-id>.<ext> raw model artifacts (HTML / SVG), filename = case id
|
outputs/<model-id>/<case-id>.<ext> raw model artifacts (HTML / SVG), filename = case id
|
||||||
models/<model-id>.json combination registry: label / vendor / harness / effort / order / run notes
|
models/<model-id>.json combination registry: label / vendor / harness / effort / order / run notes
|
||||||
site/ showcase app (Vite + React + Tailwind v4 + GSAP, bun)
|
cases.json case definitions (bilingual prompts, maintainer-owned)
|
||||||
site/src/data/cases.json case definitions (bilingual prompts, maintainer-owned)
|
scripts/validate-data.ts data validation (CI runs it on every PR)
|
||||||
.github/workflows/ deploy.yml deploys; ci.yml validates data + builds on PRs
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Development
|
> The site and vote-backend source lives in the private `nagi-bench-site` repo, deployed on Cloudflare Pages; this repo is the site's data source.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd site
|
bun scripts/validate-data.ts # the same data validation CI runs
|
||||||
bun install
|
|
||||||
bun run dev # syncs outputs/ and serves http://localhost:5173/
|
|
||||||
bun run build # data validation + type-check + build (site/dist)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Contributing a run
|
## Contributing a run
|
||||||
@@ -72,4 +70,4 @@ Contributions are data-only — no code changes needed:
|
|||||||
|
|
||||||
CI-enforced rules: dash-only lowercase ids matching the `outputs/` folder name; a **required bilingual provenance `note`** per run (which harness, what effort, one-shot or fixed); every declared run must have its artifact; multiple versions of the same combination x case go in an array with distinct `file` names (e.g. `pelican-cycling-2.svg`). Set `contributor` to your GitHub username — the site shows your avatar linked to your profile next to the run. Fill in `harness` and `effort` truthfully for new combinations: the case page renders Harness / Effort metadata chips from these fields and auto-matches brand icons for the model, vendor and harness (via [lobe-icons](https://github.com/lobehub/lobe-icons)) — contributors never touch icons.
|
CI-enforced rules: dash-only lowercase ids matching the `outputs/` folder name; a **required bilingual provenance `note`** per run (which harness, what effort, one-shot or fixed); every declared run must have its artifact; multiple versions of the same combination x case go in an array with distinct `file` names (e.g. `pelican-cycling-2.svg`). Set `contributor` to your GitHub username — the site shows your avatar linked to your profile next to the run. Fill in `harness` and `effort` truthfully for new combinations: the case page renders Harness / Effort metadata chips from these fields and auto-matches brand icons for the model, vendor and harness (via [lobe-icons](https://github.com/lobehub/lobe-icons)) — contributors never touch icons.
|
||||||
|
|
||||||
PRs get data validation + a build from CI; merges to `main` deploy automatically.
|
PRs get data validation from CI; merges to `main` rebuild the site automatically (usually instant, at most 6 hours).
|
||||||
@@ -49,18 +49,16 @@ NAGI STUDIO 的 LLM 测评案例集:同一段提示词,不同「模型 × Ha
|
|||||||
```
|
```
|
||||||
outputs/<model-id>/<case-id>.<ext> 模型产出原文件(HTML / SVG),文件名 = 案例 id
|
outputs/<model-id>/<case-id>.<ext> 模型产出原文件(HTML / SVG),文件名 = 案例 id
|
||||||
models/<model-id>.json 组合登记:label / vendor / harness / effort / order / 各案例运行备注
|
models/<model-id>.json 组合登记:label / vendor / harness / effort / order / 各案例运行备注
|
||||||
site/ 展示站点(Vite + React + Tailwind v4 + GSAP,bun 驱动)
|
cases.json 案例定义:双语标题与提示词(维护者维护)
|
||||||
site/src/data/cases.json 案例定义:双语标题与提示词(维护者维护)
|
scripts/validate-data.ts 数据校验(CI 对每个 PR 自动执行)
|
||||||
.github/workflows/ deploy.yml 自动部署;ci.yml 对 PR 跑数据校验 + 构建
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 本地开发 / Development
|
> 站点与投票后端的源码在私有仓库 `nagi-bench-site` 中,部署于 Cloudflare Pages;本仓库是站点的数据源。
|
||||||
|
|
||||||
|
## 本地校验 / Validation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd site
|
bun scripts/validate-data.ts # 与 CI 相同的数据校验
|
||||||
bun install
|
|
||||||
bun run dev # 同步 outputs/ 并启动 http://localhost:5173/
|
|
||||||
bun run build # 数据校验 + 类型检查 + 产物构建(输出到 site/dist)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 贡献一个模型产出 / Contributing a run
|
## 贡献一个模型产出 / Contributing a run
|
||||||
@@ -97,4 +95,4 @@ bun run build # 数据校验 + 类型检查 + 产物构建(输出到 site/d
|
|||||||
- `contributor` 填你的 GitHub 用户名,站点会在产出旁展示你的头像并链接到主页;
|
- `contributor` 填你的 GitHub 用户名,站点会在产出旁展示你的头像并链接到主页;
|
||||||
- 新组合的 `harness`(运行环境)与 `effort`(思考配额)请如实填写:站点会据此在测评页生成「运行环境 / 思考配额」metadata 徽章,并自动为模型、厂商、Harness 匹配品牌 icon(来自 [lobe-icons](https://github.com/lobehub/lobe-icons)),贡献者无需处理任何图标。
|
- 新组合的 `harness`(运行环境)与 `effort`(思考配额)请如实填写:站点会据此在测评页生成「运行环境 / 思考配额」metadata 徽章,并自动为模型、厂商、Harness 匹配品牌 icon(来自 [lobe-icons](https://github.com/lobehub/lobe-icons)),贡献者无需处理任何图标。
|
||||||
|
|
||||||
提 PR 后 CI 会跑数据校验 + 构建;合入 `main` 自动部署。
|
提 PR 后 CI 自动校验数据;合入 `main` 后站点自动重建(通常即时,最长 6 小时)。
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
// Validates the contribution data: models/<model-id>.json registrations and
|
||||||
|
// their artifacts under outputs/. Runs in CI for every PR and push, so a
|
||||||
|
// malformed or undocumented contribution fails before it can ship.
|
||||||
|
import { existsSync, readFileSync, readdirSync } from 'node:fs'
|
||||||
|
import { dirname, join } from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
|
||||||
|
const root = join(dirname(fileURLToPath(import.meta.url)), '..')
|
||||||
|
const modelsDir = join(root, 'models')
|
||||||
|
const outputsDir = join(root, 'outputs')
|
||||||
|
|
||||||
|
const cases = JSON.parse(readFileSync(join(root, 'cases.json'), 'utf8')) as Array<{
|
||||||
|
id: string
|
||||||
|
kind: string
|
||||||
|
}>
|
||||||
|
const caseKind = new Map(cases.map((c) => [c.id, c.kind]))
|
||||||
|
|
||||||
|
const ID_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/
|
||||||
|
const errors: string[] = []
|
||||||
|
|
||||||
|
const isBilingual = (v: unknown): boolean => {
|
||||||
|
if (!v || typeof v !== 'object') return false
|
||||||
|
const { zh, en } = v as { zh?: unknown; en?: unknown }
|
||||||
|
return typeof zh === 'string' && zh.trim() !== '' && typeof en === 'string' && en.trim() !== ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const modelIds = new Set<string>()
|
||||||
|
for (const f of readdirSync(modelsDir).filter((f) => f.endsWith('.json'))) {
|
||||||
|
const id = f.replace(/\.json$/, '')
|
||||||
|
modelIds.add(id)
|
||||||
|
if (!ID_RE.test(id)) {
|
||||||
|
errors.push(`models/${f}: id "${id}" must be lowercase letters/digits/dashes only (it doubles as the outputs/ folder name)`)
|
||||||
|
}
|
||||||
|
|
||||||
|
type RunJson = { note?: unknown; file?: unknown; contributor?: unknown }
|
||||||
|
let def: { label?: unknown; vendor?: unknown; order?: unknown; runs?: Record<string, RunJson | RunJson[]> }
|
||||||
|
try {
|
||||||
|
def = JSON.parse(readFileSync(join(modelsDir, f), 'utf8'))
|
||||||
|
} catch (e) {
|
||||||
|
errors.push(`models/${f}: invalid JSON (${e})`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof def.label !== 'string' || !def.label.trim()) errors.push(`models/${f}: "label" is required`)
|
||||||
|
if (typeof def.vendor !== 'string' || !def.vendor.trim()) errors.push(`models/${f}: "vendor" is required`)
|
||||||
|
if (def.order !== undefined && typeof def.order !== 'number') errors.push(`models/${f}: "order" must be a number`)
|
||||||
|
|
||||||
|
for (const [caseId, runRaw] of Object.entries(def.runs ?? {})) {
|
||||||
|
if (!caseKind.has(caseId)) {
|
||||||
|
errors.push(`models/${f}: unknown case "${caseId}" (known: ${[...caseKind.keys()].join(', ')})`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const variants = Array.isArray(runRaw) ? runRaw : [runRaw]
|
||||||
|
if (!variants.length) errors.push(`models/${f}: runs.${caseId} is an empty array`)
|
||||||
|
const seenFiles = new Set<string>()
|
||||||
|
variants.forEach((run, i) => {
|
||||||
|
const where = variants.length > 1 ? `runs.${caseId}[${i}]` : `runs.${caseId}`
|
||||||
|
if (!isBilingual(run?.note)) {
|
||||||
|
errors.push(`models/${f}: ${where} needs a bilingual "note" ({ "zh", "en" }) documenting provenance (tool, effort level, one-shot or fixed)`)
|
||||||
|
}
|
||||||
|
if (run?.file !== undefined && typeof run.file !== 'string') {
|
||||||
|
errors.push(`models/${f}: ${where}.file must be a string`)
|
||||||
|
}
|
||||||
|
if (run?.contributor !== undefined && (typeof run.contributor !== 'string' || !run.contributor.trim())) {
|
||||||
|
errors.push(`models/${f}: ${where}.contributor must be a non-empty GitHub username`)
|
||||||
|
}
|
||||||
|
const file = (typeof run?.file === 'string' && run.file) || `${caseId}.${caseKind.get(caseId)}`
|
||||||
|
if (seenFiles.has(file)) {
|
||||||
|
errors.push(`models/${f}: ${where} resolves to the same artifact "${file}" as another variant — extra variants must set a distinct "file"`)
|
||||||
|
}
|
||||||
|
seenFiles.add(file)
|
||||||
|
if (!existsSync(join(outputsDir, id, file))) {
|
||||||
|
errors.push(`models/${f}: ${where} declared but artifact missing: outputs/${id}/${file}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const entry of readdirSync(outputsDir, { withFileTypes: true })) {
|
||||||
|
if (entry.isDirectory() && !modelIds.has(entry.name)) {
|
||||||
|
errors.push(`outputs/${entry.name}/ exists but models/${entry.name}.json is missing`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every file under outputs/<id>/ must be declared by a run — loose artifacts
|
||||||
|
// would otherwise ship without provenance.
|
||||||
|
for (const id of modelIds) {
|
||||||
|
const dir = join(outputsDir, id)
|
||||||
|
if (!existsSync(dir)) continue
|
||||||
|
const def = JSON.parse(readFileSync(join(modelsDir, `${id}.json`), 'utf8')) as {
|
||||||
|
runs?: Record<string, { file?: string } | Array<{ file?: string }>>
|
||||||
|
}
|
||||||
|
const declared = new Set<string>()
|
||||||
|
for (const [caseId, runRaw] of Object.entries(def.runs ?? {})) {
|
||||||
|
for (const run of Array.isArray(runRaw) ? runRaw : [runRaw]) {
|
||||||
|
declared.add(run?.file ?? `${caseId}.${caseKind.get(caseId)}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const file of readdirSync(dir)) {
|
||||||
|
if (!declared.has(file)) {
|
||||||
|
errors.push(`outputs/${id}/${file} is not declared by any run in models/${id}.json`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.length) {
|
||||||
|
console.error(`Data validation failed (${errors.length}):\n- ${errors.join('\n- ')}`)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
console.log(`Data OK: ${modelIds.size} models, ${caseKind.size} cases validated`)
|
||||||
Reference in new issue
Block a user