From 1562d30584120c7ba43cf8cd78772121d6519ede Mon Sep 17 00:00:00 2001 From: peptiderepo Date: Fri, 12 Jun 2026 02:32:14 +0000 Subject: [PATCH] =?UTF-8?q?fix(setup):=20retry=20login=20up=20to=203=C3=97?= =?UTF-8?q?=20with=2090s=20timeout=20and=2015s=20backoff?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hostinger rate-limits GitHub runner IPs on wp-login.php, causing TimeoutError (30s) during the auth setup step. This has produced 6 deploy-gate failures over 2 days across multiple repos. Changes: - Raise navigation timeout from 30s to 90s per attempt - Wrap the full login sequence in a retry loop (up to 3 attempts) - Create a fresh BrowserContext per attempt so stale cookies / throttle state cannot carry over between retries - Wait 15s between attempts to let transient throttle windows clear - Log each attempt number and outcome clearly (console.log + console.error) so any future failure is diagnosable from the CI log without a local repro - On final failure, surface a hint pointing to the Hostinger throttle cause Assertion behaviour: unchanged. The #wpadminbar check and storageState write are identical to before — retries only affect getting past wp-login. No changes to smoke test assertions, playwright.config.ts, or workflow files. Agent-Session: e2e-login-flake-20260612 --- global.setup.ts | 91 ++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 71 insertions(+), 20 deletions(-) diff --git a/global.setup.ts b/global.setup.ts index 5bb2da0..25aea31 100755 --- a/global.setup.ts +++ b/global.setup.ts @@ -7,6 +7,11 @@ * * The saved auth state (playwright/.auth/admin.json) is reused by the * "chromium" project so individual tests don't each incur a login round-trip. + * + * Retry hardening (2026-06-12): Hostinger rate-limits GitHub runner IPs on + * wp-login.php, causing TimeoutError on page.goto. The login attempt is now + * retried up to MAX_ATTEMPTS times with a fresh browser context per attempt + * and BACKOFF_MS delay between attempts so transient throttle windows clear. */ import { test as setup, expect } from '@playwright/test'; @@ -15,9 +20,14 @@ import * as fs from 'fs'; const AUTH_FILE = path.join(__dirname, 'playwright/.auth/admin.json'); -setup('authenticate as WP admin', async ({ page }) => { +const MAX_ATTEMPTS = 3; +const BACKOFF_MS = 15_000; // 15 s between attempts +const NAV_TIMEOUT = 90_000; // raised from 30 s → 90 s per attempt + +setup('authenticate as WP admin', async ({ browser }) => { const user = process.env.WP_ADMIN_USER; const password = process.env.WP_ADMIN_PASSWORD; + const baseURL = process.env.BASE_URL ?? 'https://staging.peptiderepo.com'; if (!user || !password) { throw new Error( @@ -25,28 +35,69 @@ setup('authenticate as WP admin', async ({ page }) => { ); } - // Ensure directory exists + // Ensure auth directory exists fs.mkdirSync(path.dirname(AUTH_FILE), { recursive: true }); - await page.goto('/wp-login.php'); - await page.fill('#user_login', user); - await page.fill('#user_pass', password); - await page.click('#wp-submit'); + let lastError: unknown; - // Wait for any post-login navigation. The site may redirect successful logins - // to the homepage (via a login_redirect filter) rather than to wp-admin — - // staging does this. So we don't assert the post-submit URL; instead, we - // verify we're no longer ON wp-login.php (which would indicate a credential - // rejection or 2FA prompt) and then navigate explicitly to wp-admin. - await page.waitForURL((url) => !url.pathname.includes('/wp-login.php'), { - timeout: 20_000, - }); + for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) { + console.log( + `[setup] Login attempt ${attempt}/${MAX_ATTEMPTS} — navigating to wp-login.php ...` + ); - // Navigate explicitly into wp-admin to capture the authenticated admin session. - await page.goto('/wp-admin/'); - await expect(page).toHaveURL(/\/wp-admin\//, { timeout: 20_000 }); - await expect(page.locator('#wpadminbar')).toBeVisible(); + // Fresh context per attempt so stale cookies / throttle state don't carry over. + const context = await browser.newContext({ baseURL }); + const page = await context.newPage(); - // Persist session cookies + localStorage to disk - await page.context().storageState({ path: AUTH_FILE }); + try { + await page.goto('/wp-login.php', { timeout: NAV_TIMEOUT }); + await page.fill('#user_login', user); + await page.fill('#user_pass', password); + await page.click('#wp-submit'); + + // Wait for any post-login navigation. The site may redirect successful + // logins to the homepage (via a login_redirect filter) rather than to + // wp-admin — staging does this. So we don't assert the post-submit URL; + // instead, we verify we're no longer ON wp-login.php (which would + // indicate a credential rejection or 2FA prompt) and then navigate + // explicitly to wp-admin. + await page.waitForURL( + (url) => !url.pathname.includes('/wp-login.php'), + { timeout: NAV_TIMEOUT } + ); + + // Navigate explicitly into wp-admin to capture the authenticated admin session. + await page.goto('/wp-admin/', { timeout: NAV_TIMEOUT }); + await expect(page).toHaveURL(/\/wp-admin\//, { timeout: 20_000 }); + await expect(page.locator('#wpadminbar')).toBeVisible({ timeout: 20_000 }); + + // Persist session cookies + localStorage to disk + await page.context().storageState({ path: AUTH_FILE }); + + console.log(`[setup] Login succeeded on attempt ${attempt}/${MAX_ATTEMPTS}`); + await context.close(); + return; // success — exit setup + } catch (err) { + lastError = err; + const message = err instanceof Error ? err.message : String(err); + console.error( + `[setup] Login attempt ${attempt}/${MAX_ATTEMPTS} FAILED: ${message}` + ); + + await context.close(); + + if (attempt < MAX_ATTEMPTS) { + console.log(`[setup] Waiting ${BACKOFF_MS / 1000}s before retry ...`); + await new Promise((resolve) => setTimeout(resolve, BACKOFF_MS)); + } + } + } + + // All attempts exhausted — surface the last error with a clear diagnostic. + throw new Error( + `[setup] All ${MAX_ATTEMPTS} login attempts failed. ` + + `Last error: ${lastError instanceof Error ? lastError.message : String(lastError)}\n` + + `Hint: Hostinger may still be throttling GitHub runner IPs. ` + + `Re-run the workflow once the throttle window clears, or increase BACKOFF_MS / MAX_ATTEMPTS.` + ); });