Run seeds only once (#475)
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/promote/production Build is passing
continuous-integration/drone/tag Build is passing

## Description of the implemented changes
The changes were:
- [ ] Bugfixing
- [x] New Feature
- [ ] Breaking Change
- [x] Refactoring

**Seeds run only on first startup.** On every application start (e.g. `just run`, Docker entrypoint), seed scripts are still invoked, but they exit immediately when the admin user already exists. This avoids duplicate seed data (e.g. join requests), keeps startup fast after the first run, and works the same in dev and production.

## What has been changed?

- **`lib/mv/release.ex`**
  - Added `bootstrap_seeds_applied?/0`: returns whether the admin user (from `ADMIN_EMAIL` or default `admin@localhost`) exists. We check the admin *user*, not the Admin *role*, so we do not skip when only migrations have run (migrations can create the Admin role for the system actor).
  - `run_seeds/0`: if `bootstrap_seeds_applied?()` is true, prints “Seeds already applied (admin user exists). Skipping.” and returns without running bootstrap or dev seeds; otherwise unchanged behaviour.
  - Module docs updated for the new function and the skip behaviour.

- **`priv/repo/seeds.exs`**
  - Ensures the app is started (`Application.ensure_all_started(:mv)`).
  - If `Mv.Release.bootstrap_seeds_applied?()` is true, prints the same skip message and does not run bootstrap or dev seeds; otherwise runs as before (bootstrap + dev seeds in dev/test).
  - Comment at the top updated to describe the skip behaviour.

- **Documentation**
  - `CODE_GUIDELINES.md` §1.2.1: seeds run on every start but exit early when already applied; mentions `bootstrap_seeds_applied?/0`.
  - `docs/admin-bootstrap-and-oidc-role-sync.md`: run_seeds skips when admin user exists; description of `run_seeds/0` updated.
  - `CHANGELOG.md` [Unreleased]: new “Seeds run only when needed” entry under Changed.

## Definition of Done
### Code Quality
- [x] No new technical depths
- [x] Linting passed
- [x] Documentation is added where needed

### Accessibility
- [x] New elements are properly defined with html-tags *(no new UI)*
- [x] Colour contrast follows WCAG criteria *(no new UI)*
- [x] Aria labels are added when needed *(no new UI)*
- [x] Everything is accessible by keyboard *(no new UI)*
- [x] Tab-Order is comprehensible *(no new UI)*
- [x] All interactive elements have a visible focus *(no new UI)*

### Testing
- [x] Tests for new code are written *(existing seeds and release tests cover behaviour; idempotency test still passes when second run skips)*
- [x] All tests pass
- [x] axe-core dev tools show no critical or major issues *(no UI changes)*

## Additional Notes

- **Review focus:** Logic in `Mv.Release` and `priv/repo/seeds.exs`; the “already applied” check is a single DB read for the admin user. On failure (e.g. DB down), `bootstrap_seeds_applied?/0` returns `false`, so seeds run (safe for first deploy).
- **Suggested check:** Run `mix test test/seeds_test.exs test/mv/release_test.exs` to confirm seeds and release behaviour.

Reviewed-on: #475
Co-authored-by: Simon <s.thiessen@local-it.org>
Co-committed-by: Simon <s.thiessen@local-it.org>
This commit is contained in:
Simon 2026-03-16 19:27:31 +01:00 committed by simon
parent c381b86b5e
commit f8a3cc4c47
6 changed files with 78 additions and 37 deletions

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). Set `FORCE_SEEDS=true` to re-run seeds even when already applied. 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.
@ -19,6 +19,7 @@ defmodule Mv.Release do
alias Mv.Authorization.Role
require Ash.Query
require Logger
def migrate do
load_app()
@ -28,13 +29,37 @@ 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
e ->
Logger.warning("Could not check seed status (#{inspect(e)}), assuming not applied.")
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 user exists); set `FORCE_SEEDS=true` to override and re-run.
- 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,23 +67,27 @@ defmodule Mv.Release do
{:error, {app, reason}} -> raise "Failed to start #{inspect(app)}: #{inspect(reason)}"
end
priv = :code.priv_dir(@app)
bootstrap_path = Path.join(priv, "repo/seeds_bootstrap.exs")
dev_path = Path.join(priv, "repo/seeds_dev.exs")
if bootstrap_seeds_applied?() and System.get_env("FORCE_SEEDS") != "true" do
IO.puts("Seeds already applied. Skipping. (Set FORCE_SEEDS=true to override)")
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()
Code.compiler_options(ignore_module_conflict: true)
prev = Code.compiler_options()
Code.compiler_options(ignore_module_conflict: true)
try do
Code.eval_file(bootstrap_path)
IO.puts("✅ Bootstrap seeds completed.")
try do
Code.eval_file(bootstrap_path)
IO.puts("✅ Bootstrap seeds completed.")
if System.get_env("RUN_DEV_SEEDS") == "true" do
Code.eval_file(dev_path)
IO.puts("✅ Dev seeds completed.")
if System.get_env("RUN_DEV_SEEDS") == "true" do
Code.eval_file(dev_path)
IO.puts("✅ Dev seeds completed.")
end
after
Code.compiler_options(prev)
end
after
Code.compiler_options(prev)
end
end