Skip to content
Closed
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
5 changes: 4 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ docker
!docker/**/*.entrypoint.sh
!docker/**/*entrypoint.sh

ghost/core/core/built/admin
# Note: ghost/core/core/built/admin is needed for E2E image, so not excluded

# Exclude test directories
ghost/core/test

# Ignore local config files (.json and .jsonc)
ghost/core/config.local.json*
62 changes: 50 additions & 12 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -959,17 +959,33 @@ jobs:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

job_docker_build:
name: Build & Push Docker Image
name: Build & Push E2E Docker Image
runs-on: ubuntu-latest
needs: [job_setup]
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-tags: true
submodules: true

- uses: actions/setup-node@v4
env:
FORCE_COLOR: 0
with:
node-version: ${{ env.NODE_VERSION }}

- name: Restore caches
uses: ./.github/actions/restore-cache
env:
DEPENDENCY_CACHE_KEY: ${{ needs.job_setup.outputs.dependency_cache_key }}

- name: Build assets for E2E image
run: yarn build

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

Expand All @@ -987,9 +1003,9 @@ jobs:
if [ "${{ github.event_name }}" = "pull_request" ] && [ "${{ github.event.pull_request.head.repo.full_name }}" != "${{ github.repository }}" ]; then
IS_FORK_PR="true"
fi
IMAGE_NAME="ghcr.io/${{ github.repository_owner }}/ghost-development"
IMAGE_NAME="ghcr.io/${{ github.repository_owner }}/ghost-e2e"
if [ "$IS_FORK_PR" = "true" ]; then
IMAGE_NAME="ghcr.io/${{ github.event.pull_request.head.repo.owner.login }}/ghost-development"
IMAGE_NAME="ghcr.io/${{ github.event.pull_request.head.repo.owner.login }}/ghost-e2e"
fi
CACHE_KEY=$(echo $IMAGE_NAME | tr '[:upper:]' '[:lower:]')
echo "is-fork-pr=$IS_FORK_PR" >> $GITHUB_OUTPUT
Expand All @@ -1004,6 +1020,21 @@ jobs:
echo " Image name: $IMAGE_NAME"
echo " Cache key: $CACHE_KEY"

- name: Collect version tags
id: version_tags
shell: bash
run: |
# Collect semver-ish tags pointing at this commit so the e2e image can be pulled by version.
TAG_LINES=""
while read -r tag; do
[ -z "$tag" ] && continue
TAG_LINES="${TAG_LINES}type=raw,value=${tag}\n"
done < <(git tag --points-at "$GITHUB_SHA" | grep -E '^[vV]?[0-9]+\.[0-9]+\.[0-9]+' || true)

echo "extra-tags<<EOF" >> $GITHUB_OUTPUT
printf "%b" "$TAG_LINES" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT

- name: Docker meta
id: meta
uses: docker/metadata-action@v5
Expand All @@ -1013,11 +1044,13 @@ jobs:
tags: |
type=ref,event=branch
type=ref,event=pr
type=ref,event=tag
type=sha
type=raw,value=latest,enable={{is_default_branch}}
${{ steps.version_tags.outputs.extra-tags }}
labels: |
org.opencontainers.image.title=Ghost Development
org.opencontainers.image.description=Ghost development build
org.opencontainers.image.title=Ghost E2E
org.opencontainers.image.description=Ghost E2E test image
org.opencontainers.image.vendor=TryGhost
maintainer=TryGhost

Expand All @@ -1026,7 +1059,7 @@ jobs:
id: build
with:
context: .
file: Dockerfile
file: e2e/Dockerfile
push: ${{ steps.strategy.outputs.should-push }}
load: ${{ steps.strategy.outputs.should-load }}
tags: ${{ steps.meta.outputs.tags }}
Expand Down Expand Up @@ -1166,10 +1199,8 @@ jobs:
- name: Setup Docker Registry Mirrors
uses: ./.github/actions/setup-docker-registry-mirrors

- name: Pull images
env:
GHOST_IMAGE_TAG: ${{ steps.load.outputs.image-tag }}
run: docker compose -f e2e/compose.yml pull
- name: Pull build mode gateway image
run: docker pull caddy:2-alpine

- uses: actions/setup-node@v4
with:
Expand All @@ -1183,12 +1214,19 @@ jobs:
- name: Setup Playwright
uses: ./.github/actions/setup-playwright

- name: Start E2E infra
run: yarn workspace @tryghost/e2e infra:up

- name: Run e2e tests
env:
GHOST_IMAGE_TAG: ${{ steps.load.outputs.image-tag }}
TEST_WORKERS_COUNT: 1
GHOST_E2E_MODE: build
GHOST_E2E_IMAGE: ${{ steps.load.outputs.image-tag }}
run: yarn test:e2e:all --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --retries=2

- name: Stop E2E infra
if: always()
run: yarn workspace @tryghost/e2e infra:down

- name: Upload blob report to GitHub Actions Artifacts
if: failure()
uses: actions/upload-artifact@v4
Expand Down
12 changes: 9 additions & 3 deletions compose.dev.analytics.yaml → compose.analytics.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# Analytics (Tinybird) configuration for Ghost development environment
# Use with: docker compose -f compose.dev.yaml -f compose.dev.analytics.yaml up
# Analytics (Tinybird) configuration for Ghost development and E2E testing
# Use with: docker compose -f compose.infra.yaml -f compose.dev.yaml -f compose.analytics.yaml up
#
# This file adds Tinybird analytics services and configuration to ghost-dev.
# This file adds Tinybird analytics services. Can be combined with:
# - compose.dev.yaml for interactive development
# - E2E tests (EnvironmentManager starts this automatically)

services:
analytics:
Expand Down Expand Up @@ -57,7 +59,11 @@ services:
tinybird-local:
condition: service_healthy

# ghost-dev extension - only applied when used with compose.dev.yaml
# For E2E tests, Ghost containers are managed dynamically by EnvironmentManager
ghost-dev:
profiles:
- dev
volumes:
# Mount shared-config volume to access Tinybird tokens created by tb-cli
- shared-config:/mnt/shared-config:ro
Expand Down
62 changes: 6 additions & 56 deletions compose.dev.yaml
Original file line number Diff line number Diff line change
@@ -1,55 +1,11 @@
name: ghost-dev
# Ghost development environment - Ghost container and gateway
# Requires: compose.infra.yaml for MySQL, Redis, Mailpit
#
# Usage:
# docker compose -f compose.infra.yaml -f compose.dev.yaml up
# docker compose -f compose.infra.yaml -f compose.dev.yaml -f compose.analytics.yaml up # With analytics

services:
mysql:
image: mysql:8.4.5
container_name: ghost-dev-mysql
command: --innodb-buffer-pool-size=1G --innodb-log-buffer-size=500M --innodb-change-buffer-max-size=50 --innodb-flush-log-at-trx_commit=0 --innodb-flush-method=O_DIRECT
ports:
- "3306:3306"
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-root}
MYSQL_DATABASE: ${MYSQL_DATABASE:-ghost_dev}
MYSQL_USER: ghost
MYSQL_PASSWORD: ghost
volumes:
- mysql-data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysql", "-h", "127.0.0.1", "-uroot", "-p${MYSQL_ROOT_PASSWORD:-root}", "-e", "SELECT 1"]
interval: 1s
retries: 120
timeout: 5s
start_period: 10s

redis:
image: redis:7.0
container_name: ghost-dev-redis
ports:
- "6379:6379"
volumes:
- redis-data:/data
healthcheck:
test:
- CMD
- redis-cli
- --raw
- incr
- ping
interval: 1s
retries: 120

mailpit:
image: axllent/mailpit
container_name: ghost-dev-mailpit
ports:
- "1025:1025" # SMTP server
- "8025:8025" # Web interface
- "8026:8025" # Web interface (for e2e tests)
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:8025"]
interval: 1s
retries: 30

# Main development Ghost instance
# Additional instances can be created programmatically via E2E GhostManager
ghost-dev:
Expand Down Expand Up @@ -145,15 +101,9 @@ services:
retries: 120

volumes:
mysql-data:
redis-data:
shared-config:
ghost-dev-data:
ghost-dev-images:
ghost-dev-media:
ghost-dev-files:
ghost-dev-logs:

networks:
default:
name: ghost_dev
68 changes: 68 additions & 0 deletions compose.infra.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Shared infrastructure services for Ghost development and E2E testing
# Used by: yarn dev, E2E tests (all modes)
#
# Usage:
# docker compose -f compose.infra.yaml up -d
# docker compose -f compose.infra.yaml -f compose.dev.yaml up # For yarn dev
# docker compose -f compose.infra.yaml -f compose.analytics.yaml up # For E2E tests

name: ghost-dev

