Merge branch 'codex/backend-local-invoke-setup' into 'master'

Improve backend local invoke setup

See merge request turniere/turniere-backend!32
This commit is contained in:
Daniel Schädler 2026-04-12 12:09:38 +00:00
commit 236b43edce
3 changed files with 103 additions and 14 deletions

View File

@ -9,6 +9,34 @@ For routine development, verification, Docker, and E2E work, agents should use
Use `tasks.py` and run commands through `inv ...` instead of ad-hoc shell commands for routine work. Use `tasks.py` and run commands through `inv ...` instead of ad-hoc shell commands for routine work.
## Local Setup
Assume Ruby is already installed at the version from `.ruby-version`.
For local development, the preferred path is:
- `inv setup`
- `inv start-local`
`inv setup` installs the Bundler version pinned in `Gemfile.lock`, configures Bundler for local development (`development test`, without `production`), and runs `bundle install`.
`inv start-local` runs the normal local bootstrap flow:
- `inv setup`
- `inv db-migrate`
- optional local user bootstrap via `--bootstrap-user`
- Rails server start on `0.0.0.0:3000`
Useful local commands:
- `inv setup`
- `inv db-migrate`
- `inv server`
- `inv start-local`
- `inv start-local --bootstrap-user`
- `inv bootstrap-dev-user`
- `inv bootstrap-e2e-user`
Examples: Examples:
- `inv db-migrate` - `inv db-migrate`
@ -22,6 +50,16 @@ Examples:
- `inv docker-test-shard --node-index=3 --node-total=8` - `inv docker-test-shard --node-index=3 --node-total=8`
- `inv docker-test-http-e2e` - `inv docker-test-http-e2e`
For local E2E-style startup with a usable login, prefer:
- `inv start-local --bootstrap-user`
Default local bootstrap credentials:
- email: `e2e@example.com`
- password: `password123`
- username: `e2e-user`
The GitLab CI pipeline is expected to use these tasks as well. The GitLab CI pipeline is expected to use these tasks as well.
In CI, blackbox E2E should prefer the documented GitLab service-to-service model with `FF_NETWORK_PER_BUILD: "true"` when the production image needs to talk to a Postgres sidecar. In CI, blackbox E2E should prefer the documented GitLab service-to-service model with `FF_NETWORK_PER_BUILD: "true"` when the production image needs to talk to a Postgres sidecar.
The normal Rails spec suite is sharded through `inv test-shard ...` and should stay aligned with the CI matrix configuration. The normal Rails spec suite is sharded through `inv test-shard ...` and should stay aligned with the CI matrix configuration.

View File

@ -313,6 +313,7 @@ PLATFORMS
arm64-darwin-22 arm64-darwin-22
arm64-darwin-23 arm64-darwin-23
arm64-darwin-24 arm64-darwin-24
arm64-darwin-25
x86_64-linux x86_64-linux
DEPENDENCIES DEPENDENCIES

View File

@ -35,6 +35,7 @@ BLACKBOX_MAILGUN_API_KEY = "blackbox-test-api-key"
BLACKBOX_MAILGUN_DOMAIN = "blackbox.example.com" BLACKBOX_MAILGUN_DOMAIN = "blackbox.example.com"
SPEC_ROOT = Path("spec") SPEC_ROOT = Path("spec")
E2E_SPEC_ROOT = SPEC_ROOT / "e2e" / "http" E2E_SPEC_ROOT = SPEC_ROOT / "e2e" / "http"
BUNDLER_VERSION = Path("Gemfile.lock").read_text(encoding="utf-8").split("BUNDLED WITH", 1)[1].strip().splitlines()[-1].strip()
def _env(**overrides): def _env(**overrides):
@ -43,6 +44,18 @@ def _env(**overrides):
return env return env
def _rails_env(env=None):
return str(env or os.environ.get("RAILS_ENV", "development"))
def _bundle_command(*parts):
return " ".join(["bundle", f"_{BUNDLER_VERSION}_", *parts])
def _bundle_exec(*parts):
return _bundle_command("exec", *parts)
def _wait_for_http(base_url=TEST_BASE_URL, timeout=60): def _wait_for_http(base_url=TEST_BASE_URL, timeout=60):
deadline = time.time() + timeout deadline = time.time() + timeout
healthz_url = f"{base_url}/healthz" healthz_url = f"{base_url}/healthz"
@ -128,7 +141,7 @@ def _print_header(title):
def _bootstrap_user_command(email, password, username): def _bootstrap_user_command(email, password, username):
return ( return (
"bundle exec rails runner " f"{_bundle_exec('rails', 'runner')} "
f"\"user = User.find_or_initialize_by(email: '{email}'); " f"\"user = User.find_or_initialize_by(email: '{email}'); "
f"user.username = '{username}'; " f"user.username = '{username}'; "
f"user.password = '{password}'; " f"user.password = '{password}'; "
@ -266,7 +279,7 @@ def _start_server(command, env, log_path="/tmp/turniere-e2e-server.log"):
def _start_test_server(): def _start_test_server():
return _start_server( return _start_server(
f"bundle exec rails s -e test -b {SERVER_HOST} -p {SERVER_PORT}", f"{_bundle_exec('rails', 's')} -e test -b {SERVER_HOST} -p {SERVER_PORT}",
_env(), _env(),
) )
@ -282,32 +295,52 @@ def _stop_process(process, log_handle):
@task @task
def setup(c): def setup(c):
"""Install gem dependencies locally.""" """Install the pinned Bundler and local backend gem dependencies."""
c.run("bundle install", pty=True) install_bundler = c.run(
f"gem list -i bundler -v {BUNDLER_VERSION}",
warn=True,
hide=True,
)
if not install_bundler.ok:
c.run(f"gem install bundler:{BUNDLER_VERSION}", pty=True)
c.run(f"{_bundle_command('config', 'set', 'without', 'production')}", pty=True)
c.run(f"{_bundle_command('config', 'set', 'with', 'development test')}", pty=True)
c.run(_bundle_command("install"), pty=True)
@task(name="db-migrate") @task(name="db-migrate")
def db_migrate(c): def db_migrate(c, env=None):
"""Run database migrations locally.""" """Run database migrations locally."""
c.run("bundle exec rails db:migrate", pty=True) c.run(f"RAILS_ENV={_rails_env(env)} {_bundle_exec('rails', 'db:migrate')}", pty=True)
@task @task
def server(c, host=SERVER_HOST, port=SERVER_PORT): def server(c, host=SERVER_HOST, port=SERVER_PORT, env=None):
"""Start the backend locally in development.""" """Start the backend locally in development."""
c.run(f"bundle exec rails s -b {host} -p {port}", pty=True) c.run(f"RAILS_ENV={_rails_env(env)} {_bundle_exec('rails', 's')} -b {host} -p {port}", pty=True)
@task(name="start-local")
def start_local(c, host=SERVER_HOST, port=SERVER_PORT, bootstrap_user=False):
"""Install local deps if needed, migrate the dev database, and start the server."""
setup(c)
db_migrate(c, env="development")
if bootstrap_user:
bootstrap_dev_user(c)
server(c, host=host, port=port, env="development")
@task(name="server-test") @task(name="server-test")
def server_test(c, host=SERVER_HOST, port=SERVER_PORT): def server_test(c, host=SERVER_HOST, port=SERVER_PORT):
"""Start the backend locally in test mode.""" """Start the backend locally in test mode."""
c.run(f"bundle exec rails s -e test -b {host} -p {port}", pty=True) server(c, host=host, port=port, env="test")
@task @task
def test(c): def test(c):
"""Run the normal RSpec suite.""" """Run the normal RSpec suite."""
c.run("bundle exec rspec", pty=True) c.run(_bundle_exec("rspec"), pty=True)
@task(name="test-shard") @task(name="test-shard")
@ -340,7 +373,7 @@ def test_shard(c, node_index=1, node_total=8, dry_run=False):
return return
c.run( c.run(
"bundle exec rspec --require ./spec/rails_helper " + " ".join(shlex.quote(path) for path in files), _bundle_exec("rspec") + " --require ./spec/rails_helper " + " ".join(shlex.quote(path) for path in files),
pty=True, pty=True,
) )
@ -348,14 +381,31 @@ def test_shard(c, node_index=1, node_total=8, dry_run=False):
@task @task
def lint(c): def lint(c):
"""Run RuboCop.""" """Run RuboCop."""
c.run("bundle exec rubocop", pty=True) c.run(_bundle_exec("rubocop"), pty=True)
@task(name="bootstrap-dev-user")
def bootstrap_dev_user(c, email=E2E_EMAIL, password=E2E_PASSWORD, username=E2E_USERNAME):
"""Create or update a confirmed local development user."""
command = (
f"RAILS_ENV=development {_bundle_exec('rails', 'runner')} "
f"\"user = User.find_or_initialize_by(email: '{email}'); "
f"user.username = '{username}'; "
f"user.password = '{password}'; "
f"user.password_confirmation = '{password}'; "
"user.confirmed_at = Time.current; "
"user.uid = user.email; "
"user.provider = 'email'; "
"user.save!\""
)
c.run(command, pty=True)
@task(name="bootstrap-e2e-user") @task(name="bootstrap-e2e-user")
def bootstrap_e2e_user(c, email=E2E_EMAIL, password=E2E_PASSWORD, username=E2E_USERNAME): def bootstrap_e2e_user(c, email=E2E_EMAIL, password=E2E_PASSWORD, username=E2E_USERNAME):
"""Create or update the confirmed HTTP E2E user in test env.""" """Create or update the confirmed HTTP E2E user in test env."""
command = ( command = (
"bundle exec rails runner -e test " f"RAILS_ENV=test {_bundle_exec('rails', 'runner')} "
f"\"user = User.find_or_initialize_by(email: '{email}'); " f"\"user = User.find_or_initialize_by(email: '{email}'); "
f"user.username = '{username}'; " f"user.username = '{username}'; "
f"user.password = '{password}'; " f"user.password = '{password}'; "
@ -378,7 +428,7 @@ def test_http_e2e(
): ):
"""Run HTTP E2E specs against an already running backend.""" """Run HTTP E2E specs against an already running backend."""
c.run( c.run(
"bundle exec rspec spec/e2e/http", f"{_bundle_exec('rspec')} spec/e2e/http",
env=_env( env=_env(
TURNIERE_E2E_BASE_URL=base_url, TURNIERE_E2E_BASE_URL=base_url,
TURNIERE_E2E_EMAIL=email, TURNIERE_E2E_EMAIL=email,