Run seeds only once #475
5 changed files with 70 additions and 36 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue