From d02bf63f8581206931a43dc7c29f650a2dc8e476 Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Mon, 13 Jul 2026 21:02:43 +1000 Subject: [PATCH] ci(integration): serialize live-DB jobs by compose to stop host-port flakes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Supabase and Drizzle EQL v3 integration jobs bring up docker-compose stacks that bind fixed host ports (55430 / 55432 / 55433) on shared Blacksmith runners, with no concurrency guard. Two jobs binding the same port on one host fail at `docker compose up` with "address already in use" — a transient flake unrelated to the code under test (e.g. the Supabase job on #586). Add a job-level concurrency group keyed by the compose variant (`integration-live-db-`, NOT the ref, since two PRs contend for a host port as much as two pushes to one PR). The Drizzle `supabase` leg shares the Supabase workflow's key, so those two queue instead of colliding on 55430/55433; the Drizzle `postgres` leg (55432) keeps its own key and still runs in parallel. `cancel-in-progress: false` queues — a live-DB job sharing one ZeroKMS workspace should finish, not be cut off. Also add a pre-`up` `docker compose down -v --remove-orphans || true` to clear any container leaked onto a reused runner by a hard-killed prior run that skipped its teardown. --- .github/workflows/integration-drizzle.yml | 20 ++++++++++++++++++++ .github/workflows/integration-supabase.yml | 20 ++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/.github/workflows/integration-drizzle.yml b/.github/workflows/integration-drizzle.yml index 9e5454a3..0b2a9b83 100644 --- a/.github/workflows/integration-drizzle.yml +++ b/.github/workflows/integration-drizzle.yml @@ -58,6 +58,18 @@ jobs: integration: name: Drizzle v3 integration (db=${{ matrix.db }}) runs-on: blacksmith-4vcpu-ubuntu-2404 + # Serialize every job that brings up the SAME docker-compose stack, so no two + # bind the same fixed host ports on a shared runner at once (the "address + # already in use" flake). Keyed by the compose variant, NOT the ref (two PRs + # contend for a host port just as much as two pushes to one PR), so it + # serializes across refs. The `supabase` leg uses the SAME supabase compose + # (55430 / 55433) as the Supabase workflow and shares its key + # (`integration-live-db-supabase`), so the two workflows queue rather than + # collide; the `postgres` leg (55432) has its own key and still runs in + # parallel with them. + concurrency: + group: integration-live-db-${{ matrix.db }} + cancel-in-progress: false # Fork PRs have no secrets. Skip cleanly rather than fail on something the # contributor cannot fix — `tests.yml` still gives them a green signal. if: ${{ github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository }} @@ -134,6 +146,14 @@ jobs: client-key: ${{ secrets.CS_CLIENT_KEY }} client-access-key: ${{ secrets.CS_CLIENT_ACCESS_KEY }} + # Belt-and-braces for the concurrency guard: a run that was hard-killed + # (runner crash / forced cancel) can skip its `down` and leak a container + # holding the host port onto a REUSED runner. Tear any prior stack down + # before starting a fresh one. `|| true` so a clean runner (nothing to + # remove) is not an error. + - name: Clear any leaked containers from a prior run + run: docker compose -f local/docker-compose.${{ matrix.db }}.yml down -v --remove-orphans || true + - name: Start ${{ matrix.db }} run: docker compose -f local/docker-compose.${{ matrix.db }}.yml up -d --wait diff --git a/.github/workflows/integration-supabase.yml b/.github/workflows/integration-supabase.yml index 953e19ad..64252ff5 100644 --- a/.github/workflows/integration-supabase.yml +++ b/.github/workflows/integration-supabase.yml @@ -50,6 +50,18 @@ jobs: integration: name: Supabase v3 integration (db=${{ matrix.db }}) runs-on: blacksmith-4vcpu-ubuntu-2404 + # Serialize every job that brings up the SAME docker-compose stack, so no two + # bind the same fixed host ports (55430 / 55433) on a shared runner at once — + # the "address already in use" flake. The group is keyed by the compose + # variant, NOT the ref: two different PRs contend for the same host port just + # as much as two pushes to one PR, so it must serialize across refs. It is the + # SAME key the Drizzle workflow's matching leg uses (`integration-live-db-`), + # so those two workflows also queue against each other rather than colliding. + # `cancel-in-progress: false` queues (a live-DB job that shares one ZeroKMS + # workspace should finish, not be interrupted mid-run). + concurrency: + group: integration-live-db-${{ matrix.db }} + cancel-in-progress: false # Fork PRs have no secrets. Skip cleanly rather than fail loudly on something # the contributor cannot fix — `tests.yml` still gives them a green signal. if: ${{ github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository }} @@ -106,6 +118,14 @@ jobs: client-key: ${{ secrets.CS_CLIENT_KEY }} client-access-key: ${{ secrets.CS_CLIENT_ACCESS_KEY }} + # Belt-and-braces for the concurrency guard: a run that was hard-killed + # (runner crash / forced cancel) can skip its `down` and leak a container + # holding the host port onto a REUSED runner. Tear any prior stack down + # before starting a fresh one. `|| true` so a clean runner (nothing to + # remove) is not an error. + - name: Clear any leaked containers from a prior run + run: docker compose -f local/docker-compose.${{ matrix.db }}.yml down -v --remove-orphans || true + - name: Start ${{ matrix.db }} run: docker compose -f local/docker-compose.${{ matrix.db }}.yml up -d --wait