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/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'; 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..ed71a7e3e10 --- /dev/null +++ b/packages/e2e/helpers/loadtest-header.ts @@ -0,0 +1,26 @@ +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 LOADTEST_HEADER_DOMAINS = ['shopify.com', 'myshopify.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) => + LOADTEST_HEADER_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()