Repository navigation
Expand file tree
/
Copy pathjustfile
More file actions
338 lines (274 loc) · 14.1 KB
/
Copy pathjustfile
File metadata and controls
338 lines (274 loc) · 14.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
# Fanout — task runner. Run `just` with no arguments to list every recipe.
#
# Naming: recipes are named <subsystem>-<action>, so `just --list` (which sorts
# alphabetically) groups them. Unprefixed recipes act on the Go module, which
# is the primary artifact; `ui-*` and `db-*` namespace the rest. A `-check`
# suffix always means non-mutating verification, safe to run on a dirty tree.
#
# The ordering rule this file exists to encode: the browser workspace writes
# into `internal/ui/dist` and `internal/mcp/apps`, both of which are committed
# and compiled in via `go:embed`. The binary is therefore only as fresh as the
# last UI build, so `build` always runs the UI first and `ui-check` guards the
# committed bytes against drifting from their source.
set shell := ["bash", "-euo", "pipefail", "-c"]
export CGO_ENABLED := "1"
# Keep every local Go recipe on the same exact toolchain as CI and release
# builds. This is especially important for generated dependency inventories,
# which can change when a newer Go version selects a different module graph.
go_version := `awk '$1 == "go" { print $2; exit }' go.mod`
export GOTOOLCHAIN := "go" + go_version
# Match the runtime Goose dependency; compile only its SQLite CLI driver.
goose_version := `awk '$1 == "github.com/pressly/goose/v3" { print $2; exit }' go.mod`
goose := "go run -tags=no_postgres,no_mysql,no_mssql,no_clickhouse,no_vertica,no_ydb,no_libsql github.com/pressly/goose/v3/cmd/goose@" + goose_version
# Everything `go:embed` compiles into the binary. Keep in sync with `outDir` in
# ui/host/vite.config.ts and ui/host/vite.apps.config.ts.
embedded := "internal/ui/dist internal/mcp/apps"
default:
@just --list
# Split out so CI installs exactly what a contributor does, without also
# writing git hooks into the runner.
# Browser dependencies only.
ui-deps:
cd ui/host && bun install --frozen-lockfile
# Everything needed to work on the repo: dependencies plus the git hooks.
install: ui-deps
lefthook install
# ── Build ────────────────────────────────────────────────────────────────────
# Portable React MCP Apps → internal/mcp/apps/panels.html
ui-apps:
cd ui/host && bun run build:apps
# AG-UI browser host → internal/ui/dist
ui-host:
cd ui/host && bun run build:host
# Every embedded browser asset.
ui: ui-lint ui-host ui-apps
# Browser assets, then the binary that embeds them.
build VERSION=`git describe --tags --always --dirty 2>/dev/null || echo dev`: ui
bash scripts/with-duckdb.sh go build -ldflags "-s -w -X main.version={{VERSION}}" -o bin/fanout ./cmd/fanout
# Disposable seeded instance and system Google Chrome; separate from check.
# Extra arguments go to Playwright, e.g. `just e2e e2e/performance.spec.ts`.
e2e *ARGS:
just build
cd ui/host && bun run e2e {{ARGS}}
# CI publishes ghcr.io/labstack/fanout; this is for trying the image locally
# without pushing anything.
# Build the container image.
docker TAG="local":
docker build --build-arg VERSION={{TAG}} -t fanout:{{TAG}} .
# ── Release ──────────────────────────────────────────────────────────────────
# Tags are CalVer: v{YYYY.M}.{N}, numbered from 0 within each month. Pushing
# the tag triggers release.yml, which builds the multi-architecture image,
# mirrors it to Docker Hub, publishes native archives, and moves `latest`.
# Tag the next CalVer release and push it.
release:
./scripts/release.sh
# Exercise reserved-tag retries without creating remote refs.
release-test:
bash ./scripts/release_test.sh
# Caller tags must never replace the pinned native binding.
duckdb-wrapper-test:
bash scripts/go-build-tags_test.sh
# Regenerate the legal inventory for every shipped Go target and browser app.
notices:
bash scripts/with-duckdb.sh go run ./internal/cmd/notices -root .
# Fail if THIRD_PARTY_NOTICES does not match the release dependency graph.
notices-check:
bash scripts/with-duckdb.sh go run ./internal/cmd/notices -root . -check
# ── Database ─────────────────────────────────────────────────────────────────
# Regenerate the SQLite query bindings with sqlc.
db-gen:
cd internal/db && sqlc generate
# Create a timestamped SQLite SQL migration. Edit its Up section, then db-gen.
db-migrate-create NAME:
{{goose}} -dir internal/db/migrations create {{NAME}} sql
# Apply migrations (development; production auto-applies on boot).
db-migrate-apply DB="data/control/fanout.sqlite":
go run ./internal/cmd/dbmigrate -db "{{DB}}"
# ── Go quality ───────────────────────────────────────────────────────────────
fmt:
gofmt -w .
# Non-mutating counterpart to `fmt`, for the gate.
fmt-check:
@if [ -n "$(gofmt -l .)" ]; then gofmt -d .; exit 1; fi
# Standalone; `lint` already runs govet via golangci-lint's standard set.
vet:
bash scripts/with-duckdb.sh go vet ./...
lint:
bash scripts/with-duckdb.sh golangci-lint run
test *ARGS='./...':
bash scripts/with-duckdb.sh go test {{ARGS}}
# duckdb-go's CGO vector readers trip checkptr's unsafe-pointer *alignment*
# check under -race on linux/amd64 and abort with "misaligned pointer
# conversion". The read is benign at runtime. `-d=checkptr=0` disables only
# that alignment check — the -race data-race detector stays on.
# Run the Go tests under the race detector.
test-race:
bash scripts/with-duckdb.sh go test -race -gcflags=all=-d=checkptr=0 ./...
# ── Browser quality ──────────────────────────────────────────────────────────
# `ui` and `ui-check` already run ui-lint once, so the gate does
# not invoke this separately — it exists for fast feedback on its own.
# Type-check the host, shared panels, app and build configurations.
ui-lint:
cd ui/host && bun run lint
# Run the browser host's vitest suite.
ui-test:
cd ui/host && bun run test
# Shared UI modules remain package-free.
ui-boundaries-check:
bun scripts/ui-boundaries.mjs
# Audit the one locked browser graph. GHSA-vfj7-8cjw-p6xm reaches host
# through vite-plugin-singlefile > micromatch at build time, on our own globs.
# Drop this exception when a patched braces release is available.
ui-audit:
cd ui/host && bun audit --ignore=GHSA-vfj7-8cjw-p6xm
# Compare temporary builds without requiring clean or committed embeds.
ui-check:
#!/usr/bin/env bash
set -euo pipefail
staged=$(mktemp -d)
trap 'rm -rf "$staged"' EXIT
FANOUT_UI_OUT="$staged/ui" FANOUT_APPS_OUT="$staged/apps" just ui
bun scripts/ui-compare.mjs internal/ui/dist "$staged/ui"
bun scripts/ui-compare.mjs internal/mcp/apps "$staged/apps" --apps
# ── Diagrams ─────────────────────────────────────────────────────────────────
# Renders docs/diagrams/*.d2 beside each source. Deliberately outside `build`
# and `check`: the SVG is committed, so only someone editing a diagram needs d2
# installed at all.
#
# D2 output changes between minor versions, so the recipe pins the generator
# used for committed SVGs instead of depending on a contributor's installed
# binary.
# Render every d2 diagram to SVG and publish the same assets on the docs site.
diagrams:
#!/usr/bin/env bash
set -euo pipefail
for src in docs/diagrams/*.d2; do
go run oss.terrastruct.com/d2@v0.7.1 "$src" "${src%.d2}.svg"
done
mkdir -p site/public/diagrams
cp docs/diagrams/*.svg site/public/diagrams/
# Fail when the site serves a diagram other than the committed documentation
# copy. Regeneration stays a deliberate writing step.
diagrams-check:
#!/usr/bin/env bash
set -euo pipefail
for src in docs/diagrams/*.svg; do
dst="site/public/diagrams/$(basename "$src")"
if ! cmp -s "$src" "$dst"; then
echo "$dst does not match $src; run 'just diagrams'" >&2
exit 1
fi
done
# ── Documentation site ───────────────────────────────────────────────────────
# Rewrite the generated settings reference from internal/config.
docs-generate:
bash scripts/with-duckdb.sh go run ./cmd/fanout-docgen
# Fail when a committed settings page is behind the configuration type.
docs-generate-check:
bash scripts/with-duckdb.sh go run ./cmd/fanout-docgen --check
# Install the documentation site's dependencies from the lockfile.
#
# Named to match `ui-deps`, and used by CI for the same reason: `check` ends with
# site-build, which fails as a bare exit 127 when node_modules is absent.
site-deps:
cd site && npm ci --no-audit --no-fund
# Add or update a site dependency. `site-deps` is the reproducible install;
# this is the one that may change the lockfile.
site-install:
cd site && npm install --no-audit --no-fund
# Serve the documentation site locally with live reload.
site: docs-generate
cd site && npm run dev
# Build the documentation site into site/dist.
#
# Depends on the *checking* recipe, not the writing one: `check` is a gate, and
# a gate that rewrites the tree it is inspecting cannot tell you whether the
# tree was already right. Use `just site` for the writing path during
# development.
site-build: docs-generate-check diagrams-check site-deps
cd site && npm run build
# Renders the social preview card from docs/media/social-card.typ into
# site/public, where GitHub takes it as the repository preview and the site
# serves it as its og:image.
#
# Outside `build` and `check` for the same reason as the diagrams above: the PNG
# is committed, and CI has no font path to re-render it with. `site-build`
# asserts the committed PNG is present and correctly sized, which is the part a
# gate can honestly do.
#
# The card needs Inter, which is not vendored here — the browser bundle carries
# its own copy for the UI, and a second copy in the tree to draw one image is a
# poor trade. Typst warns about an unknown font family and still exits 0, so a
# missing face does not fail the render; it quietly produces a card set in
# whatever typst found instead. This reads the warning back and fails on it.
#
# The version is pinned for the same reason d2 is: typst re-lays out text
# between releases, so a different one rewrites every glyph position and
# produces a diff that is not a real change.
#
# The mark comes from ui/host/public/favicon.svg, the canonical asset that
# internal/brand tracks, so the card cannot drift from the product logo.
social-card:
#!/usr/bin/env bash
set -euo pipefail
want_typst="0.15.1"
if ! command -v typst >/dev/null; then
echo "typst not found — install with: brew install typst" >&2
exit 1
fi
have_typst=$(typst --version | awk '{print $2}')
if [ "$have_typst" != "$want_typst" ]; then
echo "typst $have_typst is installed; the committed card was rendered with $want_typst" >&2
echo "another version re-lays out every glyph, so the diff would not be a real change" >&2
exit 1
fi
if [ -z "${FANOUT_FONT_PATH:-}" ]; then
echo "set FANOUT_FONT_PATH to a directory holding Inter" >&2
echo "the OFL original is at https://github.com/google/fonts/tree/main/ofl/inter" >&2
exit 1
fi
# Rendered aside and moved into place only once it is known good, so a run
# that fell back to a substitute face cannot leave that card in the tree.
# A directory, not `mktemp -t <name>`: that reserves a name without the .png
# suffix typst needs, so appending one both leaves the reserved file behind
# and writes to a path nothing reserved.
staged_dir=$(mktemp -d)
trap 'rm -rf "$staged_dir"' EXIT
staged="$staged_dir/social-card.png"
render=$(typst compile --root . --font-path "$FANOUT_FONT_PATH" --ppi 96 --format png \
docs/media/social-card.typ "$staged" 2>&1)
if [ -n "$render" ]; then echo "$render"; fi
if echo "$render" | grep -qi "unknown font family"; then
echo "typst could not find a font it was asked for — the card above is set in a fallback face" >&2
exit 1
fi
mv "$staged" site/public/social-card.png
echo "rendered site/public/social-card.png"
# ── Gate ─────────────────────────────────────────────────────────────────────
# lefthook's pre-push hook and CI both run this.
#
# docs-generate-check comes before the site build for the same reason it exists:
# a settings page that no longer matches the type the loader binds is a
# documented setting the binary would reject, and it should fail here rather
# than be published.
check: script-tests dashboard-eval-generate-check test-names-check go-deadcode ui-deadcode fmt-check ui-boundaries-check lint release-test duckdb-wrapper-test ui-audit ui-check notices-check test ui-test docs-generate-check site-build
@echo "All checks passed"
clean:
rm -rf bin
ui-deadcode:
cd ui/host && bun run deadcode
go-deadcode:
node scripts/go-deadcode.mjs
# Run every script test, including failure injection, as part of CI.
script-tests:
bun test scripts/*.test.ts scripts/dashboard-eval
bun scripts/dashboard-eval/main.ts --mock --no-output
# Test names describe behavior; specification scenarios such as S8 are allowed.
test-names-check:
node scripts/test-names.mjs
# Regenerate the eval response fixtures through the native server implementation.
dashboard-eval-generate:
FANOUT_EVAL_UPDATE_GOLDENS=1 just test ./internal/agent -run TestDashboardEvalGoldensCurrent
# Force fresh server execution so test caching cannot hide response-shape drift.
dashboard-eval-generate-check:
just test ./internal/agent -count=1 -run TestDashboardEvalGoldensCurrent