The job was holding the runner for hours on every push and pull request.
On 2026-08-18 it ran 3h12m, and because the runner is sequential it kept
frontend-e2e queued behind it for that whole time.
It is not a slow suite. That run executed 89 tests in 87 seconds, then
logged "Jest did not exit one second after the test run has completed"
and sat there until something killed it — an open handle keeping the
event loop alive after the run finishes. The same run also failed with
relation "orders" does not exist, so the schema was missing in CI even
though globalSetup migrates. Neither is root-caused yet; both follow the
job into its new home.
So the new workflow carries a 15-minute timeout. The suite needs about
90 seconds, so this is not a performance budget — it is a stop that turns
a post-run hang into minutes of wasted runner time rather than hours.
Tests.yml keeps a comment saying where the job went, so its absence reads
as deliberate rather than as something lost in an edit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The first dispatch failed at the wait step with nothing to act on. The
run logs showed the dind service container was never created — no service
startup output at all, and teardown reporting "No such container" for the
ID it had recorded — which is what act_runner does when it refuses a
privileged container.
From the failing step, that is indistinguishable from dockerd simply
being slow, so the step now says which one it is: if the service host
resolves, the container exists and dockerd is not serving plain TCP on
2375; if it does not resolve, the service never started and the runner
needs container.privileged.
Also raises the wait from 30s to 90s. The NAS took the full 30s before
failing, so the old ceiling was too close to the observed time to
distinguish slow from broken.
The docker CLI fallback is retained: the runner image has no docker
binary, and the static install worked.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds a workflow_dispatch job that builds a chosen ref, pushes it to the
Gitea container registry as :qa plus a commit-sha tag, and emails when it
is ready. It deliberately does not restart the QA stack — redeploying
stays a human action in Portainer.
The build runs against a Docker-in-Docker service rather than the NAS's
Docker socket. Mounting the host socket into the runner would give every
workflow on every branch root-equivalent control of the NAS, production
included; pushing to a registry means the image does not need to survive
in the build daemon.
The QA stack now pulls that image instead of requiring a local build. The
previous arrangement meant the image existed only if someone remembered
to build it, which produced two confusing failures already: a Docker Hub
"pull access denied" when the tag was missing, and a silent stale-image
deploy when the build had not been rerun.
Two runner capabilities cannot be verified from here — privileged service
containers for dind, and a docker CLI in the runner image. The workflow
checks both and fails with an explanation rather than a connection
refused, and validates all five required secrets and variables up front
rather than part-way through a build.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
npm treats NODE_ENV=production as --omit=dev, so `npm install` in the
frontend-e2e job skipped typescript and the build died on `tsc: not found`.
The same env var would have stripped vite and @playwright/test from the
frontend install, and flipped the session cookie to Secure on a run served
over plain http.
The reported summarize crash was a symptom: the job aborted before Playwright
ran, but Summarize is `if: always()` and threw ENOENT on the missing JSON,
burying the real failure. Both summarize scripts now report the missing file
and exit 0 -- the job still fails via its own step.
Also split build from start, replaced `sleep 3` with a readiness poll against
/api/config, and dump the backend log when e2e fails.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>