The journeys stack is per-run, and publishes nothing on a runner (#728) #757

Merged
claude-bot merged 1 commit from fix/journeys-concurrent-runs into main 2026-09-22 19:12:30 +00:00
Collaborator

#754's journeys job failed having run no test at all:

dependency failed to start: container questboard-e2e-redis-1 exited (0)

on a backend-only PR, ten seconds after the stack came up. That is not a flake, and it is not that PR's doing.

What was actually happening

The job ran docker compose -p questboard-e2e — hard-coded, and hard-coded again in the teardown step. So two overlapping runs (two open PRs, or a PR and the push to main that merged another) were one Compose project on one runner. The second up adopted the first's containers and restarted them, and the first job, sitting on its own redis healthcheck, watched redis stop cleanly underneath it and reported a dependency failure.

The tell is in the log: every line of that job's compose output is duplicated — two processes narrating the same project.

This gets likelier the more of the rebuild is in flight at once, which is why it turned up now and not during lane 1, when PRs went up roughly one at a time. With lanes 3 to 11 still to go it would have kept costing cycles and, worse, kept looking like a flake — the failure has no test in it to point at.

The fix, which has to be both halves

A unique project name alone would only move the collision to the host ports, because the e2e overlay publishes five of them.

  • COMPOSE_PROJECT_NAME: questboard-e2e-${{ github.run_id }}, used by the run — scripts/journeys.sh already honoured the variable — and by the teardown, which had the old name written out.
  • QB_E2E_NO_PORTS=1 adds a new docker-compose.e2e.ci.yml, which resets the five published ports.

Dropping them costs nothing, and the overlay already said so in its own comments before this PR: "Published on FRONTEND_HTTP_PORT … so a person can watch the stack in a real browser. The suite does not use it", "Inspection only", "The web UI, for a human reading what the stack sent". The suite reaches the stack by service name — playwright.config.js sets baseURL to http://frontend — so on a runner, where nobody is watching, those bindings only stop a second stack from starting.

Local runs are unchanged, deliberately. Those ports are the whole point of KEEP_STACK=1, and the script's header and the e2e README both say not to set the flag by hand.

Checks

sh -n scripts/journeys.sh clean; the workflow parses and still has its ten jobs, with the teardown reading the variable and including the new overlay. The merged Compose config rendered both ways: five published ports without the overlay, none with it, and the same ten services either way.

The real proof is this PR's own journeys job — and, better, the next two runs that overlap.

🤖 Generated with Claude Code

#754's journeys job failed having run **no test at all**: ``` dependency failed to start: container questboard-e2e-redis-1 exited (0) ``` on a backend-only PR, ten seconds after the stack came up. That is not a flake, and it is not that PR's doing. ## What was actually happening The job ran `docker compose -p questboard-e2e` — hard-coded, and hard-coded again in the teardown step. So two overlapping runs (two open PRs, or a PR and the push to `main` that merged another) were **one Compose project on one runner**. The second `up` adopted the first's containers and restarted them, and the first job, sitting on its own redis healthcheck, watched redis stop cleanly underneath it and reported a dependency failure. The tell is in the log: every line of that job's compose output is **duplicated** — two processes narrating the same project. This gets likelier the more of the rebuild is in flight at once, which is why it turned up now and not during lane 1, when PRs went up roughly one at a time. With lanes 3 to 11 still to go it would have kept costing cycles and, worse, kept looking like a flake — the failure has no test in it to point at. ## The fix, which has to be both halves **A unique project name alone would only move the collision to the host ports**, because the e2e overlay publishes five of them. - `COMPOSE_PROJECT_NAME: questboard-e2e-${{ github.run_id }}`, used by the run — `scripts/journeys.sh` already honoured the variable — and by the teardown, which had the old name written out. - `QB_E2E_NO_PORTS=1` adds a new `docker-compose.e2e.ci.yml`, which resets the five published ports. Dropping them costs nothing, and the overlay already said so in its own comments before this PR: *"Published on FRONTEND_HTTP_PORT … so a person can watch the stack in a real browser. **The suite does not use it**"*, *"Inspection only"*, *"The web UI, for a human reading what the stack sent"*. The suite reaches the stack by service name — `playwright.config.js` sets `baseURL` to `http://frontend` — so on a runner, where nobody is watching, those bindings only stop a second stack from starting. **Local runs are unchanged, deliberately.** Those ports are the whole point of `KEEP_STACK=1`, and the script's header and the e2e README both say not to set the flag by hand. ## Checks `sh -n scripts/journeys.sh` clean; the workflow parses and still has its ten jobs, with the teardown reading the variable and including the new overlay. The merged Compose config rendered both ways: **five published ports without the overlay, none with it**, and the same ten services either way. The real proof is this PR's own journeys job — and, better, the next two runs that overlap. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
fix(ci): the journeys stack is per-run, and publishes nothing on a runner (#728)
All checks were successful
CI / Backend lint (ruff) (pull_request) Successful in 56s
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 1m28s
CI / Feedback site tests (pull_request) Successful in 55s
CI / Bot/backend version sync (pull_request) Successful in 36s
CI / Docker image build (pull_request) Successful in 45s
CI / Bot tests and audit (pull_request) Successful in 2m48s
CI / Frontend tests, audit, and build (pull_request) Successful in 3m27s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Successful in 3m38s
CI / Backend migration, tests, and audit (pull_request) Successful in 13m44s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Successful in 18m37s
820089658a
#754's journeys job failed having run no test at all:

    dependency failed to start: container questboard-e2e-redis-1 exited (0)

on a backend-only PR, ten seconds after the stack came up. Not a flake, and
not that PR's doing.

The job ran `docker compose -p questboard-e2e`, hard-coded, and so did its
teardown. Two overlapping runs — two open PRs, or a PR and the push to main
that merged another — were therefore *one* Compose project on one runner. The
second `up` adopted the first's containers and restarted them, and the first
job, sitting on its own redis healthcheck, watched redis stop cleanly
underneath it. Every line of that job's compose output is duplicated, which is
the two processes talking about the same project.

It gets likelier the more of the rebuild is in flight at once, which is why it
appeared now and not during lane 1.

A unique project name alone would only move the collision to the host ports,
because the e2e overlay publishes five. So it does both:

- `COMPOSE_PROJECT_NAME=questboard-e2e-${{ github.run_id }}`, used by the run
  (scripts/journeys.sh already honours it) and by the teardown, which had the
  old name written out.
- `QB_E2E_NO_PORTS=1` adds `docker-compose.e2e.ci.yml`, which resets the five
  published ports. They cost nothing to drop: every one is documented in the
  overlay as being for a person to look at — "The suite does not use it",
  "Inspection only", "for a human reading what the stack sent" — and the suite
  reaches the stack by service name, `baseURL` being `http://frontend`.

Local runs are unchanged and deliberately so: the ports are the whole point of
`KEEP_STACK=1`, and the README says not to set the flag by hand.

Verified by rendering the merged config: five published ports without the
overlay, none with it, and the same ten services either way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign in to join this conversation.
No description provided.