From 2ce70e098aef927064afb7cf7bfb72971fbbab75 Mon Sep 17 00:00:00 2001 From: Alfonso Noriega Date: Wed, 10 Jun 2026 12:39:42 +0200 Subject: [PATCH] Fix E2E cleanup CI checks --- .github/workflows/tests-pr.yml | 27 ++++++++++++++++++ package.json | 3 +- packages/e2e/scripts/cleanup-apps.ts | 42 ++++++++++++++++++++++++++-- 3 files changed, 69 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests-pr.yml b/.github/workflows/tests-pr.yml index 88d1e4f2ae3..dc27922802a 100644 --- a/.github/workflows/tests-pr.yml +++ b/.github/workflows/tests-pr.yml @@ -219,8 +219,35 @@ jobs: if: needs.unit-tests.result != 'success' run: exit 1 + e2e-cleanup-apps: + name: 'E2E preflight app cleanup' + if: github.event_name == 'merge_group' || github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v6 + with: + repository: ${{ github.event.pull_request.head.repo.full_name || github.event.repository.full_name }} + ref: ${{ github.event.pull_request.head.ref || github.event.merge_group.head_ref }} + fetch-depth: 1 + - name: Setup deps + uses: ./.github/actions/setup-cli-deps + with: + node-version: ${{ env.PLAYWRIGHT_NODE_VERSION }} + - name: Install Playwright Chromium + run: pnpm exec playwright install chromium + working-directory: packages/e2e + - name: Delete stale E2E apps + working-directory: packages/e2e + env: + E2E_ACCOUNT_EMAIL: ${{ secrets.E2E_ACCOUNT_EMAIL }} + E2E_ACCOUNT_PASSWORD: ${{ secrets.E2E_ACCOUNT_PASSWORD }} + E2E_ORG_ID: ${{ secrets.E2E_ORG_ID }} + run: pnpm exec tsx scripts/cleanup-apps.ts --delete --max-apps 25 --max-pages 10 + e2e-tests: name: "E2E tests (shard ${{ matrix.shard }})" + needs: e2e-cleanup-apps if: github.event_name == 'merge_group' || github.event.pull_request.head.repo.full_name == github.repository runs-on: ubuntu-latest timeout-minutes: 20 diff --git a/package.json b/package.json index 2caf60084d8..03a5dfd8a9e 100644 --- a/package.json +++ b/package.json @@ -147,7 +147,8 @@ "unresolved": "error" }, "ignoreBinaries": [ - "playwright" + "playwright", + "tsx" ], "ignoreDependencies": [ "@shopify/generate-docs" diff --git a/packages/e2e/scripts/cleanup-apps.ts b/packages/e2e/scripts/cleanup-apps.ts index 7d6552a7217..71df8b33c5a 100644 --- a/packages/e2e/scripts/cleanup-apps.ts +++ b/packages/e2e/scripts/cleanup-apps.ts @@ -13,6 +13,8 @@ * pnpm --filter e2e exec tsx scripts/cleanup-apps.ts --delete # Delete only (skip uninstall — delete only apps with 0 installs) * pnpm --filter e2e exec tsx scripts/cleanup-apps.ts --headed # Show browser window * pnpm --filter e2e exec tsx scripts/cleanup-apps.ts --pattern X # Match apps containing "X" (default: "E2E-") + * pnpm --filter e2e exec tsx scripts/cleanup-apps.ts --max-apps 25 # Stop after finding 25 matching apps + * pnpm --filter e2e exec tsx scripts/cleanup-apps.ts --max-pages 5 # Stop after scanning 5 dashboard pages * * Environment variables (loaded from packages/e2e/.env): * E2E_ACCOUNT_EMAIL — Shopify account email for login @@ -59,6 +61,10 @@ export interface CleanupOptions { headed?: boolean /** Organization ID (default: from E2E_ORG_ID env) */ orgId?: string + /** Stop discovering apps after this many matching apps */ + maxApps?: number + /** Stop discovering apps after this many dashboard pages */ + maxPages?: number } /** @@ -77,6 +83,8 @@ export async function cleanupAllApps(opts: CleanupOptions = {}): Promise { console.log(`[cleanup-apps] Mode: ${MODE_LABELS[mode]}`) console.log(`[cleanup-apps] Org: ${orgId || '(not set)'}`) console.log(`[cleanup-apps] Pattern: "${pattern}"`) + if (opts.maxApps) console.log(`[cleanup-apps] Max apps: ${opts.maxApps}`) + if (opts.maxPages) console.log(`[cleanup-apps] Max pages: ${opts.maxPages}`) console.log('') if (!email || !password) { @@ -118,7 +126,11 @@ export async function cleanupAllApps(opts: CleanupOptions = {}): Promise { // Step 3: Find matching apps console.log('[cleanup-apps] Finding matching apps...') - const apps = await findAppsOnDashboard(page, pattern) + const apps = await findAppsOnDashboard(page, pattern, { + maxApps: opts.maxApps, + maxPages: opts.maxPages, + onlyUninstalled: mode === 'delete', + }) console.log(`[cleanup-apps] Found ${apps.length} app(s) matching pattern "${pattern}"`) console.log('') @@ -233,12 +245,15 @@ export async function cleanupAllApps(opts: CleanupOptions = {}): Promise { async function findAppsOnDashboard( page: Page, namePattern: string, + opts: {maxApps?: number; maxPages?: number; onlyUninstalled?: boolean} = {}, ): Promise<{name: string; url: string; installs: number}[]> { const apps: {name: string; url: string; installs: number}[] = [] let totalSeen = 0 + let pagesSeen = 0 // eslint-disable-next-line no-constant-condition while (true) { + pagesSeen++ // Recover from transient 500/502 before parsing the page for (let attempt = 1; attempt <= 3; attempt++) { if (!(await refreshIfPageError(page))) break @@ -261,13 +276,18 @@ async function findAppsOnDashboard( const installMatch = text.match(/(\d+)\s+install/i) const installs = installMatch ? parseInt(installMatch[1]!, 10) : 0 + if (opts.onlyUninstalled && installs > 0) continue const url = href.startsWith('http') ? href : `https://dev.shopify.com${href}` apps.push({name, url, installs}) + if (opts.maxApps && apps.length >= opts.maxApps) break } console.log(`[cleanup-apps] ...loaded ${totalSeen} apps`) + if (opts.maxApps && apps.length >= opts.maxApps) break + if (opts.maxPages && pagesSeen >= opts.maxPages) break + // Check for next page — navigate via href since the button click may not work const nextLink = page.locator('a[href*="next_cursor"]').first() if (!(await nextLink.isVisible({timeout: BROWSER_TIMEOUT.medium}).catch(() => false))) break @@ -392,12 +412,30 @@ async function main() { pattern = nextArg } + const maxApps = parsePositiveIntegerOption(args, '--max-apps') + const maxPages = parsePositiveIntegerOption(args, '--max-pages') + let mode: CleanupMode = 'full' if (args.includes('--list')) mode = 'list' else if (args.includes('--uninstall')) mode = 'uninstall' else if (args.includes('--delete')) mode = 'delete' - await cleanupAllApps({mode, pattern, headed}) + await cleanupAllApps({mode, pattern, headed, maxApps, maxPages}) +} + +function parsePositiveIntegerOption(args: string[], name: string): number | undefined { + const optionIndex = args.indexOf(name) + if (optionIndex === -1) return undefined + + const optionValue = args[optionIndex + 1] + const parsedValue = Number(optionValue) + if (!optionValue || !Number.isInteger(parsedValue) || parsedValue <= 0) { + console.error(`[cleanup-apps] ${name} requires a positive integer`) + process.exitCode = 1 + return undefined + } + + return parsedValue } // Run if executed directly (not imported)