diff --git a/CLAUDE.md b/CLAUDE.md index 931e0fb8..ba3888ae 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -111,7 +111,7 @@ Strong success criteria let you loop independently. Weak criteria ("make it work - `datagen/` — demo-data generator; separate Go module `github.com/oxynote/oxynote/datagen`. Demo/testing only. - `e2e/` — Playwright end-to-end suite (`@oxynote/e2e`, **pnpm**) plus the two docker-compose stacks it drives: the dev stack built from this repo, and the all-in-one image `docker/prod/` produces. Not shipped; it exercises the composed product through a real backend. Every file, script and make target belonging to one stack is named `dev` or `prod` — neither is the unnamed default. Details: [e2e/CLAUDE.md](e2e/CLAUDE.md). - `scripts/` — helpers the root Makefile calls. `run-quietly.sh` runs a build step with its output held back, replaying the log only if the step fails. -- `docker/` — dev docker-compose stack, Caddyfile, `env/` (committed `*.example.env` templates; `make setup` copies them to the gitignored `*.local.env` files the compose stack reads, and `web.example.env` also to `web/.env` for the host dev server and electron builds), `demo/` (demo-data configs for mariadb/postgres). +- `docker/` — dev docker-compose stack, Caddyfile, `env/` (committed `*.example.env` templates; `make setup` copies them to the gitignored `*.local.env` files the compose stack reads, and `web.example.env` also to `web/.env` for the host dev server and electron builds; `sync-env.sh` reconciles an existing `*.local.env` with its template, since `make setup` only copies when the local file is missing), `demo/` (demo-data configs for mariadb/postgres). - `docker/prod/` — the production all-in-one image: alpine-based Dockerfile (built via `make prod-build`; core comes from goreleaser), the image's Caddyfile (sibling of `docker/Caddyfile`), the TS launcher (`@oxynote/launcher`, **pnpm**) that validates the flat public `OXYNOTE_*` env, generates internal secrets, and supervises caddy + web + core + auth-realtime, plus the example compose deployment and the local override `make prod-run` layers on it. Details: [docker/prod/CLAUDE.md](docker/prod/CLAUDE.md). **One `.gitignore`, and it lives at the repository root.** Components do not carry their own — a rule for a nested directory is written with its path (`e2e/test-results/`, `web/coverage/`), so every exclusion in the repository is readable in one file. @@ -127,6 +127,8 @@ make run # build images + run the dev stack in the foreground (ctrl-c stop make start # build images + run the dev stack in the background make dev # backend containers + web dev server on the host (hot reload, :3000) make stop # stop the dev stack +make check-env # report variables that drifted between *.example.env and *.local.env +make sync-env # rewrite *.local.env from the templates, keeping existing values make lint # fix lint/format/type issues in web, auth-realtime, core, datagen, e2e, launcher make check-lint # the same gates, verification only diff --git a/Makefile b/Makefile index b4a92881..183001c9 100644 --- a/Makefile +++ b/Makefile @@ -28,18 +28,18 @@ QUIET := scripts/run-quietly.sh # backend containers + web dev server on the host with hot reload. Ctrl-c # stops the web dev server; `make stop` stops the containers. .PHONY: dev -dev: build-go sync-changedetection-key +dev: check-env build-go sync-changedetection-key $(COMPOSE) up --build -d cd web && NUXT_PUBLIC_APP_BASE_URL=http://localhost:3000 pnpm run start:dev:web # foreground: streams all logs, ctrl-c stops the stack .PHONY: run -run: build-go sync-changedetection-key +run: check-env build-go sync-changedetection-key $(COMPOSE) --profile web up --build # background .PHONY: start -start: build-go sync-changedetection-key +start: check-env build-go sync-changedetection-key $(COMPOSE) --profile web up --build -d .PHONY: stop @@ -189,4 +189,16 @@ prod-stop: .PHONY: sync-changedetection-key sync-changedetection-key: $(COMPOSE) up -d changedetection - ./docker/sync-changedetection-key.sh \ No newline at end of file + ./docker/sync-changedetection-key.sh + +# a variable renamed in a template never reaches an env file that already +# exists, so the stack starts against a name nobody reads. check-env fails +# before that happens; sync-env rewrites the local files from the +# templates, keeping the value of every variable they still list. +.PHONY: check-env +check-env: + ./docker/sync-env.sh + +.PHONY: sync-env +sync-env: + ./docker/sync-env.sh --fix \ No newline at end of file diff --git a/docker/sync-env.sh b/docker/sync-env.sh new file mode 100755 index 00000000..7917659d --- /dev/null +++ b/docker/sync-env.sh @@ -0,0 +1,105 @@ +#!/bin/sh +set -eu + +# `make setup` copies each *.example.env to its *.local.env only when the +# local file is missing, so a variable that is renamed, added or removed in +# a template never reaches an env file that already exists. Core then boots +# against a name nobody reads: at best a dependency silently disables +# itself, at worst it refuses to start. This script compares the variable +# names in every template against its local file — reporting the difference +# by default, rewriting the local file to match the template with --fix. +# +# --fix keeps the local value of every variable the template still lists, +# so secrets and local tweaks survive; variables the template has dropped +# are removed, which is the point. Run from the repository root. + +fix=false + +if [ "${1:-}" = "--fix" ]; then + fix=true +elif [ $# -gt 0 ]; then + echo "usage: sync-env.sh [--fix]" >&2 + exit 2 +fi + +keys() { + grep -oE '^[A-Za-z_][A-Za-z0-9_]*=' "$1" | tr -d '=' +} + +example_keys=$(mktemp) +local_keys=$(mktemp) + +trap 'rm -f "$example_keys" "$local_keys"' EXIT + +drifted=false + +for name in core auth-realtime web; do + example="docker/env/$name.example.env" + local_file="docker/env/$name.local.env" + + if [ ! -f "$local_file" ]; then + echo "sync-env: $local_file not found — run 'make setup' first" >&2 + exit 1 + fi + + keys "$example" | sort > "$example_keys" + keys "$local_file" | sort > "$local_keys" + + missing=$(comm -23 "$example_keys" "$local_keys") + unknown=$(comm -13 "$example_keys" "$local_keys") + + if [ -z "$missing" ] && [ -z "$unknown" ]; then + continue + fi + + drifted=true + + if [ "$fix" = false ]; then + echo "$local_file has drifted from $example:" + echo "$missing" | sed '/^$/d; s/^/ missing: /' + echo "$unknown" | sed '/^$/d; s/^/ unknown: /' + + continue + fi + + # rebuild the local file from the template so it picks up new + # variables, renames and comments, substituting the local value + # wherever the template still lists the variable. + awk ' + NR == FNR { + if ($0 ~ /^[A-Za-z_][A-Za-z0-9_]*=/) { + eq = index($0, "=") + value[substr($0, 1, eq - 1)] = substr($0, eq + 1) + } + + next + } + { + if ($0 ~ /^[A-Za-z_][A-Za-z0-9_]*=/) { + eq = index($0, "=") + key = substr($0, 1, eq - 1) + + if (key in value) { + print key "=" value[key] + + next + } + } + + print + } + ' "$local_file" "$example" > "$local_file.new" + + mv "$local_file.new" "$local_file" + + echo "sync-env: rewrote $local_file from $example" + echo "$missing" | sed '/^$/d; s/^/ added: /' + echo "$unknown" | sed '/^$/d; s/^/ dropped: /' +done + +if [ "$drifted" = false ]; then + echo "sync-env: env files are in sync" +elif [ "$fix" = false ]; then + echo "run 'make sync-env' to bring them in sync" + exit 1 +fi