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:
commit
236b43edce
38
AGENTS.md
38
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.
|
||||
|
|
|
|||
|
|
@ -313,6 +313,7 @@ PLATFORMS
|
|||
arm64-darwin-22
|
||||
arm64-darwin-23
|
||||
arm64-darwin-24
|
||||
arm64-darwin-25
|
||||
x86_64-linux
|
||||
|
||||
DEPENDENCIES
|
||||
|
|
|
|||
78
tasks.py
78
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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue