@@ -222,20 +222,19 @@ jobs:
222222 - name : Build the documentation
223223 run : mix docs --warnings-as-errors
224224
225- integration-test-elixir :
226- name : integration test (OTP ${{matrix.otp}} | Elixir ${{matrix.elixir}} | partition ${{matrix.partition}})
225+ # The generator, compile and formatter tests never talk to a database server,
226+ # and the sqlite3 tests use a file-backed database. Together that is the
227+ # largest slice of the suite, and it runs with no service containers at all --
228+ # nothing to pull, nothing to wait for.
229+ integration-test-no-db :
230+ name : integration test (OTP ${{matrix.otp}} | Elixir ${{matrix.elixir}} | no db)
227231
228232 runs-on : ubuntu-24.04
229233 timeout-minutes : 30
230234
231235 strategy :
232- # Keep running the remaining partitions when one fails, otherwise a single
233- # failing partition hides the results of all the others.
234236 fail-fast : false
235237 matrix :
236- # Test files are split across Elixir instances via `mix test --partitions`.
237- # Keep this list in sync with the `--partitions` count in the test step below.
238- partition : [1, 2, 3]
239238 elixir : ["1.18.4", "1.20.4"]
240239 include :
241240 - elixir : " 1.18.4"
@@ -247,7 +246,122 @@ jobs:
247246 env :
248247 ELIXIR_ASSERT_TIMEOUT : 10000
249248 PHX_CI : true
250- MIX_TEST_PARTITION : ${{ matrix.partition }}
249+ PHX_TEST_GROUP : no-db
250+ # Set to half of the runner vCPU cores to compile deps/NIFs
251+ # concurrently across OS processes without CPU oversubscription.
252+ # See https://docs.github.com/en/actions/reference/runners/github-hosted-runners#standard-github-hosted-runners-for-public-repositories
253+ # and https://elixir-lang.org/blog/2025/10/16/elixir-v1-19-0-released/
254+ MIX_OS_DEPS_COMPILE_PARTITION_COUNT : 2
255+ MAKEFLAGS : " -j2"
256+
257+ steps :
258+ - name : Checkout
259+ uses : actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
260+ with :
261+ persist-credentials : false
262+
263+ - name : Mount installer/tmp on tmpfs
264+ run : |
265+ mkdir -p installer/tmp
266+ sudo mount -t tmpfs -o size=4G,uid=$(id -u),gid=$(id -g) tmpfs installer/tmp
267+
268+ - name : Set up Elixir
269+ uses : erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1
270+ with :
271+ elixir-version : ${{ matrix.elixir }}
272+ otp-version : ${{ matrix.otp }}
273+
274+ - name : Restore deps and _build cache
275+ uses : actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
276+ with :
277+ path : |
278+ integration_test/deps
279+ integration_test/_build
280+ key : integration-deps-${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-${{ hashFiles('integration_test/mix.lock') }}
281+ restore-keys : |
282+ integration-deps-${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}
283+
284+ - name : Fetch dependencies
285+ working-directory : integration_test
286+ run : mix deps.get
287+
288+ - name : Compile dependencies
289+ working-directory : integration_test
290+ run : mix deps.compile
291+ env :
292+ MIX_ENV : test
293+
294+ - name : Run integration tests
295+ working-directory : integration_test
296+ run : |
297+ mix test --include database:sqlite3 \
298+ --formatter ExUnit.CLIFormatter \
299+ --formatter Phoenix.Integration.SummaryFormatter
300+
301+ # One job per database server, so each pulls exactly one image instead of all
302+ # three. `--only database:<name>` runs that server's tests and nothing else;
303+ # together with the `no-db` job above, every test runs exactly once.
304+ integration-test-db :
305+ name : integration test (OTP ${{matrix.otp}} | Elixir ${{matrix.elixir}} | ${{matrix.db.name}})
306+
307+ runs-on : ubuntu-24.04
308+ timeout-minutes : 30
309+
310+ strategy :
311+ fail-fast : false
312+ matrix :
313+ db :
314+ - name : postgresql
315+ image : postgres:18
316+ port : 5432
317+ # Probe over TCP (-h 127.0.0.1) rather than the Unix socket: initdb
318+ # runs a temporary server with `listen_addresses` empty, and a
319+ # socket probe reports ready against *that* server, before the real
320+ # one is listening.
321+ health_cmd : pg_isready -U postgres -h 127.0.0.1
322+ health_retries : 10
323+ health_start_period : 30s
324+
325+ - name : mysql
326+ image : mysql:26
327+ port : 3306
328+ # Two things to be aware of:
329+ #
330+ # 1. Initializing the data directory on first boot takes 30s+ on a
331+ # slow runner, which overruns the retry budget. Probes that fail
332+ # during --health-start-period do not consume that budget, and
333+ # the first successful probe ends the period early.
334+ # 2. The entrypoint starts a temporary server during initialization
335+ # with `port: 0` (Unix socket only). `mysqladmin ping -h
336+ # localhost` uses the socket, so it reports healthy against that
337+ # temporary server -- which is then shut down and restarted.
338+ # Probing over TCP only succeeds once the real server is up.
339+ health_cmd : mysqladmin ping -h 127.0.0.1 --protocol=TCP
340+ health_retries : 10
341+ health_start_period : 60s
342+
343+ - name : mssql
344+ image : mcr.microsoft.com/mssql/server:2019-latest
345+ port : 1433
346+ # Block scalar so the \" escapes reach docker verbatim, exactly as
347+ # they did when this was written inline.
348+ health_cmd : >-
349+ /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P \"$SA_PASSWORD\" -C -Q 'SELECT 1' 2>/dev/null || /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P \"$SA_PASSWORD\" -Q 'SELECT 1' 2>/dev/null
350+ health_retries : 25
351+ health_start_period : 30s
352+
353+ elixir : ["1.18.4", "1.20.4"]
354+ include :
355+ - elixir : " 1.18.4"
356+ otp : " 27.3.4.3"
357+
358+ - elixir : " 1.20.4"
359+ otp : " 29.0.5"
360+
361+ env :
362+ ELIXIR_ASSERT_TIMEOUT : 10000
363+ PHX_CI : true
364+ PHX_TEST_GROUP : ${{ matrix.db.name }}
251365 # Set to half of the runner vCPU cores to compile deps/NIFs
252366 # concurrently across OS processes without CPU oversubscription.
253367 # See https://docs.github.com/en/actions/reference/runners/github-hosted-runners#standard-github-hosted-runners-for-public-repositories
@@ -256,56 +370,27 @@ jobs:
256370 MAKEFLAGS : " -j2"
257371
258372 services :
259- postgres :
260- image : postgres:18
373+ # A single, matrix-driven service: the job needs exactly one database, so
374+ # only one image is ever pulled. The service name does not matter -- the
375+ # job runs on the host, so tests reach the server through the published
376+ # port on localhost rather than a container network alias.
377+ db :
378+ image : ${{ matrix.db.image }}
261379 ports :
262- - 5432:5432
380+ - ${{ matrix.db.port }}:${{ matrix.db.port }}
381+ # `env:` keys cannot be matrix-driven, so all three images' variables are
382+ # set unconditionally. Each image ignores the ones meant for the others.
263383 env :
264384 POSTGRES_PASSWORD : postgres
265- # Probe over TCP (-h 127.0.0.1) rather than the Unix socket: initdb runs
266- # a temporary server with `listen_addresses` empty, and a socket probe
267- # reports ready against *that* server, before the real one is listening.
268- options : >-
269- --health-cmd "pg_isready -U postgres -h 127.0.0.1"
270- --health-interval 2s
271- --health-timeout 3s
272- --health-retries 10
273- --health-start-period 30s
274- mysql :
275- image : mysql:26
276- ports :
277- - 3306:3306
278- env :
279385 MYSQL_ALLOW_EMPTY_PASSWORD : " yes"
280- # Things to be aware of:
281- #
282- # 1. Initializing the data directory on first boot takes 30s+ on a slow
283- # runner, which overruns the retry budget below. Probes that fail
284- # during --health-start-period do not consume that budget, and the
285- # first successful probe ends the period early.
286- # 2. The entrypoint starts a temporary server during initialization with
287- # `port: 0` (Unix socket only). `mysqladmin ping -h localhost` uses
288- # the socket, so it reports healthy against that temporary server --
289- # which is then shut down and restarted. Probing over TCP instead
290- # only succeeds once the real server is listening on 3306.
291- options : >-
292- --health-cmd "mysqladmin ping -h 127.0.0.1 --protocol=TCP"
293- --health-interval 2s
294- --health-timeout 3s
295- --health-retries 10
296- --health-start-period 60s
297- mssql :
298- image : mcr.microsoft.com/mssql/server:2019-latest
299- env :
300386 ACCEPT_EULA : Y
301387 SA_PASSWORD : some!Password
302- ports :
303- - 1433:1433
304388 options : >-
305- --health-cmd "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P \"$SA_PASSWORD\" -C -Q 'SELECT 1' 2>/dev/null || /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P \"$SA_PASSWORD\" -Q 'SELECT 1' 2>/dev/null "
389+ --health-cmd "${{ matrix.db.health_cmd }} "
306390 --health-interval 2s
307391 --health-timeout 3s
308- --health-retries 25
392+ --health-retries ${{ matrix.db.health_retries }}
393+ --health-start-period ${{ matrix.db.health_start_period }}
309394
310395 steps :
311396 - name : Checkout
@@ -347,6 +432,6 @@ jobs:
347432 - name : Run integration tests
348433 working-directory : integration_test
349434 run : |
350- mix test --include database --partitions 3 \
435+ mix test --only database:${{ matrix.db.name }} \
351436 --formatter ExUnit.CLIFormatter \
352437 --formatter Phoenix.Integration.SummaryFormatter
0 commit comments