This commit is contained in:
Executable
+32
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
echo "::group::docker ps -a"
|
||||
docker ps -a --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}'
|
||||
echo "::endgroup::"
|
||||
|
||||
dump_container_logs() {
|
||||
local pattern="$1"
|
||||
local label="$2"
|
||||
local found=0
|
||||
|
||||
while IFS= read -r container_name; do
|
||||
if [[ -z "$container_name" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
found=1
|
||||
echo "::group::${label}: ${container_name}"
|
||||
docker inspect "$container_name" --format 'State={{json .State}}' || true
|
||||
docker logs --tail=500 "$container_name" || true
|
||||
echo "::endgroup::"
|
||||
done < <(docker ps -a --format '{{.Names}}' | grep -E "$pattern" || true)
|
||||
|
||||
if [[ "$found" -eq 0 ]]; then
|
||||
echo "No containers matched ${label} pattern: ${pattern}"
|
||||
fi
|
||||
}
|
||||
|
||||
dump_container_logs '^ghost-e2e-worker-' 'Ghost worker'
|
||||
dump_container_logs '^ghost-e2e-gateway-' 'E2E gateway'
|
||||
dump_container_logs '^ghost-dev-(mysql|redis|mailpit|analytics|analytics-db|tinybird-local|tb-cli)$' 'E2E infra'
|
||||
Executable
+10
@@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
docker compose -f compose.dev.yaml -f compose.dev.analytics.yaml stop \
|
||||
analytics tb-cli tinybird-local mailpit redis mysql
|
||||
Executable
+25
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
source "$SCRIPT_DIR/resolve-e2e-mode.sh"
|
||||
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
MODE="$(resolve_e2e_mode)"
|
||||
export GHOST_E2E_MODE="$MODE"
|
||||
|
||||
if [[ "$MODE" != "build" ]]; then
|
||||
DEV_COMPOSE_PROJECT="${COMPOSE_PROJECT_NAME:-ghost-dev}"
|
||||
GHOST_DEV_IMAGE="${DEV_COMPOSE_PROJECT}-ghost-dev"
|
||||
GATEWAY_IMAGE="${DEV_COMPOSE_PROJECT}-ghost-dev-gateway"
|
||||
|
||||
if ! docker image inspect "$GHOST_DEV_IMAGE" >/dev/null 2>&1 || ! docker image inspect "$GATEWAY_IMAGE" >/dev/null 2>&1; then
|
||||
echo "Building missing dev images for E2E (${GHOST_DEV_IMAGE}, ${GATEWAY_IMAGE})..."
|
||||
docker compose -f compose.dev.yaml -f compose.dev.analytics.yaml build ghost-dev ghost-dev-gateway
|
||||
fi
|
||||
fi
|
||||
|
||||
docker compose -f compose.dev.yaml -f compose.dev.analytics.yaml up -d --wait \
|
||||
mysql redis mailpit tinybird-local analytics
|
||||
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
|
||||
echo "This script must be sourced, not executed" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
PLAYWRIGHT_VERSION="$(node -p 'require("./e2e/package.json").devDependencies["@playwright/test"]')"
|
||||
PLAYWRIGHT_IMAGE="mcr.microsoft.com/playwright:v${PLAYWRIGHT_VERSION}-noble"
|
||||
WORKSPACE_PATH="${GITHUB_WORKSPACE:-$REPO_ROOT}"
|
||||
|
||||
export SCRIPT_DIR
|
||||
export REPO_ROOT
|
||||
export PLAYWRIGHT_VERSION
|
||||
export PLAYWRIGHT_IMAGE
|
||||
export WORKSPACE_PATH
|
||||
Executable
+37
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/load-playwright-container-env.sh"
|
||||
GATEWAY_IMAGE="${GHOST_E2E_GATEWAY_IMAGE:-caddy:2-alpine}"
|
||||
|
||||
echo "Preparing E2E build-mode runtime"
|
||||
echo "Playwright image: ${PLAYWRIGHT_IMAGE}"
|
||||
echo "Gateway image: ${GATEWAY_IMAGE}"
|
||||
|
||||
pids=()
|
||||
labels=()
|
||||
|
||||
run_bg() {
|
||||
local label="$1"
|
||||
shift
|
||||
labels+=("$label")
|
||||
(
|
||||
echo "[${label}] starting"
|
||||
"$@"
|
||||
echo "[${label}] done"
|
||||
) &
|
||||
pids+=("$!")
|
||||
}
|
||||
|
||||
run_bg "pull-gateway-image" docker pull "$GATEWAY_IMAGE"
|
||||
run_bg "pull-playwright-image" docker pull "$PLAYWRIGHT_IMAGE"
|
||||
run_bg "start-infra" env GHOST_E2E_MODE=build bash "$REPO_ROOT/e2e/scripts/infra-up.sh"
|
||||
|
||||
for i in "${!pids[@]}"; do
|
||||
if ! wait "${pids[$i]}"; then
|
||||
echo "[${labels[$i]}] failed" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
node "$REPO_ROOT/e2e/scripts/sync-tinybird-state.mjs"
|
||||
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
|
||||
SKIP_IMAGE_BUILD="${GHOST_E2E_SKIP_IMAGE_BUILD:-false}"
|
||||
|
||||
if [[ "$SKIP_IMAGE_BUILD" != "true" && -z "${GHOST_E2E_BASE_IMAGE:-}" ]]; then
|
||||
echo "GHOST_E2E_BASE_IMAGE is required when building the E2E image in-job" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
echo "Preparing CI E2E job"
|
||||
echo "E2E image: ${GHOST_E2E_IMAGE:-ghost-e2e:local}"
|
||||
echo "Skip image build: ${SKIP_IMAGE_BUILD}"
|
||||
|
||||
if [[ "$SKIP_IMAGE_BUILD" != "true" ]]; then
|
||||
echo "Base image: ${GHOST_E2E_BASE_IMAGE}"
|
||||
fi
|
||||
|
||||
pids=()
|
||||
labels=()
|
||||
|
||||
run_bg() {
|
||||
local label="$1"
|
||||
shift
|
||||
labels+=("$label")
|
||||
(
|
||||
echo "[${label}] starting"
|
||||
"$@"
|
||||
echo "[${label}] done"
|
||||
) &
|
||||
pids+=("$!")
|
||||
}
|
||||
|
||||
# Mostly IO-bound runtime prep (image pulls + infra startup + Tinybird sync) can
|
||||
# overlap with the app/docker builds.
|
||||
run_bg "runtime-preflight" bash "$REPO_ROOT/e2e/scripts/prepare-ci-e2e-build-mode.sh"
|
||||
|
||||
if [[ "$SKIP_IMAGE_BUILD" == "true" ]]; then
|
||||
echo "Using prebuilt E2E image; skipping app and Docker image build."
|
||||
else
|
||||
# Build the assets + E2E image layer while IO-heavy prep is running.
|
||||
pnpm --filter @tryghost/e2e build:apps
|
||||
pnpm --filter @tryghost/e2e build:docker
|
||||
fi
|
||||
|
||||
for i in "${!pids[@]}"; do
|
||||
if ! wait "${pids[$i]}"; then
|
||||
echo "[${labels[$i]}] failed" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
LOCAL_ADMIN_DEV_SERVER_URL="${LOCAL_ADMIN_DEV_SERVER_URL:-http://127.0.0.1:5174}"
|
||||
|
||||
resolve_e2e_mode() {
|
||||
if [[ -n "${GHOST_E2E_MODE:-}" ]]; then
|
||||
case "$GHOST_E2E_MODE" in
|
||||
dev|build)
|
||||
printf '%s' "$GHOST_E2E_MODE"
|
||||
return
|
||||
;;
|
||||
*)
|
||||
echo "Invalid GHOST_E2E_MODE: '$GHOST_E2E_MODE'. Expected one of: dev, build." >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
if curl --silent --fail --max-time 1 "$LOCAL_ADMIN_DEV_SERVER_URL" >/dev/null 2>&1; then
|
||||
printf 'dev'
|
||||
return
|
||||
fi
|
||||
|
||||
printf 'build'
|
||||
}
|
||||
Executable
+26
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SHARD_INDEX="${E2E_SHARD_INDEX:-}"
|
||||
SHARD_TOTAL="${E2E_SHARD_TOTAL:-}"
|
||||
RETRIES="${E2E_RETRIES:-2}"
|
||||
|
||||
if [[ -z "$SHARD_INDEX" || -z "$SHARD_TOTAL" ]]; then
|
||||
echo "Missing E2E_SHARD_INDEX or E2E_SHARD_TOTAL environment variables" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/load-playwright-container-env.sh"
|
||||
|
||||
docker run --rm --network host --ipc host \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v "${WORKSPACE_PATH}:${WORKSPACE_PATH}" \
|
||||
-w "${WORKSPACE_PATH}/e2e" \
|
||||
-e CI=true \
|
||||
-e TEST_WORKERS_COUNT="${TEST_WORKERS_COUNT:-1}" \
|
||||
-e COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-ghost-dev}" \
|
||||
-e GHOST_E2E_MODE="${GHOST_E2E_MODE:-build}" \
|
||||
-e GHOST_E2E_IMAGE="${GHOST_E2E_IMAGE:-ghost-e2e:local}" \
|
||||
-e GHOST_E2E_GATEWAY_IMAGE="${GHOST_E2E_GATEWAY_IMAGE:-caddy:2-alpine}" \
|
||||
"$PLAYWRIGHT_IMAGE" \
|
||||
bash -c "corepack enable && pnpm test:all --shard=${SHARD_INDEX}/${SHARD_TOTAL} --retries=${RETRIES}"
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
source "$SCRIPT_DIR/resolve-e2e-mode.sh"
|
||||
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
GHOST_E2E_MODE="$(resolve_e2e_mode)"
|
||||
export GHOST_E2E_MODE
|
||||
|
||||
if [[ "$GHOST_E2E_MODE" == "dev" ]]; then
|
||||
echo "E2E mode: dev (detected admin dev server at $LOCAL_ADMIN_DEV_SERVER_URL)"
|
||||
else
|
||||
echo "E2E mode: build (admin dev server not detected at $LOCAL_ADMIN_DEV_SERVER_URL)"
|
||||
echo " Tip: For local development, run 'pnpm dev' first — dev mode is faster and doesn't require a pre-built Docker image."
|
||||
fi
|
||||
|
||||
# Dev-mode E2E Ghost containers mount the local workspace package, which needs a
|
||||
# built entrypoint before Ghost can require it during boot.
|
||||
if [[ "$GHOST_E2E_MODE" == "dev" ]]; then
|
||||
pnpm --filter @tryghost/parse-email-address build >/dev/null
|
||||
fi
|
||||
|
||||
if [[ "${CI:-}" != "true" ]]; then
|
||||
node "$REPO_ROOT/e2e/scripts/sync-tinybird-state.mjs"
|
||||
fi
|
||||
|
||||
cd "$REPO_ROOT/e2e"
|
||||
exec "$@"
|
||||
@@ -0,0 +1,116 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import {execFileSync} from 'node:child_process';
|
||||
import {fileURLToPath} from 'node:url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const repoRoot = path.resolve(__dirname, '../..');
|
||||
const stateDir = path.resolve(repoRoot, 'e2e/data/state');
|
||||
const configPath = path.resolve(stateDir, 'tinybird.json');
|
||||
|
||||
const composeArgs = [
|
||||
'compose',
|
||||
'-f', path.resolve(repoRoot, 'compose.dev.yaml'),
|
||||
'-f', path.resolve(repoRoot, 'compose.dev.analytics.yaml')
|
||||
];
|
||||
const composeProject = process.env.COMPOSE_PROJECT_NAME || 'ghost-dev';
|
||||
|
||||
function log(message) {
|
||||
process.stdout.write(`${message}\n`);
|
||||
}
|
||||
|
||||
function parseEnv(raw) {
|
||||
const vars = {};
|
||||
|
||||
for (const line of raw.split('\n')) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith('#')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const separatorIndex = trimmed.indexOf('=');
|
||||
if (separatorIndex === -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
vars[trimmed.slice(0, separatorIndex).trim()] = trimmed.slice(separatorIndex + 1).trim();
|
||||
}
|
||||
|
||||
return vars;
|
||||
}
|
||||
|
||||
function clearConfigIfPresent() {
|
||||
if (fs.existsSync(configPath)) {
|
||||
fs.rmSync(configPath, {force: true});
|
||||
log(`Removed stale Tinybird config at ${configPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
function runCompose(args) {
|
||||
return execFileSync('docker', [...composeArgs, ...args], {
|
||||
cwd: repoRoot,
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'pipe']
|
||||
});
|
||||
}
|
||||
|
||||
function isTinybirdRunning() {
|
||||
const output = execFileSync('docker', [
|
||||
'ps',
|
||||
'--filter', `label=com.docker.compose.project=${composeProject}`,
|
||||
'--filter', 'label=com.docker.compose.service=tinybird-local',
|
||||
'--filter', 'status=running',
|
||||
'--format', '{{.Names}}'
|
||||
], {
|
||||
cwd: repoRoot,
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'pipe']
|
||||
});
|
||||
|
||||
return Boolean(output.trim());
|
||||
}
|
||||
|
||||
function fetchConfigFromTbCli() {
|
||||
return runCompose([
|
||||
'run',
|
||||
'--rm',
|
||||
'-T',
|
||||
'tb-cli',
|
||||
'cat',
|
||||
'/mnt/shared-config/.env.tinybird'
|
||||
]);
|
||||
}
|
||||
|
||||
function writeConfig(env) {
|
||||
fs.mkdirSync(stateDir, {recursive: true});
|
||||
fs.writeFileSync(configPath, JSON.stringify({
|
||||
workspaceId: env.TINYBIRD_WORKSPACE_ID,
|
||||
adminToken: env.TINYBIRD_ADMIN_TOKEN,
|
||||
trackerToken: env.TINYBIRD_TRACKER_TOKEN
|
||||
}, null, 2));
|
||||
}
|
||||
|
||||
try {
|
||||
if (!isTinybirdRunning()) {
|
||||
clearConfigIfPresent();
|
||||
log(`Tinybird is not running for compose project ${composeProject}; skipping Tinybird state sync (non-analytics runs are allowed)`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const rawEnv = fetchConfigFromTbCli();
|
||||
const env = parseEnv(rawEnv);
|
||||
|
||||
if (!env.TINYBIRD_WORKSPACE_ID || !env.TINYBIRD_ADMIN_TOKEN) {
|
||||
clearConfigIfPresent();
|
||||
throw new Error('Tinybird is running but required config values are missing in /mnt/shared-config/.env.tinybird');
|
||||
}
|
||||
|
||||
writeConfig(env);
|
||||
log(`Wrote Tinybird config to ${configPath}`);
|
||||
} catch (error) {
|
||||
clearConfigIfPresent();
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
log(`Tinybird state sync failed: ${message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
Reference in New Issue
Block a user