Merge pull request 'Fix seeds to run in production' (#462) from fix/seeds into main
All checks were successful
continuous-integration/drone/push Build is passing

Reviewed-on: #462
This commit is contained in:
moritz 2026-03-09 15:25:05 +01:00
commit 085f61d10d
5 changed files with 57 additions and 4 deletions

View file

@ -2,24 +2,26 @@
## Overview
- **Admin bootstrap:** In production, no seeds run. The first admin user is created/updated from environment variables in the Docker entrypoint (after migrate, before 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` (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)
### 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.
- `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_FILE` Path to a file containing the password (e.g. Docker secret).
### Release Task
### 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.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
- rel/overlays/bin/docker-entrypoint.sh After migrate, runs seed_admin(), then starts the server.
- rel/overlays/bin/docker-entrypoint.sh After migrate, runs run_seeds(), then seed_admin(), then starts the server.
### Seeds (Dev/Test)

View file

@ -6,6 +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).
- `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.
@ -26,6 +28,40 @@ defmodule Mv.Release do
end
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).
Uses paths from the application's priv dir so it works in releases (no Mix). Idempotent.
"""
def run_seeds do
case Application.ensure_all_started(@app) do
{:ok, _} -> :ok
{: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")
prev = Code.compiler_options()
Code.compiler_options(ignore_module_conflict: true)
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.")
end
after
Code.compiler_options(prev)
end
end
def rollback(repo, version) do
load_app()
{:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version))

View file

@ -5,6 +5,9 @@
# Bootstrap runs in all environments. Dev seeds (members, groups, sample data)
# run only in dev and test.
#
# 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.
#
# Compiler option ignore_module_conflict is set only during seed evaluation
# 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.

View file

@ -1,6 +1,15 @@
# Bootstrap seeds: run in all environments (dev, test, prod).
# Creates only data required for system startup: fee types, custom fields,
# roles, admin user, system user, global settings. No members, no groups.
#
# Safe to run from release (no Mix): env is taken from MIX_ENV when Mix.env/0 is not available.
mix_env =
try do
Mix.env()
rescue
UndefinedFunctionError -> (System.get_env("MIX_ENV") || "prod") |> String.to_atom()
end
alias Mv.Accounts
alias Mv.Membership
@ -121,7 +130,7 @@ end
admin_email = System.get_env("ADMIN_EMAIL") || "admin@localhost"
System.put_env("ADMIN_EMAIL", admin_email)
if Mix.env() in [:dev, :test] and is_nil(System.get_env("ADMIN_PASSWORD")) and
if mix_env in [:dev, :test] and is_nil(System.get_env("ADMIN_PASSWORD")) and
is_nil(System.get_env("ADMIN_PASSWORD_FILE")) do
System.put_env("ADMIN_PASSWORD", "testpassword")
end

View file

@ -4,6 +4,9 @@ set -e
echo "==> Running database migrations..."
/app/bin/migrate
echo "==> Running seeds (bootstrap; dev if RUN_DEV_SEEDS=true)..."
/app/bin/mv eval "Mv.Release.run_seeds()"
echo "==> Seeding admin user from ENV (ADMIN_EMAIL, ADMIN_PASSWORD)..."
/app/bin/mv eval "Mv.Release.seed_admin()"