From 1fbc8afe270ecd4ca142c3a87964d95681a3c7ca Mon Sep 17 00:00:00 2001 From: Matthew Volk Date: Tue, 27 Jan 2026 14:40:13 -0600 Subject: [PATCH] ci: add Ignition deploy E2E workflow (Vitest) --- .github/workflows/cli-e2e.yml | 59 ++++++++++ packages/catalyst/e2e/deploy.e2e.spec.ts | 135 +++++++++++++++++++++++ packages/catalyst/package.json | 1 + packages/catalyst/vitest.e2e.config.ts | 12 ++ 4 files changed, 207 insertions(+) create mode 100644 .github/workflows/cli-e2e.yml create mode 100644 packages/catalyst/e2e/deploy.e2e.spec.ts create mode 100644 packages/catalyst/vitest.e2e.config.ts diff --git a/.github/workflows/cli-e2e.yml b/.github/workflows/cli-e2e.yml new file mode 100644 index 000000000..b3e67357b --- /dev/null +++ b/.github/workflows/cli-e2e.yml @@ -0,0 +1,59 @@ +name: CLI Deploy E2E + +on: + pull_request: + branches: [canary] + +concurrency: + group: cli-deploy-e2e + cancel-in-progress: true + +env: + TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} + TURBO_TEAM: ${{ vars.TURBO_TEAM }} + TURBO_REMOTE_CACHE_SIGNATURE_KEY: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} + +jobs: + ignition-deploy: + name: Ignition Deploy E2E + runs-on: ubuntu-latest + timeout-minutes: 20 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 2 + + - uses: pnpm/action-setup@v3 + + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version-file: ".nvmrc" + cache: "pnpm" + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build workspace packages + run: pnpm --filter "./packages/*" build + + - name: Run Ignition deploy E2E (Vitest) + run: pnpm --filter @bigcommerce/catalyst test:e2e + env: + HOME: ${{ runner.temp }}/e2e-home + BIGCOMMERCE_STORE_HASH: ${{ secrets.DEPLOY_E2E_STORE_HASH }} + BIGCOMMERCE_ACCESS_TOKEN: ${{ secrets.DEPLOY_E2E_ACCESS_TOKEN }} + BIGCOMMERCE_STOREFRONT_TOKEN: ${{ secrets.DEPLOY_E2E_STOREFRONT_TOKEN }} + BIGCOMMERCE_CHANNEL_ID: ${{ secrets.DEPLOY_E2E_CHANNEL_ID }} + AUTH_SECRET: ${{ secrets.DEPLOY_E2E_AUTH_SECRET }} + BIGCOMMERCE_PROJECT_UUID: ${{ secrets.DEPLOY_E2E_PROJECT_UUID }} + + - name: Upload E2E logs + if: ${{ !cancelled() }} + uses: actions/upload-artifact@v4 + with: + name: cli-e2e-logs + path: /tmp/e2e-*.log + retention-days: 7 diff --git a/packages/catalyst/e2e/deploy.e2e.spec.ts b/packages/catalyst/e2e/deploy.e2e.spec.ts new file mode 100644 index 000000000..fcc60a7ab --- /dev/null +++ b/packages/catalyst/e2e/deploy.e2e.spec.ts @@ -0,0 +1,135 @@ +import { existsSync, readdirSync } from 'node:fs'; +import { join } from 'node:path'; +import { execaCommand } from 'execa'; +import { describe, test, expect, beforeAll } from 'vitest'; + +const REPO_ROOT = join(import.meta.dirname, '..', '..', '..'); +const CORE_DIR = join(REPO_ROOT, 'core'); + +const REQUIRED_ENV_VARS = [ + 'BIGCOMMERCE_STORE_HASH', + 'BIGCOMMERCE_ACCESS_TOKEN', + 'BIGCOMMERCE_STOREFRONT_TOKEN', + 'BIGCOMMERCE_CHANNEL_ID', + 'AUTH_SECRET', + 'BIGCOMMERCE_PROJECT_UUID', +] as const; + +const POLL_CONFIG = { + initialDelay: 15_000, + maxDelay: 120_000, + maxAttempts: 10, +}; + +async function pollUrl(url: string): Promise<{ status: number; body: string }> { + let delay = POLL_CONFIG.initialDelay; + + for (let attempt = 1; attempt <= POLL_CONFIG.maxAttempts; attempt++) { + console.log(` Attempt ${attempt}/${POLL_CONFIG.maxAttempts} (delay=${delay / 1000}s)`); + await new Promise((resolve) => setTimeout(resolve, delay)); + + try { + const response = await fetch(url); + const body = await response.text(); + + console.log(` HTTP ${response.status}, body size: ${body.length} bytes`); + + if (response.status === 200 && / { + let deploymentUrl: string | undefined; + let env: Record; + + beforeAll(() => { + env = {} as Record; + + for (const varName of REQUIRED_ENV_VARS) { + const value = process.env[varName]; + if (value) { + env[varName] = value; + } + } + + console.log(`node: ${process.version}`); + }); + + test('preflight — all required env vars are set', () => { + for (const varName of REQUIRED_ENV_VARS) { + expect(process.env[varName], `Missing env var: ${varName}`).toBeTruthy(); + } + }); + + test('link — creates project.json', async () => { + const result = await execaCommand( + `pnpm catalyst link --project-uuid ${env.BIGCOMMERCE_PROJECT_UUID}`, + { cwd: CORE_DIR, reject: false, all: true, env }, + ); + + console.log(result.all); + expect(result.exitCode).toBe(0); + expect(existsSync(join(CORE_DIR, '.bigcommerce', 'project.json'))).toBe(true); + }); + + test('build — produces dist output', async () => { + const result = await execaCommand('pnpm catalyst build --framework catalyst', { + cwd: CORE_DIR, + reject: false, + all: true, + env, + }); + + console.log(result.all); + expect(result.exitCode).toBe(0); + + const distDir = join(CORE_DIR, '.bigcommerce', 'dist'); + expect(existsSync(distDir)).toBe(true); + expect(readdirSync(distDir).length).toBeGreaterThan(0); + }); + + test('deploy — exits successfully', async () => { + const result = await execaCommand( + [ + 'pnpm catalyst deploy', + `--project-uuid ${env.BIGCOMMERCE_PROJECT_UUID}`, + `--secret "BIGCOMMERCE_STORE_HASH=${env.BIGCOMMERCE_STORE_HASH}"`, + `--secret "BIGCOMMERCE_STOREFRONT_TOKEN=${env.BIGCOMMERCE_STOREFRONT_TOKEN}"`, + `--secret "BIGCOMMERCE_CHANNEL_ID=${env.BIGCOMMERCE_CHANNEL_ID}"`, + `--secret "AUTH_SECRET=${env.AUTH_SECRET}"`, + ].join(' '), + { cwd: CORE_DIR, reject: false, all: true, env }, + ); + + console.log(result.all); + expect(result.exitCode).toBe(0); + + const match = /Deployment URL:\s*(\S+)/.exec(result.all ?? ''); + if (match?.[1]) { + deploymentUrl = match[1]; + console.log(`Deployment URL: ${deploymentUrl}`); + } + }); + + test('deployment URL returns HTTP 200', async ({ skip }) => { + if (!deploymentUrl) { + skip(); + return; + } + + const { status, body } = await pollUrl(deploymentUrl); + + expect(status).toBe(200); + expect(body.toLowerCase()).toContain('