Files
mygit/e2e/helpers/environment/environment-manager.ts
T
DuckQ1u 93d1b7c3d3
Copilot Setup Steps / copilot-setup-steps (push) Has been cancelled
first commit
2026-04-22 19:51:20 +07:00

161 lines
5.3 KiB
TypeScript

import baseDebug from '@tryghost/debug';
import logging from '@tryghost/logging';
import {GhostInstance, MySQLManager} from './service-managers';
import {GhostManager} from './service-managers/ghost-manager';
import {randomUUID} from 'crypto';
import type {GhostConfig} from '@/helpers/playwright/fixture';
const debug = baseDebug('e2e:EnvironmentManager');
/**
* Environment modes for E2E testing.
*
* - dev: Uses dev infrastructure with hot-reloading dev servers
* - build: Uses pre-built image (local or registry, controlled by GHOST_E2E_IMAGE)
*/
export type EnvironmentMode = 'dev' | 'build';
type GhostEnvOverrides = GhostConfig | Record<string, string>;
/**
* Orchestrates e2e test environment.
*
* Supports two modes controlled by GHOST_E2E_MODE environment variable:
* - dev: Uses dev infrastructure with hot-reloading
* - build: Uses pre-built image (set GHOST_E2E_IMAGE for registry images)
*
* All modes use the same infrastructure (MySQL, Redis, Mailpit, Tinybird)
* started via docker compose. Ghost and gateway containers are created
* dynamically per-worker for test isolation.
*/
export class EnvironmentManager {
private readonly mode: EnvironmentMode;
private readonly workerIndex: number;
private readonly mysql: MySQLManager;
private readonly ghost: GhostManager;
private initialized = false;
constructor() {
this.mode = this.detectMode();
this.workerIndex = parseInt(process.env.TEST_PARALLEL_INDEX || '0', 10);
this.mysql = new MySQLManager();
this.ghost = new GhostManager({
workerIndex: this.workerIndex,
mode: this.mode
});
}
/**
* Detect environment mode from GHOST_E2E_MODE environment variable.
*/
private detectMode(): EnvironmentMode {
const envMode = process.env.GHOST_E2E_MODE;
if (envMode === 'build' || envMode === 'dev') {
return envMode;
}
logging.warn('GHOST_E2E_MODE is not set; defaulting to build mode. Use the e2e shell entrypoints for automatic mode resolution.');
return 'build';
}
/**
* Global setup - creates database snapshot for test isolation.
*
* Creates the worker 0 containers (Ghost + Gateway) and waits for Ghost to
* become healthy. Ghost automatically runs migrations on startup. Once healthy,
* we snapshot the database for test isolation.
*/
async globalSetup(): Promise<void> {
logging.info(`Starting ${this.mode} environment global setup...`);
await this.cleanupResources();
// Create base database
await this.mysql.recreateBaseDatabase('ghost_e2e_base');
// Create containers and wait for Ghost to be healthy (runs migrations)
await this.ghost.setup('ghost_e2e_base');
await this.ghost.waitForReady();
this.initialized = true;
// Snapshot the migrated database for test isolation
await this.mysql.createSnapshot('ghost_e2e_base');
logging.info(`${this.mode} environment global setup complete`);
}
/**
* Global teardown - cleanup resources.
*/
async globalTeardown(): Promise<void> {
if (this.shouldPreserveEnvironment()) {
logging.info('PRESERVE_ENV is set - skipping teardown');
return;
}
logging.info(`Starting ${this.mode} environment global teardown...`);
await this.cleanupResources();
logging.info(`${this.mode} environment global teardown complete`);
}
/**
* Per-test setup - creates containers on first call, then clones database and restarts Ghost.
*/
async perTestSetup(options: {
config?: GhostEnvOverrides;
stripe?: {
secretKey: string;
publishableKey: string;
};
} = {}): Promise<GhostInstance> {
// Lazy initialization of Ghost containers (once per worker)
if (!this.initialized) {
debug('Initializing Ghost containers for worker', this.workerIndex, 'in mode', this.mode);
await this.ghost.setup();
this.initialized = true;
}
const siteUuid = randomUUID();
const instanceId = `ghost_e2e_${siteUuid.replace(/-/g, '_')}`;
// Setup database
await this.mysql.setupTestDatabase(instanceId, siteUuid, {
stripe: options.stripe
});
// Restart Ghost with new database
await this.ghost.restartWithDatabase(instanceId, options.config);
await this.ghost.waitForReady();
const port = this.ghost.getGatewayPort();
return {
containerId: this.ghost.ghostContainerId!,
instanceId,
database: instanceId,
port,
baseUrl: `http://localhost:${port}`,
siteUuid
};
}
/**
* Per-test teardown - drops test database.
*/
async perTestTeardown(instance: GhostInstance): Promise<void> {
await this.mysql.cleanupTestDatabase(instance.database);
}
private async cleanupResources(): Promise<void> {
logging.info('Cleaning up e2e resources...');
await this.ghost.cleanupAllContainers();
await this.mysql.dropAllTestDatabases();
await this.mysql.deleteSnapshot();
logging.info('E2E resources cleaned up');
}
private shouldPreserveEnvironment(): boolean {
return process.env.PRESERVE_ENV === 'true';
}
}