mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-07-09 16:00:59 +00:00
Harden release integration diagnostics and login retries
This commit is contained in:
parent
fbddd91fcb
commit
5fe6bfde57
2 changed files with 69 additions and 16 deletions
36
.github/workflows/create-release.yml
vendored
36
.github/workflows/create-release.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue