From 32d689b997a8a5b981787b1d96bc68ec6765338e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isaac=20Rold=C3=A1n?= Date: Fri, 31 Jul 2026 13:37:26 +0200 Subject: [PATCH 1/3] Use a secret for the E2E loadtest header Assisted-By: devx/b10c5055-6491-423b-95ca-fa9757fa6553 --- .github/workflows/tests-pr.yml | 13 +++++++-- packages/e2e/.env.example | 4 +++ packages/e2e/helpers/loadtest-header.ts | 33 ++++++++++++++++++++++ packages/e2e/scripts/cleanup-apps.ts | 13 ++++++--- packages/e2e/scripts/cleanup-stores.ts | 13 ++++++--- packages/e2e/scripts/prime-browser-auth.ts | 17 +++++------ packages/e2e/setup/browser.ts | 5 ++-- packages/e2e/setup/global-auth.ts | 8 ++---- 8 files changed, 80 insertions(+), 26 deletions(-) create mode 100644 packages/e2e/helpers/loadtest-header.ts diff --git a/.github/workflows/tests-pr.yml b/.github/workflows/tests-pr.yml index d088e5ea821..0f13c0f0d03 100644 --- a/.github/workflows/tests-pr.yml +++ b/.github/workflows/tests-pr.yml @@ -253,20 +253,26 @@ jobs: E2E_ACCOUNT_PASSWORD: ${{ secrets.E2E_ACCOUNT_PASSWORD }} E2E_STORE_FQDN: ${{ secrets.E2E_STORE_FQDN }} E2E_ORG_ID: ${{ secrets.E2E_ORG_ID }} + E2E_LOADTEST_HEADER: ${{ secrets.E2E_LOADTEST_HEADER }} run: pnpm exec playwright test --shard ${{ matrix.shard }} - name: Upload Playwright report uses: actions/upload-artifact@v4 if: ${{ !cancelled() }} with: name: playwright-report-${{ strategy.job-index }} - path: packages/e2e/playwright-report/ + # Playwright traces contain request headers and must not expose E2E_LOADTEST_HEADER. + path: | + packages/e2e/playwright-report/ + !packages/e2e/playwright-report/**/*.zip retention-days: 14 - name: Upload test results uses: actions/upload-artifact@v4 if: ${{ !cancelled() }} with: name: playwright-results-${{ strategy.job-index }} - path: packages/e2e/test-results/ + path: | + packages/e2e/test-results/ + !packages/e2e/test-results/**/trace.zip retention-days: 14 e2e-cleanup: @@ -297,6 +303,7 @@ jobs: E2E_ACCOUNT_EMAIL: ${{ secrets.E2E_ACCOUNT_EMAIL }} E2E_ACCOUNT_PASSWORD: ${{ secrets.E2E_ACCOUNT_PASSWORD }} E2E_ORG_ID: ${{ secrets.E2E_ORG_ID }} + E2E_LOADTEST_HEADER: ${{ secrets.E2E_LOADTEST_HEADER }} run: pnpm --filter e2e exec tsx scripts/prime-browser-auth.ts - name: Cleanup current-run E2E apps if: ${{ always() }} @@ -305,6 +312,7 @@ jobs: E2E_ACCOUNT_EMAIL: ${{ secrets.E2E_ACCOUNT_EMAIL }} E2E_ACCOUNT_PASSWORD: ${{ secrets.E2E_ACCOUNT_PASSWORD }} E2E_ORG_ID: ${{ secrets.E2E_ORG_ID }} + E2E_LOADTEST_HEADER: ${{ secrets.E2E_LOADTEST_HEADER }} run: | RUN_TOKEN=$(node -e "process.stdout.write(BigInt(process.env.GITHUB_RUN_ID).toString(36))") pnpm --filter e2e exec tsx scripts/cleanup-apps.ts --pattern "r${RUN_TOKEN}" @@ -315,6 +323,7 @@ jobs: E2E_ACCOUNT_EMAIL: ${{ secrets.E2E_ACCOUNT_EMAIL }} E2E_ACCOUNT_PASSWORD: ${{ secrets.E2E_ACCOUNT_PASSWORD }} E2E_ORG_ID: ${{ secrets.E2E_ORG_ID }} + E2E_LOADTEST_HEADER: ${{ secrets.E2E_LOADTEST_HEADER }} run: | RUN_TOKEN=$(node -e "process.stdout.write(BigInt(process.env.GITHUB_RUN_ID).toString(36))") pnpm --filter e2e exec tsx scripts/cleanup-stores.ts --pattern "r${RUN_TOKEN}" diff --git a/packages/e2e/.env.example b/packages/e2e/.env.example index 3e896c46f0d..b9b336403ee 100644 --- a/packages/e2e/.env.example +++ b/packages/e2e/.env.example @@ -9,3 +9,7 @@ E2E_ACCOUNT_PASSWORD= # Required: dedicated e2e org ID # CI secret: E2E_ORG_ID E2E_ORG_ID= + +# Required: full X-Shopify-Loadtest- header name +# CI secret: E2E_LOADTEST_HEADER +E2E_LOADTEST_HEADER= diff --git a/packages/e2e/helpers/loadtest-header.ts b/packages/e2e/helpers/loadtest-header.ts new file mode 100644 index 00000000000..a14877cae98 --- /dev/null +++ b/packages/e2e/helpers/loadtest-header.ts @@ -0,0 +1,33 @@ +import type {BrowserContext} from '@playwright/test' + +const LOADTEST_HEADER_PATTERN = /^X-Shopify-Loadtest-[\da-f]{8}(?:-[\da-f]{4}){3}-[\da-f]{12}$/i +const SHOPIFY_APEX_DOMAINS = [ + 'shopify.com', + 'myshopify.com', + 'shopifysvc.com', + 'shopifycdn.com', + 'shopifyapps.com', + 'shopifycloud.com', +] + +export async function addLoadtestHeader(context: BrowserContext): Promise { + const loadtestHeader = process.env.E2E_LOADTEST_HEADER?.trim() + + if (!loadtestHeader) { + throw new Error('E2E_LOADTEST_HEADER is required') + } + + if (!LOADTEST_HEADER_PATTERN.test(loadtestHeader)) { + throw new Error('E2E_LOADTEST_HEADER must contain a full X-Shopify-Loadtest- header name') + } + + await context.route( + (requestUrl) => + SHOPIFY_APEX_DOMAINS.some( + (apexDomain) => requestUrl.hostname === apexDomain || requestUrl.hostname.endsWith(`.${apexDomain}`), + ), + async (route, request) => { + await route.continue({headers: {...request.headers(), [loadtestHeader]: 'true'}}) + }, + ) +} diff --git a/packages/e2e/scripts/cleanup-apps.ts b/packages/e2e/scripts/cleanup-apps.ts index 06fb7c047ca..d60676030d5 100644 --- a/packages/e2e/scripts/cleanup-apps.ts +++ b/packages/e2e/scripts/cleanup-apps.ts @@ -18,6 +18,7 @@ * E2E_ACCOUNT_EMAIL — Shopify account email for login * E2E_ACCOUNT_PASSWORD — Shopify account password * E2E_ORG_ID — Organization ID to scan for apps + * E2E_LOADTEST_HEADER — Load-test bypass header name */ import {config} from 'dotenv' @@ -30,11 +31,17 @@ import {getLastPageStatus, navigateToDashboard, refreshIfPageError, trackMainFra import {deleteAppFromDevDashboard} from '../setup/app.js' import {uninstallAppFromStore} from '../setup/store.js' import {completeLogin} from '../helpers/browser-login.js' +import {addLoadtestHeader} from '../helpers/loadtest-header.js' import type {Page} from '@playwright/test' // Load .env from packages/e2e/ (not cwd) only if not already configured const __dirname = path.dirname(fileURLToPath(import.meta.url)) -if (!process.env.E2E_ACCOUNT_EMAIL || !process.env.E2E_ACCOUNT_PASSWORD || !process.env.E2E_ORG_ID) { +if ( + !process.env.E2E_ACCOUNT_EMAIL || + !process.env.E2E_ACCOUNT_PASSWORD || + !process.env.E2E_ORG_ID || + !process.env.E2E_LOADTEST_HEADER +) { config({path: path.resolve(__dirname, '../.env')}) } @@ -131,11 +138,9 @@ export async function cleanupAllApps(opts: CleanupOptions = {}): Promise { const browser = await chromium.launch({headless: !opts.headed}) const context = await browser.newContext({ - extraHTTPHeaders: { - 'X-Shopify-Loadtest-Bf8d22e7-120e-4b5b-906c-39ca9d5499a9': 'true', - }, ...(storageStatePath ? {storageState: storageStatePath} : {}), }) + await addLoadtestHeader(context) context.setDefaultTimeout(BROWSER_TIMEOUT.max) context.setDefaultNavigationTimeout(BROWSER_TIMEOUT.max) const page = await context.newPage() diff --git a/packages/e2e/scripts/cleanup-stores.ts b/packages/e2e/scripts/cleanup-stores.ts index dc9f5cbee94..31e57503ceb 100644 --- a/packages/e2e/scripts/cleanup-stores.ts +++ b/packages/e2e/scripts/cleanup-stores.ts @@ -17,6 +17,7 @@ * E2E_ACCOUNT_EMAIL — Shopify account email for login * E2E_ACCOUNT_PASSWORD — Shopify account password * E2E_ORG_ID — Organization ID to scan for stores + * E2E_LOADTEST_HEADER — Load-test bypass header name */ import {config} from 'dotenv' @@ -28,6 +29,7 @@ import {BROWSER_TIMEOUT} from '../setup/constants.js' import {deleteStore, dismissDevConsole, isStoreAppsEmpty} from '../setup/store.js' import {refreshIfPageError, trackMainFrameStatus} from '../setup/browser.js' import {completeLogin} from '../helpers/browser-login.js' +import {addLoadtestHeader} from '../helpers/loadtest-header.js' import { ListAppDevStores, type ListAppDevStoresQuery, @@ -39,7 +41,12 @@ import type {Page} from '@playwright/test' // Load .env from packages/e2e/ (not cwd) only if not already configured const __dirname = path.dirname(fileURLToPath(import.meta.url)) -if (!process.env.E2E_ACCOUNT_EMAIL || !process.env.E2E_ACCOUNT_PASSWORD || !process.env.E2E_ORG_ID) { +if ( + !process.env.E2E_ACCOUNT_EMAIL || + !process.env.E2E_ACCOUNT_PASSWORD || + !process.env.E2E_ORG_ID || + !process.env.E2E_LOADTEST_HEADER +) { config({path: path.resolve(__dirname, '../.env')}) } @@ -113,11 +120,9 @@ export async function cleanupStores(opts: CleanupStoresOptions = {}): Promise({ const storageStatePath = process.env.E2E_BROWSER_STATE_PATH const hasValidStorageState = storageStatePath && fs.existsSync(storageStatePath) const context = await browser.newContext({ - extraHTTPHeaders: { - 'X-Shopify-Loadtest-Bf8d22e7-120e-4b5b-906c-39ca9d5499a9': 'true', - }, ...(hasValidStorageState ? {storageState: storageStatePath} : {}), }) + await addLoadtestHeader(context) context.setDefaultTimeout(BROWSER_TIMEOUT.max) context.setDefaultNavigationTimeout(BROWSER_TIMEOUT.max) const page = await context.newPage() diff --git a/packages/e2e/setup/global-auth.ts b/packages/e2e/setup/global-auth.ts index 2a0b761aed3..d232aa611f7 100644 --- a/packages/e2e/setup/global-auth.ts +++ b/packages/e2e/setup/global-auth.ts @@ -12,6 +12,7 @@ import {CLI_TIMEOUT, BROWSER_TIMEOUT} from './constants.js' import {stripAnsi} from '../helpers/strip-ansi.js' import {waitForText} from '../helpers/wait-for-text.js' import {completeLogin} from '../helpers/browser-login.js' +import {addLoadtestHeader} from '../helpers/loadtest-header.js' import {execa} from 'execa' import {chromium, type Page} from '@playwright/test' import * as path from 'path' @@ -103,11 +104,8 @@ export default async function globalSetup() { // Complete login in a headless browser const browser = await chromium.launch({headless: !process.env.E2E_HEADED}) try { - const context = await browser.newContext({ - extraHTTPHeaders: { - 'X-Shopify-Loadtest-Bf8d22e7-120e-4b5b-906c-39ca9d5499a9': 'true', - }, - }) + const context = await browser.newContext() + await addLoadtestHeader(context) context.setDefaultTimeout(BROWSER_TIMEOUT.max) context.setDefaultNavigationTimeout(BROWSER_TIMEOUT.max) const page = await context.newPage() From a3cbd4ac09040502d518ff6398ac7822e822f2b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isaac=20Rold=C3=A1n?= Date: Fri, 31 Jul 2026 14:01:50 +0200 Subject: [PATCH 2/3] Narrow loadtest header injection domains Assisted-By: devx/b10c5055-6491-423b-95ca-fa9757fa6553 --- packages/e2e/helpers/loadtest-header.ts | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/packages/e2e/helpers/loadtest-header.ts b/packages/e2e/helpers/loadtest-header.ts index a14877cae98..ed71a7e3e10 100644 --- a/packages/e2e/helpers/loadtest-header.ts +++ b/packages/e2e/helpers/loadtest-header.ts @@ -1,14 +1,7 @@ import type {BrowserContext} from '@playwright/test' const LOADTEST_HEADER_PATTERN = /^X-Shopify-Loadtest-[\da-f]{8}(?:-[\da-f]{4}){3}-[\da-f]{12}$/i -const SHOPIFY_APEX_DOMAINS = [ - 'shopify.com', - 'myshopify.com', - 'shopifysvc.com', - 'shopifycdn.com', - 'shopifyapps.com', - 'shopifycloud.com', -] +const LOADTEST_HEADER_DOMAINS = ['shopify.com', 'myshopify.com'] export async function addLoadtestHeader(context: BrowserContext): Promise { const loadtestHeader = process.env.E2E_LOADTEST_HEADER?.trim() @@ -23,7 +16,7 @@ export async function addLoadtestHeader(context: BrowserContext): Promise await context.route( (requestUrl) => - SHOPIFY_APEX_DOMAINS.some( + LOADTEST_HEADER_DOMAINS.some( (apexDomain) => requestUrl.hostname === apexDomain || requestUrl.hostname.endsWith(`.${apexDomain}`), ), async (route, request) => { From 809b09b1aeba32345c12ac0d759a96091c6734be Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 3 Aug 2026 07:25:57 +0000 Subject: [PATCH 3/3] update generated graphql files --- packages/app/src/cli/api/graphql/admin/generated/types.d.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/app/src/cli/api/graphql/admin/generated/types.d.ts b/packages/app/src/cli/api/graphql/admin/generated/types.d.ts index 678c7e4367d..869f90af9df 100644 --- a/packages/app/src/cli/api/graphql/admin/generated/types.d.ts +++ b/packages/app/src/cli/api/graphql/admin/generated/types.d.ts @@ -214,10 +214,6 @@ export type MetaobjectAdminAccess = | 'MERCHANT_READ' /** The merchant has read and write access. No other apps have access. */ | 'MERCHANT_READ_WRITE' - /** The merchant and other apps have no access. */ - | 'PRIVATE' - /** The merchant and other apps have read-only access. */ - | 'PUBLIC_READ' /** The merchant and other apps have read and write access. */ | 'PUBLIC_READ_WRITE';