first commit
Copilot Setup Steps / copilot-setup-steps (push) Has been cancelled

This commit is contained in:
2026-04-22 19:51:20 +07:00
commit 93d1b7c3d3
579 changed files with 99797 additions and 0 deletions
+32
View File
@@ -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'
+10
View File
@@ -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
+25
View File
@@ -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
+37
View File
@@ -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"
+56
View File
@@ -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
+26
View File
@@ -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'
}
+26
View File
@@ -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}"
+31
View File
@@ -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 "$@"
+116
View File
@@ -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);
}