feat: only run all seeds on first startup
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/promote/production Build is passing

This commit is contained in:
Simon 2026-03-16 17:52:59 +01:00
parent a049ccb8e3
commit c40f3135a1
Signed by: simon
GPG key ID: 40E7A58C4AA1EDB2
5 changed files with 70 additions and 36 deletions

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. - **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 ### 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. - **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 ### Fixed

View file

@ -284,13 +284,13 @@ end
### 1.2.1 Database Seeds ### 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_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. - **`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 ### 1.3 Domain-Driven Design

View file

@ -2,7 +2,7 @@
## Overview ## 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. - **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) ## Admin Bootstrap (Part A)
@ -16,7 +16,7 @@
### Release Tasks ### 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. - `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 ### Entrypoint

View file

@ -6,8 +6,8 @@ defmodule Mv.Release do
## Tasks ## Tasks
- `migrate/0` - Runs all pending Ecto migrations. - `migrate/0` - Runs all pending Ecto migrations.
- `run_seeds/0` - Runs bootstrap seeds (fee types, custom fields, roles, settings). - `bootstrap_seeds_applied?/0` - Returns whether bootstrap was already applied (admin user exists). Used to skip re-running seeds.
In production, set `RUN_DEV_SEEDS=true` to also run dev seeds (members, groups, sample data). - `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 - `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 or ADMIN_PASSWORD_FILE). Idempotent; can be run on every deployment or via shell
to update the admin password without redeploying. to update the admin password without redeploying.
@ -28,13 +28,35 @@ defmodule Mv.Release do
end end
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 """ @doc """
Runs seed scripts so the database has required bootstrap data (and optionally dev data). 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). - 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). - 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 def run_seeds do
case Application.ensure_all_started(@app) do case Application.ensure_all_started(@app) do
@ -42,23 +64,27 @@ defmodule Mv.Release do
{:error, {app, reason}} -> raise "Failed to start #{inspect(app)}: #{inspect(reason)}" {:error, {app, reason}} -> raise "Failed to start #{inspect(app)}: #{inspect(reason)}"
end end
priv = :code.priv_dir(@app) if bootstrap_seeds_applied?() do
bootstrap_path = Path.join(priv, "repo/seeds_bootstrap.exs") IO.puts("Seeds already applied (admin user exists). Skipping.")
dev_path = Path.join(priv, "repo/seeds_dev.exs") else
priv = :code.priv_dir(@app)
bootstrap_path = Path.join(priv, "repo/seeds_bootstrap.exs")
dev_path = Path.join(priv, "repo/seeds_dev.exs")
prev = Code.compiler_options() prev = Code.compiler_options()
Code.compiler_options(ignore_module_conflict: true) Code.compiler_options(ignore_module_conflict: true)
try do try do
Code.eval_file(bootstrap_path) Code.eval_file(bootstrap_path)
IO.puts("✅ Bootstrap seeds completed.") IO.puts("✅ Bootstrap seeds completed.")
if System.get_env("RUN_DEV_SEEDS") == "true" do if System.get_env("RUN_DEV_SEEDS") == "true" do
Code.eval_file(dev_path) Code.eval_file(dev_path)
IO.puts("✅ Dev seeds completed.") IO.puts("✅ Dev seeds completed.")
end
after
Code.compiler_options(prev)
end end
after
Code.compiler_options(prev)
end end
end end

View file

@ -3,7 +3,8 @@
# mix run priv/repo/seeds.exs # mix run priv/repo/seeds.exs
# #
# Bootstrap runs in all environments. Dev seeds (members, groups, sample data) # 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 # 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. # container entrypoint. Set RUN_DEV_SEEDS=true to also run dev seeds there.
@ -12,19 +13,25 @@
# so that eval_file of bootstrap/dev does not emit "redefining module" warnings; # 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. # it is always restored in `after` to avoid hiding real conflicts elsewhere.
prev = Code.compiler_options() _ = Application.ensure_all_started(:mv)
Code.compiler_options(ignore_module_conflict: true)
try do if Mv.Release.bootstrap_seeds_applied?() do
# Always run bootstrap (fee types, custom fields, roles, admin, system user, settings) IO.puts("Seeds already applied (admin user exists). Skipping.")
Code.eval_file("priv/repo/seeds_bootstrap.exs") else
prev = Code.compiler_options()
Code.compiler_options(ignore_module_conflict: true)
# In dev and test only: run dev seeds (20 members, groups, custom field values) try do
if Mix.env() in [:dev, :test] do # Always run bootstrap (fee types, custom fields, roles, admin, system user, settings)
Code.eval_file("priv/repo/seeds_dev.exs") Code.eval_file("priv/repo/seeds_bootstrap.exs")
# In dev and test only: run dev seeds (20 members, groups, custom field values)
if Mix.env() in [:dev, :test] do
Code.eval_file("priv/repo/seeds_dev.exs")
end
IO.puts("✅ All seeds completed.")
after
Code.compiler_options(prev)
end end
IO.puts("✅ All seeds completed.")
after
Code.compiler_options(prev)
end end