Harden release integration diagnostics and login retries

This commit is contained in:
rcourtman 2026-07-09 12:50:55 +01:00
parent fbddd91fcb
commit 5fe6bfde57
2 changed files with 69 additions and 16 deletions

View file

@ -582,6 +582,42 @@ jobs:
docker compose -f docker-compose.test.yml down -v
- name: Collect integration diagnostics
if: failure()
working-directory: tests/integration
run: |
mkdir -p release-integration-diagnostics
{
echo "=== Docker containers ==="
docker ps -a || true
echo
echo "=== Pulse test server logs ==="
docker logs pulse-test-server 2>&1 || echo "No pulse-test-server container"
echo
echo "=== Mock GitHub server logs ==="
docker logs pulse-mock-github 2>&1 || echo "No pulse-mock-github container"
} | tee release-integration-diagnostics/docker.log
- name: Upload integration Playwright report
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release-integration-playwright-report
path: tests/integration/playwright-report/
if-no-files-found: ignore
retention-days: 14
- name: Upload integration failures
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release-integration-failures
path: |
tests/integration/test-results/
tests/integration/release-integration-diagnostics/
if-no-files-found: ignore
retention-days: 14
- name: Cleanup
if: always()
working-directory: tests/integration

View file

@ -948,10 +948,33 @@ export async function login(page: Page, credentials = E2E_CREDENTIALS) {
return "timeout waiting for authenticated app state after login submission";
};
const prepareFreshLoginAttempt = async (): Promise<"login" | "authenticated" | "unknown"> => {
await waitForPulseReady(page);
await page.goto("/");
await waitForAppShell(page);
const retryState = await Promise.race([
usernameInput
.waitFor({ state: "visible", timeout: 15_000 })
.then(() => "login" as const)
.catch(() => undefined),
page
.waitForURL(AUTHENTICATED_URL, { timeout: 15_000 })
.then(() => "authenticated" as const)
.catch(() => undefined),
]);
return retryState ?? "unknown";
};
const isRetryableLoginOutcome = (outcome: string): boolean =>
/too many|failed to connect to server|server error|timeout waiting/i.test(outcome);
// The backend allows 10 login attempts per minute per IP and counts
// successful ones, so bursts of session logins (worker fixtures plus
// per-test session auth) can transiently trip the limiter. Back off and
// retry through a fresh login form.
// per-test session auth) can transiently trip the limiter. CI can also see
// a short transport miss while the compose service is healthy-but-settling.
// Back off and retry through a fresh login form.
const MAX_LOGIN_ATTEMPTS = 3;
let lastOutcome = "pending";
for (let attempt = 1; attempt <= MAX_LOGIN_ATTEMPTS; attempt++) {
@ -959,23 +982,17 @@ export async function login(page: Page, credentials = E2E_CREDENTIALS) {
if (lastOutcome === "authenticated") {
return;
}
if (/too many/i.test(lastOutcome) && attempt < MAX_LOGIN_ATTEMPTS) {
await page.waitForTimeout(15_000);
await page.goto("/");
await waitForAppShell(page);
const retryState = await Promise.race([
usernameInput
.waitFor({ state: "visible", timeout: 15_000 })
.then(() => "login")
.catch(() => undefined),
page
.waitForURL(AUTHENTICATED_URL, { timeout: 15_000 })
.then(() => "authenticated")
.catch(() => undefined),
]);
if (attempt < MAX_LOGIN_ATTEMPTS && isRetryableLoginOutcome(lastOutcome)) {
const backoffMs = /too many/i.test(lastOutcome) ? 15_000 : 2_000 * attempt;
await page.waitForTimeout(backoffMs);
const retryState = await prepareFreshLoginAttempt();
if (retryState === "authenticated") {
return;
}
if (retryState !== "login") {
lastOutcome = `retry login form unavailable after ${lastOutcome}`;
break;
}
continue;
}
break;