Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions .github/workflows/cli-e2e.yml
Original file line number Diff line number Diff line change
@@ -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
135 changes: 135 additions & 0 deletions packages/catalyst/e2e/deploy.e2e.spec.ts
Original file line number Diff line number Diff line change
@@ -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 && /<html/i.test(body)) {
return { status: response.status, body };
}
} catch (err: unknown) {
const message = err instanceof Error ? err.message : String(err);
console.log(` Fetch error: ${message}`);
}

delay = Math.min(delay * 2, POLL_CONFIG.maxDelay);
}

throw new Error(`Deployment URL did not return valid HTML after ${POLL_CONFIG.maxAttempts} attempts`);
}

describe('Ignition Deploy E2E', () => {
let deploymentUrl: string | undefined;
let env: Record<string, string>;

beforeAll(() => {
env = {} as Record<string, string>;

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('<html');
});
});
1 change: 1 addition & 0 deletions packages/catalyst/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"test:e2e": "vitest run --config vitest.e2e.config.ts",
"build": "tsup"
},
"engines": {
Expand Down
12 changes: 12 additions & 0 deletions packages/catalyst/vitest.e2e.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { defineConfig } from 'vitest/config';

export default defineConfig({
test: {
environment: 'node',
// NO setupFiles — deliberately excludes vitest.setup.ts (which starts MSW)
include: ['e2e/**/*.e2e.spec.ts'],
testTimeout: 900_000, // 15 minutes per test
hookTimeout: 120_000, // 2 minutes for hooks
// NO coverage thresholds
},
});
Loading