services:
mysql:
image: mysql:8.4.5
container_name: ghost-dev-mysql
command: --innodb-buffer-pool-size=1G --innodb-log-buffer-size=500M --innodb-change-buffer-max-size=50 --innodb-flush-log-at-trx_commit=0 --innodb-flush-method=O_DIRECT
ports:
- "3306:3306"
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-root}
MYSQL_DATABASE: ${MYSQL_DATABASE:-ghost_dev}
MYSQL_USER: ghost
MYSQL_PASSWORD: ghost
volumes:
- mysql-data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysql", "-h", "127.0.0.1", "-uroot", "-p${MYSQL_ROOT_PASSWORD:-root}", "-e", "SELECT 1"]
interval: 1s
retries: 120
timeout: 5s
start_period: 10s

redis:
image: redis:7.0
container_name: ghost-dev-redis
ports:
- "6379:6379"
volumes:
- redis-data:/data
healthcheck:
test:
- CMD
- redis-cli
- --raw
- incr
- ping
interval: 1s
retries: 120

mailpit:
image: axllent/mailpit
container_name: ghost-dev-mailpit
ports:
- "1025:1025" # SMTP server
- "8025:8025" # Web interface
- "8026:8025" # Web interface (for e2e tests)
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:8025"]
interval: 1s
retries: 30

volumes:
mysql-data:
redis-data:
shared-config:

networks:
default:
name: ghost_dev
35 changes: 35 additions & 0 deletions docker/dev-gateway/Caddyfile.build
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Build mode Caddyfile
# Used for testing pre-built images (local or registry)

{
admin off
}

:80 {
log {
output stdout
}

# Analytics API - proxy to analytics service
# Handles paths like /.ghost/analytics/* or /blog/.ghost/analytics/*
@analytics_paths path_regexp analytics_match ^(.*)/\.ghost/analytics(.*)$
handle @analytics_paths {
rewrite * {re.analytics_match.2}
reverse_proxy {env.ANALYTICS_PROXY_TARGET} {
header_up Host {host}
header_up X-Forwarded-Host {host}
header_up X-Real-IP {remote_host}
header_up X-Forwarded-For {remote_host}
}
}

# Everything else to Ghost
handle {
reverse_proxy {env.GHOST_BACKEND} {
header_up Host {host}
header_up X-Real-IP {remote_host}
header_up X-Forwarded-For {remote_host}
header_up X-Forwarded-Proto https
}
}
}
45 changes: 45 additions & 0 deletions e2e/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# E2E Testing Dockerfile - assets served via Ghost's /content/files/
# Assumes `yarn build` was run on host before building this image
FROM node:22-bullseye-slim

WORKDIR /home/ghost

# Install curl for health checks
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*

# Copy package files and install dependencies
COPY package.json yarn.lock ./
COPY ghost/core/package.json ghost/core/package.json
COPY ghost/i18n/package.json ghost/i18n/package.json
COPY ghost/parse-email-address/package.json ghost/parse-email-address/package.json
RUN yarn install --frozen-lockfile

# Copy Ghost core and dependencies
COPY ghost/core ghost/core
COPY ghost/i18n ghost/i18n
COPY ghost/parse-email-address ghost/parse-email-address

# Copy built public apps to content/files (Ghost serves these natively)
COPY apps/portal/umd ghost/core/content/files/portal
COPY apps/comments-ui/umd ghost/core/content/files/comments-ui
COPY apps/sodo-search/umd ghost/core/content/files/sodo-search
COPY apps/signup-form/umd ghost/core/content/files/signup-form
COPY apps/announcement-bar/umd ghost/core/content/files/announcement-bar

# Load Tinybird/Stripe config from shared-config (if available)
COPY docker/ghost-dev/entrypoint.sh /home/ghost/entrypoint.sh
RUN chmod +x /home/ghost/entrypoint.sh

# Set environment variables for local asset URLs
ENV portal__url=/content/files/portal/portal.min.js
ENV comments__url=/content/files/comments-ui/comments-ui.min.js
ENV sodoSearch__url=/content/files/sodo-search/sodo-search.min.js
ENV sodoSearch__styles=/content/files/sodo-search/main.css
ENV signupForm__url=/content/files/signup-form/signup-form.min.js
ENV announcementBar__url=/content/files/announcement-bar/announcement-bar.min.js

WORKDIR /home/ghost/ghost/core
EXPOSE 2368

ENTRYPOINT ["/home/ghost/entrypoint.sh"]
CMD ["node", "--import=tsx", "index.js"]
Loading