-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathjustfile
More file actions
390 lines (346 loc) · 15.6 KB
/
Copy pathjustfile
File metadata and controls
390 lines (346 loc) · 15.6 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
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
# Load .env so overriding a port is a one-line edit, not an exported variable.
set dotenv-load := true
backend_dir := "apps/backend"
frontend_dir := "apps/frontend"
log_dir := justfile_directory() / "logs"
# Development ports. Deliberately not the usual 5432 / 8080 / 5173: a developer
# runs several projects on one machine, and a port clash at first launch is a
# terrible first impression. Override in .env if these clash too.
dev_db_port := env("CHALENDIA_DEV_DB_PORT", "5442")
dev_api_port := env("CHALENDIA_DEV_API_PORT", "8090")
dev_web_port := env("CHALENDIA_DEV_WEB_PORT", "5183")
dev_db_url := "postgres://chalendia:chalendia@127.0.0.1:" + dev_db_port + "/chalendia"
compose_dev := "docker compose -f docker-compose.yaml -f docker-compose.dev.yaml"
# The journeys are replayed once per variant, so the report shows the theme and
# the phone layout the design claims. Narrow it with E2E_VARIANTS to iterate.
e2e_variants := "desktop-light desktop-dark mobile-light"
default:
@just --list
# =============================================================================
# DEV — everything needed to get a shop running locally
# =============================================================================
# One-time setup: prerequisites, git hooks, .env
dev-setup:
#!/usr/bin/env bash
set -euo pipefail
missing=()
command -v cargo >/dev/null || missing+=("rust (https://rustup.rs)")
command -v node >/dev/null || missing+=("node (https://nodejs.org/)")
command -v docker >/dev/null || missing+=("docker (https://docs.docker.com/engine/install/)")
# The AVIF encoder builds its assembly paths with nasm and refuses to build
# without it (docs/backend/adr/0008-image-pipeline.md).
command -v nasm >/dev/null || missing+=("nasm (https://nasm.us, or your package manager)")
if [ ${#missing[@]} -gt 0 ]; then
echo "missing prerequisites:"
for tool in "${missing[@]}"; do echo " - $tool"; done
exit 1
fi
docker info >/dev/null 2>&1 || { echo "the docker daemon is not running"; exit 1; }
git config core.hooksPath .githooks
if [ ! -f .env ]; then
cp .env.example .env
echo "created .env from .env.example"
fi
echo "prerequisites present, git hooks enabled"
echo "next: just dev-start"
# Start the database, apply migrations, run the API and the dev server
dev-start: dev-db
#!/usr/bin/env bash
set -euo pipefail
just _dev-run-api
just _dev-run-web
echo ""
echo " shop http://localhost:{{dev_web_port}}"
echo " api http://localhost:{{dev_api_port}}/api/health"
echo " database localhost:{{dev_db_port}}"
echo ""
echo " logs: just dev-logs stop: just dev-stop"
# Stop the dev server, the API and the database
dev-stop:
#!/usr/bin/env bash
set -euo pipefail
just _dev-kill web
just _dev-kill api
{{compose_dev}} stop database >/dev/null 2>&1 || true
echo "stopped"
# What is running right now
dev-status:
#!/usr/bin/env bash
set -euo pipefail
for name in api web; do
pid_file="{{log_dir}}/${name}.pid"
if [ -f "$pid_file" ] && kill -0 "$(cat "$pid_file")" 2>/dev/null; then
echo "${name}: running (pid $(cat "$pid_file"))"
else
echo "${name}: stopped"
fi
done
if docker compose ps --status running --services 2>/dev/null | grep -qx database; then
echo "database: running on port {{dev_db_port}}"
else
echo "database: stopped"
fi
# Follow the API and dev server output
dev-logs:
#!/usr/bin/env bash
set -euo pipefail
mkdir -p {{log_dir}}
touch {{log_dir}}/api.log {{log_dir}}/web.log
tail -f {{log_dir}}/api.log {{log_dir}}/web.log
# Start the development database and wait until it answers queries
dev-db:
#!/usr/bin/env bash
set -euo pipefail
docker info >/dev/null 2>&1 || { echo "the docker daemon is not running"; exit 1; }
{{compose_dev}} up -d database
echo "waiting for postgres on port {{dev_db_port}}..."
# `pg_isready` answers yes during the image's own init restart, so poll a
# real query instead: that is what the API will do.
until docker compose exec -T database psql -U chalendia -d chalendia -c 'select 1' >/dev/null 2>&1; do
sleep 1
done
echo "database ready"
# Wipe the development database and start again from an empty schema
dev-db-reset:
#!/usr/bin/env bash
set -euo pipefail
just dev-stop
{{compose_dev}} down -v
just dev-db
# Open a psql shell on the development database
dev-db-shell:
docker compose exec database psql -U chalendia -d chalendia
_dev-run-api:
#!/usr/bin/env bash
set -euo pipefail
just _dev-guard api {{dev_api_port}}
mkdir -p {{log_dir}}
cd {{backend_dir}}
echo "building the api..."
# Offline: the macros need a schema at compile time, and a database that was
# just created has none — the binary being built is what migrates it.
SQLX_OFFLINE=true cargo build --quiet
DATABASE_URL="{{dev_db_url}}" \
CHALENDIA_PUBLIC_URL="http://localhost:{{dev_web_port}}" \
CHALENDIA_BIND="127.0.0.1:{{dev_api_port}}" \
CHALENDIA_CORS_ORIGINS="http://localhost:{{dev_web_port}}" \
./target/debug/chalendia-backend > {{log_dir}}/api.log 2>&1 &
echo $! > {{log_dir}}/api.pid
# Migrations run before the listener opens, so a reachable /health proves
# the schema is in place too.
for _ in $(seq 1 60); do
curl -sf "http://127.0.0.1:{{dev_api_port}}/api/health" >/dev/null 2>&1 && break
kill -0 "$(cat {{log_dir}}/api.pid)" 2>/dev/null || { echo "the api died on startup:"; tail -5 {{log_dir}}/api.log; exit 1; }
sleep 0.5
done
echo "api started (pid $(cat {{log_dir}}/api.pid), logs → logs/api.log)"
_dev-run-web:
#!/usr/bin/env bash
set -euo pipefail
just _dev-guard web {{dev_web_port}}
mkdir -p {{log_dir}}
[ -d {{frontend_dir}}/node_modules ] || (cd {{frontend_dir}} && npm ci)
cd {{frontend_dir}}
CHALENDIA_DEV_API_URL="http://127.0.0.1:{{dev_api_port}}" \
npm run dev -- --port {{dev_web_port}} --strictPort > {{log_dir}}/web.log 2>&1 &
echo $! > {{log_dir}}/web.pid
for _ in $(seq 1 60); do
curl -sf "http://localhost:{{dev_web_port}}" >/dev/null 2>&1 && break
sleep 0.5
done
echo "dev server started (pid $(cat {{log_dir}}/web.pid), logs → logs/web.log)"
# Refuse to start a second copy rather than leaving an orphan on the port
_dev-guard name port:
#!/usr/bin/env bash
set -euo pipefail
pid_file="{{log_dir}}/{{name}}.pid"
if [ -f "$pid_file" ] && kill -0 "$(cat "$pid_file")" 2>/dev/null; then
echo "{{name}} is already running (pid $(cat "$pid_file")). Run 'just dev-stop' first."
exit 1
fi
if ss -ltn 2>/dev/null | grep -q ":{{port}} "; then
echo "port {{port}} is already in use by something else. Set CHALENDIA_DEV_$(echo {{name}} | tr a-z A-Z)_PORT in .env."
exit 1
fi
rm -f "$pid_file"
_dev-kill name:
#!/usr/bin/env bash
set -euo pipefail
pid_file="{{log_dir}}/{{name}}.pid"
[ -f "$pid_file" ] || exit 0
pid=$(cat "$pid_file")
if kill -0 "$pid" 2>/dev/null; then
# The dev server spawns a child that holds the port; kill the children
# too or the next start hits a port that is still bound.
pkill -TERM -P "$pid" 2>/dev/null || true
kill -TERM "$pid" 2>/dev/null || true
for _ in $(seq 1 20); do
kill -0 "$pid" 2>/dev/null || break
sleep 0.5
done
kill -KILL "$pid" 2>/dev/null || true
fi
rm -f "$pid_file"
# =============================================================================
# SETUP
# =============================================================================
# Enable the repository's git hooks — run once per clone
hooks-install:
git config core.hooksPath .githooks
@echo "hooks enabled: $(git config core.hooksPath)"
# Check whether this clone actually runs the repository's hooks
hooks-check:
@test "$(git config core.hooksPath)" = ".githooks" \
&& echo "ok: hooks are active" \
|| (echo "INACTIVE: run 'just hooks-install' — every hook here is inert"; exit 1)
# Install frontend dependencies
frontend-install:
cd {{frontend_dir}} && npm ci
# =============================================================================
# GATES — what CI runs, runnable locally with the same command
# =============================================================================
# Format and lint the backend. Offline on purpose: the committed query cache is
# what makes linting possible without a database running.
backend-check:
cd {{backend_dir}} && cargo fmt --check
cd {{backend_dir}} && SQLX_OFFLINE=true cargo clippy --all-targets -- -D warnings
# Needs a database: the integration tests run against a real one.
backend-test:
#!/usr/bin/env bash
set -euo pipefail
url="${DATABASE_URL:?DATABASE_URL is not set — copy .env.example to .env}"
# Tests what matters — the database answers — rather than how it was
# started, so this works the same locally and in CI.
hostport="${url#*@}"; hostport="${hostport%%/*}"
host="${hostport%%:*}"; port="${hostport##*:}"
if ! timeout 2 bash -c "cat < /dev/null > /dev/tcp/${host}/${port}" 2>/dev/null; then
echo "no database answering on ${host}:${port} — start one with 'just dev-db'"
exit 1
fi
# Compiled against the committed query cache, run against a real database:
# the macros need a schema at compile time, and CI's service container has
# none until the suite itself migrates its temporary databases.
cd {{backend_dir}} && SQLX_OFFLINE=true cargo test
# Regenerate the committed query cache — run after changing any SQL
backend-sqlx-prepare:
cd {{backend_dir}} && cargo sqlx prepare -- --all-targets
# Fail when the committed query cache no longer matches the queries.
# Migrates first: the macros are verified against a schema, and a database that
# has never been migrated has none — which is every fresh CI service container.
backend-sqlx-check:
cd {{backend_dir}} && cargo sqlx migrate run
cd {{backend_dir}} && cargo sqlx prepare --check -- --all-targets
frontend-check:
cd {{frontend_dir}} && npm run typecheck
cd {{frontend_dir}} && npm run lint
frontend-test:
cd {{frontend_dir}} && npm run test
frontend-build:
cd {{frontend_dir}} && npm run build
# =============================================================================
# API CONTRACT — generated from the handlers, consumed by the frontend
# =============================================================================
# Regenerate the contract and the frontend types it feeds
api-generate:
cd {{backend_dir}} && SQLX_OFFLINE=true cargo run --quiet --bin openapi > api/openapi.json
cd {{frontend_dir}} && npm run codegen:api
# Fail when the committed frontend types no longer match the contract.
# The contract's own freshness is checked by a backend test, so a stale
# document fails `just backend-test` rather than needing node here.
api-check:
#!/usr/bin/env bash
set -euo pipefail
cd {{frontend_dir}} && npm run codegen:api >/dev/null
cd {{justfile_directory()}}
if ! git diff --quiet -- {{frontend_dir}}/src/shared/api/generated; then
echo "the generated api types are stale — run 'just api-generate' and commit the result"
git diff --stat -- {{frontend_dir}}/src/shared/api/generated
exit 1
fi
echo "api types: current"
# =============================================================================
# END TO END — the journeys, and the captures ozalid reviews them from
# =============================================================================
# Run the journeys against a shop created from scratch, once per variant
e2e:
#!/usr/bin/env bash
set -euo pipefail
trap 'just dev-stop >/dev/null 2>&1 || true' EXIT
# Stale reports would be pushed alongside this run's, so the previous ones go.
rm -rf {{frontend_dir}}/tmp/e2e-report
# A failing variant is the most interesting thing to look at, so the run
# goes on to the end — the exit code carries the verdict.
failed=0
for variant in ${E2E_VARIANTS:-{{e2e_variants}}}; do
echo "── ${variant} ───────────────────────────────────────────────"
# Setup happens once, so each variant starts from an empty database —
# that is the behaviour under test, not an inconvenience to work around.
just dev-stop
{{compose_dev}} down -v
# Not silenced: a stack that failed to start must fail here, not later
# as a connection refused in every case.
just dev-start
for _ in $(seq 1 60); do
curl -sf "http://localhost:{{dev_web_port}}" >/dev/null 2>&1 \
&& curl -sf "http://localhost:{{dev_api_port}}/api/shop" >/dev/null 2>&1 && break
sleep 1
done
cd {{frontend_dir}}
E2E_BASE_URL="http://localhost:{{dev_web_port}}" E2E_VARIANT="${variant}" npx playwright test || failed=1
cd {{justfile_directory()}}
done
exit "${failed}"
# Run the journeys against the container image, the way CI and an operator do
e2e-image:
#!/usr/bin/env bash
# One origin, no dev server — the shape a merchant actually installs.
set -euo pipefail
trap 'docker compose down -v >/dev/null 2>&1 || true' EXIT
rm -rf {{frontend_dir}}/tmp/e2e-report
failed=0
for variant in ${E2E_VARIANTS:-{{e2e_variants}}}; do
echo "── ${variant} ───────────────────────────────────────────────"
docker compose down -v >/dev/null 2>&1 || true
CHALENDIA_PORT={{dev_api_port}} docker compose up -d --build
for _ in $(seq 1 90); do
curl -sf "http://localhost:{{dev_api_port}}/api/shop" >/dev/null 2>&1 && break
sleep 2
done
cd {{frontend_dir}}
E2E_BASE_URL="http://localhost:{{dev_api_port}}" E2E_VARIANT="${variant}" npx playwright test || failed=1
cd {{justfile_directory()}}
done
exit "${failed}"
# Test the tooling. Plain `node --test`: these are scripts, not an application,
# and the frontend's runner would have to be taught about a directory outside it.
tools-test:
node --test tools/
# Push the last run's captures to ozalid, where they are reviewed
ozalid-push:
#!/usr/bin/env bash
# Local and before the pull request: the visual result is one of the two
# things only the user grants, and a capture reaching CI is a capture
# nobody looked at in time. What changed is not computed here — ozalid is
# asked which content it does not hold, and that is what changed.
set -euo pipefail
set -a; . ./.env; set +a
OZALID_REVISION="$(git rev-parse --short HEAD)" node tools/ozalid/push.mjs "$@"
# Show what a push would send, and write nothing
ozalid-dry:
#!/usr/bin/env bash
set -euo pipefail
set -a; . ./.env; set +a
node tools/ozalid/push.mjs --dry-run
# Everything a pull request must pass, in one command
check: backend-check backend-test frontend-check frontend-test frontend-build api-check tools-test
# =============================================================================
# PACKAGING
# =============================================================================
# Build the container image
image:
docker build -t chalendia:dev .
# Run the shop the way an operator does: image plus database, nothing else
up:
docker compose up -d --build
down:
docker compose down