diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2a8c0ae4..751eedfd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,17 +11,16 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.15"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v7 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v7 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} allow-prereleases: true cache: pip cache-dependency-path: pyproject.toml - check-latest: true - name: Build extension for --load-extension test run: |- (cd tests && gcc ext.c -fPIC -shared -o ext.so) diff --git a/datasette/cli.py b/datasette/cli.py index 2694c1f6..2259e672 100644 --- a/datasette/cli.py +++ b/datasette/cli.py @@ -670,9 +670,11 @@ def serve( raise click.ClickException("--token can only be used with --get") if get: - # --get means we don't run Uvicorn at all + # Run async soundness checks before startup hooks, since invoke_startup + # now populates internal tables which requires querying each database run_sync(lambda: check_databases(ds)) + # Run the "startup" plugin hooks try: run_sync(ds.invoke_startup) except StartupError as e: @@ -707,7 +709,8 @@ def serve( # on the loop (asyncio.create_task, Lock/Queue/Event objects, ...) is # still alive when the server starts handling requests. async def _serve_async(): - # Populate internal catalog tables before invoke_startup + # Run async soundness checks before startup hooks, since invoke_startup + # now populates internal tables which requires querying each database await check_databases(ds) # Run the full startup sequence (immutable-database table-count diff --git a/tests/test_cli_serve_server.py b/tests/test_cli_serve_server.py index b76180fd..2f113ded 100644 --- a/tests/test_cli_serve_server.py +++ b/tests/test_cli_serve_server.py @@ -127,6 +127,11 @@ def test_startup_error_fails_fast_before_port_binds(serve_with_plugins): A "startup" plugin hook that raises StartupError must fail fast: print the message, exit non-zero, and never accept a connection on the port - the failure must happen before uvicorn.Server binds the socket. + + Note this is a characterization test, not a regression test: it also + passes on unmodified main, where startup already ran ahead of + uvicorn.run(). It earns its keep once startup moves into the ASGI + lifespan, where fail-fast is genuinely at risk. """ proc, port = serve_with_plugins( {"startup_error_plugin": STARTUP_ERROR_PLUGIN}, wait_for_startup=False