Run seeds only once #475

Merged
simon merged 6 commits from bugfix/rund-seeds-just-once into main 2026-03-16 19:27:33 +01:00
5 changed files with 70 additions and 36 deletions
Showing only changes of commit c40f3135a1 - Show all commits

View file

@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Success toast auto-dismiss** Success flash messages (e.g. “Settings saved”) hide automatically after 5 seconds instead of requiring the user to close them.
### Changed
- **Seeds run only when needed** Bootstrap and dev seeds are skipped on application start when the admin user already exists (`Mv.Release.bootstrap_seeds_applied?/0`). This avoids duplicate data and speeds up startup in dev and production after the first run.
- **Unauthenticated access** Users who are not logged in are redirected to sign-in without showing a “no permission” message; the message is only shown to logged-in users who lack access.
### Fixed

View file

@ -284,13 +284,13 @@ end
### 1.2.1 Database Seeds
Seeds are split into **bootstrap** and **dev**:
Seeds are split into **bootstrap** and **dev**. They run on every start (e.g. `just run`, Docker entrypoint) but **exit early** if already applied so startup stays fast and no duplicate data is created.
- **`priv/repo/seeds.exs`** Entrypoint. Runs `seeds_bootstrap.exs` always; runs `seeds_dev.exs` only when `Mix.env()` is `:dev` or `:test`.
- **`priv/repo/seeds.exs`** Entrypoint. If the admin user (ADMIN_EMAIL or default) already exists, skips entirely; otherwise runs `seeds_bootstrap.exs` and, in dev/test, `seeds_dev.exs`.
- **`priv/repo/seeds_bootstrap.exs`** Creates only data required for system startup: membership fee types, custom fields, roles, admin user, system user, global settings (including default membership fee type). No members, no groups. Used in all environments (dev, test, prod).
- **`priv/repo/seeds_dev.exs`** Creates 20 sample members, groups, and optional custom field values. Run only in dev and test.
In production, running `mix run priv/repo/seeds.exs` executes only the bootstrap part (no dev seeds).
In production, running `mix run priv/repo/seeds.exs` (or `Mv.Release.run_seeds/0`) executes only the bootstrap part when not yet applied (no dev seeds unless `RUN_DEV_SEEDS=true`). The “already applied” check uses `Mv.Release.bootstrap_seeds_applied?/0` (admin user exists).
### 1.3 Domain-Driven Design

View file

@ -2,7 +2,7 @@
## Overview
- **Admin bootstrap:** In production, the Docker entrypoint runs migrate, then `Mv.Release.run_seeds/0` (bootstrap seeds; set `RUN_DEV_SEEDS=true` to also run dev seeds), then `seed_admin/0` from ENV, then the server. Password can be changed without redeploy via `bin/mv eval "Mv.Release.seed_admin()"`.
- **Admin bootstrap:** In production, the Docker entrypoint runs migrate, then `Mv.Release.run_seeds/0` (skips if admin user already exists; otherwise runs bootstrap seeds; set `RUN_DEV_SEEDS=true` to also run dev seeds), then `seed_admin/0` from ENV, then the server. Password can be changed without redeploy via `bin/mv eval "Mv.Release.seed_admin()"`.
- **OIDC role sync:** Optional mapping from OIDC groups (e.g. from Authentik profile scope) to the Admin role. Users in the configured admin group get the Admin role on registration and on each sign-in.
## Admin Bootstrap (Part A)
@ -16,7 +16,7 @@
### Release Tasks
- `Mv.Release.run_seeds/0` Runs bootstrap seeds (fee types, custom fields, roles, settings). If `RUN_DEV_SEEDS` env is `"true"`, also runs dev seeds (members, groups, sample data). Idempotent.
- `Mv.Release.run_seeds/0` If the admin user already exists (bootstrap already applied), skips; otherwise runs bootstrap seeds (fee types, custom fields, roles, settings). If `RUN_DEV_SEEDS` env is `"true"`, also runs dev seeds (members, groups, sample data). Safe to call on every start.
- `Mv.Release.seed_admin/0` Reads ADMIN_EMAIL and password from ADMIN_PASSWORD or ADMIN_PASSWORD_FILE. If both email and password are set: creates or updates the user with the Admin role. If only ADMIN_EMAIL is set: sets the Admin role on an existing user with that email (for OIDC-only admins); does not create a user. Idempotent.
### Entrypoint

View file

@ -6,8 +6,8 @@ defmodule Mv.Release do
## Tasks
- `migrate/0` - Runs all pending Ecto migrations.
- `run_seeds/0` - Runs bootstrap seeds (fee types, custom fields, roles, settings).
In production, set `RUN_DEV_SEEDS=true` to also run dev seeds (members, groups, sample data).
- `bootstrap_seeds_applied?/0` - Returns whether bootstrap was already applied (admin user exists). Used to skip re-running seeds.
- `run_seeds/0` - If bootstrap already applied, skips; otherwise runs bootstrap seeds (fee types, custom fields, roles, settings). In production, set `RUN_DEV_SEEDS=true` to also run dev seeds (members, groups, sample data).
- `seed_admin/0` - Ensures an admin user exists from ENV (ADMIN_EMAIL, ADMIN_PASSWORD
or ADMIN_PASSWORD_FILE). Idempotent; can be run on every deployment or via shell
to update the admin password without redeploying.
@ -28,13 +28,35 @@ defmodule Mv.Release do
end
end
@doc """
Returns whether bootstrap seeds have already been applied (admin user exists).
We check for the admin user (from ADMIN_EMAIL or default), not the Admin role,
because migrations may create the Admin role for the system actor. Only seeds
create the admin (login) user. Used to skip re-running seeds on subsequent starts.
Call only when the application is already started.
"""
def bootstrap_seeds_applied? do
admin_email = get_env("ADMIN_EMAIL", "admin@localhost")
case User
|> Ash.Query.filter(email == ^admin_email)
|> Ash.read_one(authorize?: false, domain: Mv.Accounts) do
{:ok, %User{}} -> true
_ -> false
end
rescue
_ -> false
end
@doc """
Runs seed scripts so the database has required bootstrap data (and optionally dev data).
- Always runs bootstrap seeds (fee types, custom fields, roles, system user, settings).
- If `RUN_DEV_SEEDS` env is set to `"true"`, also runs dev seeds (members, groups, sample data).
- Skips if bootstrap was already applied (Admin role exists); otherwise runs bootstrap seeds.
- If `RUN_DEV_SEEDS` env is set to `"true"`, also runs dev seeds (members, groups, sample data)
when bootstrap is run.
Uses paths from the application's priv dir so it works in releases (no Mix). Idempotent.
Uses paths from the application's priv dir so it works in releases (no Mix).
"""
def run_seeds do
case Application.ensure_all_started(@app) do
@ -42,6 +64,9 @@ defmodule Mv.Release do
{:error, {app, reason}} -> raise "Failed to start #{inspect(app)}: #{inspect(reason)}"
end
if bootstrap_seeds_applied?() do
IO.puts("Seeds already applied (admin user exists). Skipping.")
else
priv = :code.priv_dir(@app)
bootstrap_path = Path.join(priv, "repo/seeds_bootstrap.exs")
dev_path = Path.join(priv, "repo/seeds_dev.exs")
@ -61,6 +86,7 @@ defmodule Mv.Release do
Code.compiler_options(prev)
end
end
end
def rollback(repo, version) do
load_app()

View file

@ -3,7 +3,8 @@
# mix run priv/repo/seeds.exs
#
# Bootstrap runs in all environments. Dev seeds (members, groups, sample data)
# run only in dev and test.
# run only in dev and test. Skips entirely if bootstrap was already applied
# (Admin role exists), so safe to run on every start.
#
# In production (release): seeds are run via Mv.Release.run_seeds/0 from the
# container entrypoint. Set RUN_DEV_SEEDS=true to also run dev seeds there.
@ -12,10 +13,15 @@
# so that eval_file of bootstrap/dev does not emit "redefining module" warnings;
# it is always restored in `after` to avoid hiding real conflicts elsewhere.
prev = Code.compiler_options()
Code.compiler_options(ignore_module_conflict: true)
_ = Application.ensure_all_started(:mv)
try do
if Mv.Release.bootstrap_seeds_applied?() do
IO.puts("Seeds already applied (admin user exists). Skipping.")
else
prev = Code.compiler_options()
Code.compiler_options(ignore_module_conflict: true)
try do
# Always run bootstrap (fee types, custom fields, roles, admin, system user, settings)
Code.eval_file("priv/repo/seeds_bootstrap.exs")
@ -25,6 +31,7 @@ try do
end
IO.puts("✅ All seeds completed.")
after
after
Code.compiler_options(prev)
end
end