Run seeds only once (#475)
## 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:
parent
c381b86b5e
commit
f8a3cc4c47
6 changed files with 78 additions and 37 deletions
|
|
@ -14,6 +14,7 @@ ASSOCIATION_NAME="Sportsclub XYZ"
|
||||||
# Optional: Admin user (created/updated on container start via Release.seed_admin)
|
# Optional: Admin user (created/updated on container start via Release.seed_admin)
|
||||||
# In production, set these so the first admin can log in. Change password without redeploy:
|
# In production, set these so the first admin can log in. Change password without redeploy:
|
||||||
# bin/mv eval "Mv.Release.seed_admin()" (with new ADMIN_PASSWORD or ADMIN_PASSWORD_FILE)
|
# bin/mv eval "Mv.Release.seed_admin()" (with new ADMIN_PASSWORD or ADMIN_PASSWORD_FILE)
|
||||||
|
# FORCE_SEEDS=true re-runs bootstrap seeds even when admin user exists (e.g. after changing roles/custom fields).
|
||||||
# ADMIN_EMAIL=admin@example.com
|
# ADMIN_EMAIL=admin@example.com
|
||||||
# ADMIN_PASSWORD=secure-password
|
# ADMIN_PASSWORD=secure-password
|
||||||
# ADMIN_PASSWORD_FILE=/run/secrets/admin_password
|
# ADMIN_PASSWORD_FILE=/run/secrets/admin_password
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,15 @@ All notable changes to this project will be documented in this file.
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## [Unreleased]
|
## [1.1.1] - 2026-03-16
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
- **FORCE_SEEDS** – Environment variable. When set to `"true"`, bootstrap (and optionally dev) seeds are run even when the admin user already exists, so you can re-apply changed seed data (e.g. new roles or custom fields) without deleting the admin user.
|
||||||
- **Improved OIDC-only mode** – Admin can enable “Only OIDC sign-in” in settings; when enabled, direct registration is disabled and sign-in page redirects to OIDC when configured.
|
- **Improved OIDC-only mode** – Admin can enable “Only OIDC sign-in” in settings; when enabled, direct registration is disabled and sign-in page redirects to OIDC when configured.
|
||||||
- **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. Set `FORCE_SEEDS=true` to override and re-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 (unless `FORCE_SEEDS=true`); 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). Set `FORCE_SEEDS=true` to re-run seeds even when already applied.
|
||||||
|
|
||||||
### 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 unless `FORCE_SEEDS=true`; 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)
|
||||||
|
|
@ -10,13 +10,14 @@
|
||||||
### Environment Variables
|
### Environment Variables
|
||||||
|
|
||||||
- `RUN_DEV_SEEDS` – If set to `"true"`, `run_seeds/0` also runs dev seeds (members, groups, sample data). Otherwise only bootstrap seeds run.
|
- `RUN_DEV_SEEDS` – If set to `"true"`, `run_seeds/0` also runs dev seeds (members, groups, sample data). Otherwise only bootstrap seeds run.
|
||||||
|
- `FORCE_SEEDS` – If set to `"true"`, seeds are run even when the admin user already exists (e.g. after changing bootstrap data such as roles or custom fields). Otherwise seeds are skipped when bootstrap was already applied.
|
||||||
- `ADMIN_EMAIL` – Email of the admin user to create/update. If unset, seed_admin/0 does nothing.
|
- `ADMIN_EMAIL` – Email of the admin user to create/update. If unset, seed_admin/0 does nothing.
|
||||||
- `ADMIN_PASSWORD` – Password for the admin user. If unset (and no file), no new user is created; if a user with ADMIN_EMAIL already exists (e.g. OIDC-only), their role is set to Admin (no password change).
|
- `ADMIN_PASSWORD` – Password for the admin user. If unset (and no file), no new user is created; if a user with ADMIN_EMAIL already exists (e.g. OIDC-only), their role is set to Admin (no password change).
|
||||||
- `ADMIN_PASSWORD_FILE` – Path to a file containing the password (e.g. Docker secret).
|
- `ADMIN_PASSWORD_FILE` – Path to a file containing the password (e.g. Docker secret).
|
||||||
|
|
||||||
### 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 unless `FORCE_SEEDS=true`; 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). 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
|
- `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.
|
||||||
|
|
@ -19,6 +19,7 @@ defmodule Mv.Release do
|
||||||
alias Mv.Authorization.Role
|
alias Mv.Authorization.Role
|
||||||
|
|
||||||
require Ash.Query
|
require Ash.Query
|
||||||
|
require Logger
|
||||||
|
|
||||||
def migrate do
|
def migrate do
|
||||||
load_app()
|
load_app()
|
||||||
|
|
@ -28,13 +29,37 @@ 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
|
||||||
|
e ->
|
||||||
|
Logger.warning("Could not check seed status (#{inspect(e)}), assuming not applied.")
|
||||||
|
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 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).
|
- 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,6 +67,9 @@ 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
|
||||||
|
|
||||||
|
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)
|
priv = :code.priv_dir(@app)
|
||||||
bootstrap_path = Path.join(priv, "repo/seeds_bootstrap.exs")
|
bootstrap_path = Path.join(priv, "repo/seeds_bootstrap.exs")
|
||||||
dev_path = Path.join(priv, "repo/seeds_dev.exs")
|
dev_path = Path.join(priv, "repo/seeds_dev.exs")
|
||||||
|
|
@ -61,6 +89,7 @@ defmodule Mv.Release do
|
||||||
Code.compiler_options(prev)
|
Code.compiler_options(prev)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def rollback(repo, version) do
|
def rollback(repo, version) do
|
||||||
load_app()
|
load_app()
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,9 @@
|
||||||
# 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 user exists), so safe to run on every start. Set FORCE_SEEDS=true to
|
||||||
|
# re-run seeds even when already applied.
|
||||||
#
|
#
|
||||||
# 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,6 +14,11 @@
|
||||||
# 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.
|
||||||
|
|
||||||
|
_ = Application.ensure_all_started(:mv)
|
||||||
|
|
||||||
|
if Mv.Release.bootstrap_seeds_applied?() and System.get_env("FORCE_SEEDS") != "true" do
|
||||||
|
IO.puts("Seeds already applied. Skipping. (Set FORCE_SEEDS=true to override)")
|
||||||
|
else
|
||||||
prev = Code.compiler_options()
|
prev = Code.compiler_options()
|
||||||
Code.compiler_options(ignore_module_conflict: true)
|
Code.compiler_options(ignore_module_conflict: true)
|
||||||
|
|
||||||
|
|
@ -28,3 +35,4 @@ try do
|
||||||
after
|
after
|
||||||
Code.compiler_options(prev)
|
Code.compiler_options(prev)
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue