fe180fd125
## Summary Add a Caddy reverse proxy behind a new `--profile serve-static` so a contributor can exercise the production build locally with the per-locale routing the on-prem reverse proxy will use (per ADR-0019). Closes the gap surfaced by PR #96: the locale switcher / accessibility route fusion / cookie plumbing all need a prod-faithful local setup, and `nx serve-static` falls short (no SPA fallback per locale, no smart `/` redirect, exposes the `http-server` directory-listing footgun we hit during the perf-gate fix in PR #92). ## What lands - **[`infra/local/Caddyfile`](infra/local/Caddyfile)** — explicit `route` block: - `GET /` → 302 to `/{locale}/` based on `Accept-Language`, falling back to `/fr/` (APF audience). - `/fr/*` → `dist/.../browser/fr/` with SPA fallback to `fr/index.html`. - `/en/*` → mirror. - Catch-all → 302 to `/fr/`. - **[`infra/local/dev.compose.yml`](infra/local/dev.compose.yml)** — new `serve-static` service on the `serve-static` profile. Bind-mounts the Caddyfile and `dist/apps/portal-shell/browser/` read-only. Port 4200, overridable via `SERVE_STATIC_PORT`. - **[`infra/local/.env.example`](infra/local/.env.example)** — adds `SERVE_STATIC_PORT=4200`. - **[`infra/local/dev.sh`](infra/local/dev.sh)** — registers `serve-static` in `ALL_PROFILES` so `dev.sh down|status|logs` catches the new container, and `dev.sh up serve-static` works. - **[`infra/README.md`](infra/README.md)** — file row, workflow snippet, cheat-sheet row, and a service-endpoint row with the `nx build … -c=production` prerequisite called out. ## Workflow ``` pnpm exec nx build portal-shell --configuration=production ./infra/local/dev.sh up serve-static open http://localhost:4200/ # → /fr/ or /en/ per Accept-Language ``` ## Decision worth flagging Used **Caddy** rather than nginx or Traefik. Reason: minimal Caddyfile, single binary, no daemon config drift, sensible defaults (TLS off explicitly for local-only). Same family of choice as the rest of `infra/local/` — small, single-purpose images. `redir` in a Caddyfile is **ambiguous** when the first arg starts with `/`: Caddy reads it as a path matcher rather than a redirect target. Using `redir * /fr/ 302` (explicit `*` matcher) avoids the gotcha. Documented inline in the Caddyfile via a comment block. ## What this PR explicitly does NOT do - Wire TLS. Local convenience only, binds to `localhost`. - Replace `nx run portal-shell:serve-static` (still used by Lighthouse CI in `ci:perf`). - Set the `__Host-portal_locale` cookie or honour it for the smart redirect. Cookie handling needs the BFF route (ADR-0019 future PR). - Land an on-prem reverse-proxy ADR. The on-prem infra ADR is phase 3b. ## Verified locally | Probe | Expected | Actual | |---|---|---| | `GET / -H 'Accept-Language: fr'` | 302 `/fr/` | ✓ | | `GET / -H 'Accept-Language: en'` | 302 `/en/` | ✓ | | `GET /unknown` | 302 `/fr/` | ✓ | | `GET /fr/deep/route` | 200 (SPA fallback to `fr/index.html`) | ✓ | | `GET /fr/favicons/favicon.svg` | 200 (asset under locale folder) | ✓ | | `/fr/index.html` markup | `lang="fr"`, `<base href="/fr/">` | ✓ | | `/en/index.html` markup | `lang="en"`, `<base href="/en/">` | ✓ | ## Test plan - [x] `docker compose -f infra/local/dev.compose.yml --profile serve-static config` validates clean. - [x] `dev.sh up serve-static` brings the container up; `dev.sh status` lists it; `dev.sh stop serve-static` brings it down. - [x] Routing probes above all pass. - [ ] Manual: build + serve-static + click the locale switcher → URL becomes `/{other-locale}/`, the matching bundle boots, no console errors. (Verifies PR #95 + #96 end-to-end against a prod-faithful proxy.) - [ ] Manual: `/fr/accessibilite` → router-level redirect to `/fr/accessibility` (verifies PR #94 under SPA fallback). - [ ] Manual: `Accept-Language: en` in browser settings → root URL lands on `/en/`. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #97
151 lines
4.2 KiB
Bash
Executable File
151 lines
4.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Convenience wrapper around infra/local/dev.compose.yml. Documented
|
|
# in infra/README.md → "Local-dev stack" → "Convenience script".
|
|
#
|
|
# Hides the Compose-profile gotcha: `docker compose down` only acts
|
|
# on services whose profile is currently active, so anything brought
|
|
# up with `--profile X` keeps running unless the same flag is on
|
|
# `down`. Every teardown / status / log command in this script
|
|
# always includes every profile in scope, so profile-gated services
|
|
# are visible and stoppable.
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
COMPOSE_FILE="${SCRIPT_DIR}/dev.compose.yml"
|
|
|
|
# Profiles defined in dev.compose.yml. Keep in sync if a new profile
|
|
# is added.
|
|
ALL_PROFILES=(dbtools observability serve-static)
|
|
|
|
# Build "--profile p1 --profile p2 …" as separate arguments.
|
|
build_all_profile_flags() {
|
|
local p
|
|
ALL_PROFILE_FLAGS=()
|
|
for p in "${ALL_PROFILES[@]}"; do
|
|
ALL_PROFILE_FLAGS+=(--profile "$p")
|
|
done
|
|
}
|
|
|
|
# Run docker compose with every profile flagged in.
|
|
dc_all() {
|
|
build_all_profile_flags
|
|
docker compose -f "$COMPOSE_FILE" "${ALL_PROFILE_FLAGS[@]}" "$@"
|
|
}
|
|
|
|
# Run docker compose with no profiles (core services only).
|
|
dc_core() {
|
|
docker compose -f "$COMPOSE_FILE" "$@"
|
|
}
|
|
|
|
# Run `up -d` with a custom set of profiles.
|
|
up_with_profiles() {
|
|
local p
|
|
local -a flags=()
|
|
for p in "$@"; do
|
|
flags+=(--profile "$p")
|
|
done
|
|
docker compose -f "$COMPOSE_FILE" "${flags[@]}" up -d
|
|
}
|
|
|
|
usage() {
|
|
cat <<'USAGE'
|
|
Usage: ./infra/local/dev.sh <command> [args]
|
|
|
|
Commands:
|
|
up [target...] Bring up the stack.
|
|
Targets:
|
|
(none) core only (postgres + redis + otel-collector)
|
|
all core + all profiles
|
|
dbtools core + pgweb
|
|
observability core + jaeger
|
|
serve-static core + caddy (production-build reverse proxy)
|
|
Multiple targets allowed (e.g. `up dbtools observability`).
|
|
|
|
down [-v] Tear the stack down. Always runs with every
|
|
profile in scope, so profile-gated services
|
|
(pgweb, jaeger) are caught too. -v also wipes
|
|
the named Docker volumes.
|
|
|
|
stop <service> Stop a single service (containers stay around).
|
|
Use `up <service>` (or restart) to bring it back.
|
|
|
|
restart <service> Restart a single service.
|
|
|
|
status docker compose ps, with every profile in scope.
|
|
|
|
logs [service] Follow logs (one service or all of them).
|
|
|
|
exec <service> <cmd> Run a command inside a running service container.
|
|
|
|
help Show this help.
|
|
|
|
<other> [args...] Anything else is passed through to
|
|
`docker compose -f dev.compose.yml ...` with
|
|
every profile in scope.
|
|
|
|
Examples:
|
|
./infra/local/dev.sh up
|
|
./infra/local/dev.sh up all
|
|
./infra/local/dev.sh up observability
|
|
./infra/local/dev.sh down -v
|
|
./infra/local/dev.sh stop pgweb
|
|
./infra/local/dev.sh logs otel-collector
|
|
./infra/local/dev.sh exec postgres psql -U "$POSTGRES_USER" -d "$POSTGRES_DB"
|
|
USAGE
|
|
}
|
|
|
|
require_args() {
|
|
local verb="$1"
|
|
local n="$2"
|
|
local hint="$3"
|
|
if [[ $# -lt $((3 + n)) ]]; then
|
|
echo "Error: '$verb' requires $hint." >&2
|
|
echo "Try: ./infra/local/dev.sh help" >&2
|
|
exit 64
|
|
fi
|
|
}
|
|
|
|
cmd="${1:-help}"
|
|
[[ $# -gt 0 ]] && shift
|
|
|
|
case "$cmd" in
|
|
up | start)
|
|
if [[ $# -eq 0 ]]; then
|
|
dc_core up -d
|
|
elif [[ "$1" == "all" ]]; then
|
|
dc_all up -d
|
|
else
|
|
up_with_profiles "$@"
|
|
fi
|
|
;;
|
|
down)
|
|
dc_all down "$@"
|
|
;;
|
|
stop)
|
|
require_args stop 1 "a service name (use 'down' to tear down the whole stack)" "$@"
|
|
dc_all stop "$@"
|
|
;;
|
|
restart)
|
|
require_args restart 1 "a service name" "$@"
|
|
dc_all restart "$@"
|
|
;;
|
|
status | ps)
|
|
dc_all ps "$@"
|
|
;;
|
|
logs)
|
|
dc_all logs -f "$@"
|
|
;;
|
|
exec)
|
|
require_args exec 2 "a service name and a command" "$@"
|
|
dc_all exec "$@"
|
|
;;
|
|
help | -h | --help | "")
|
|
usage
|
|
;;
|
|
*)
|
|
# Pass-through to docker compose, with profiles in scope.
|
|
dc_all "$cmd" "$@"
|
|
;;
|
|
esac
|