diff --git a/AGENTS.md b/AGENTS.md index 986bad6..aa27d02 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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. +## 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: - `inv db-migrate` @@ -22,6 +50,16 @@ Examples: - `inv docker-test-shard --node-index=3 --node-total=8` - `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. 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. diff --git a/Gemfile.lock b/Gemfile.lock index 954a4c4..15088d9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -313,6 +313,7 @@ PLATFORMS arm64-darwin-22 arm64-darwin-23 arm64-darwin-24 + arm64-darwin-25 x86_64-linux DEPENDENCIES diff --git a/tasks.py b/tasks.py index e038edb..4f43797 100644 --- a/tasks.py +++ b/tasks.py @@ -35,6 +35,7 @@ BLACKBOX_MAILGUN_API_KEY = "blackbox-test-api-key" BLACKBOX_MAILGUN_DOMAIN = "blackbox.example.com" SPEC_ROOT = Path("spec") 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): @@ -43,6 +44,18 @@ def _env(**overrides): 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): deadline = time.time() + timeout healthz_url = f"{base_url}/healthz" @@ -128,7 +141,7 @@ def _print_header(title): def _bootstrap_user_command(email, password, username): return ( - "bundle exec rails runner " + f"{_bundle_exec('rails', 'runner')} " f"\"user = User.find_or_initialize_by(email: '{email}'); " f"user.username = '{username}'; " f"user.password = '{password}'; " @@ -266,7 +279,7 @@ def _start_server(command, env, log_path="/tmp/turniere-e2e-server.log"): def _start_test_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(), ) @@ -282,32 +295,52 @@ def _stop_process(process, log_handle): @task def setup(c): - """Install gem dependencies locally.""" - c.run("bundle install", pty=True) + """Install the pinned Bundler and local backend gem dependencies.""" + 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") -def db_migrate(c): +def db_migrate(c, env=None): """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 -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.""" - 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") def server_test(c, host=SERVER_HOST, port=SERVER_PORT): """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 def test(c): """Run the normal RSpec suite.""" - c.run("bundle exec rspec", pty=True) + c.run(_bundle_exec("rspec"), pty=True) @task(name="test-shard") @@ -340,7 +373,7 @@ def test_shard(c, node_index=1, node_total=8, dry_run=False): return 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, ) @@ -348,14 +381,31 @@ def test_shard(c, node_index=1, node_total=8, dry_run=False): @task def lint(c): """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") 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.""" 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.username = '{username}'; " f"user.password = '{password}'; " @@ -378,7 +428,7 @@ def test_http_e2e( ): """Run HTTP E2E specs against an already running backend.""" c.run( - "bundle exec rspec spec/e2e/http", + f"{_bundle_exec('rspec')} spec/e2e/http", env=_env( TURNIERE_E2E_BASE_URL=base_url, TURNIERE_E2E_EMAIL=email,