Skip to content
Merged
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
129 changes: 129 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -128,5 +128,134 @@ jobs:
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: var/coverage/clover.xml
flags: unit
fail_ci_if_error: false
verbose: true

e2e:
name: E2E Tests (${{ matrix.shard }}/${{ strategy.job-total }})
runs-on: ubuntu-latest
timeout-minutes: 20
needs: lint
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3]
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3.7.1

- name: Pull or build E2E image
run: |
# Try to pull pre-built image, fall back to building
docker pull ghcr.io/netresearch/timetracker:e2e || docker buildx bake app-e2e --load

- name: Install dependencies
env:
COMPOSE_PROFILES: e2e
run: |
# Create directories with proper permissions for non-root container user
mkdir -p vendor var/cache var/log bin var/coverage/e2e playwright-report test-results
chmod -R 777 vendor var bin playwright-report test-results
docker compose run --rm app-e2e composer install --no-interaction --prefer-dist --ignore-platform-req=php
docker compose run --rm app-e2e npm install --legacy-peer-deps
docker compose run --rm app-e2e npm run build

- name: Start E2E stack with coverage
env:
COMPOSE_PROFILES: e2e
COVERAGE_ENABLED: '1'
XDEBUG_MODE: coverage
run: |
# Start the E2E stack
docker compose up -d

# Wait for database
docker compose exec -T db-e2e mariadb-admin ping -h 127.0.0.1 -uroot -pglobal123 --wait --connect-timeout=60

# Clear and warmup Symfony cache for test environment
docker compose exec -T app-e2e php bin/console cache:clear --env=test || true
docker compose exec -T app-e2e php bin/console cache:warmup --env=test || true

# Show running containers for debugging
docker compose ps

# Wait for app to be ready (increased timeout, will fail if not ready)
echo "Waiting for app to be ready..."
for i in {1..30}; do
HTTP_CODE=$(curl -s -o /tmp/response.html -w "%{http_code}" http://localhost:8766/login 2>&1)
if [ "$HTTP_CODE" = "200" ]; then
echo "App is ready after $i attempts (HTTP $HTTP_CODE)"
break
fi
if [ $i -eq 30 ]; then
echo "App failed to start within timeout (last HTTP code: $HTTP_CODE)"
echo "=== Last response body ==="
cat /tmp/response.html 2>/dev/null | head -100 || true
echo "=== PHP-FPM logs ==="
docker compose logs app-e2e | tail -50
echo "=== Nginx logs ==="
docker compose logs httpd-e2e | tail -20
echo "=== Symfony error log ==="
docker compose exec -T app-e2e cat var/log/test.log 2>/dev/null | tail -100 || echo "No test.log found"
exit 1
fi
echo "Attempt $i: HTTP $HTTP_CODE, waiting..."
sleep 3
done

# Verify coverage is enabled
curl -s http://localhost:8766/coverage.php?action=status || echo "Coverage endpoint not responding"

- name: Run E2E tests (shard ${{ matrix.shard }}/3)
env:
COMPOSE_PROFILES: e2e
run: |
# Run Playwright from inside the container (browsers pre-installed)
# Use internal Docker network: httpd-e2e is the service name, port 80 is internal
docker compose run --rm \
-e E2E_BASE_URL=http://httpd-e2e:80 \
-e CI=true \
app-e2e npx playwright test --shard=${{ matrix.shard }}/3 -x

- name: Collect E2E coverage
if: always()
run: |
# Fetch coverage report from the app
curl -s "http://localhost:8766/coverage.php?action=report&format=clover" > var/coverage/e2e-clover-${{ matrix.shard }}.xml || true
# Check if coverage file has content
if [ -s var/coverage/e2e-clover-${{ matrix.shard }}.xml ]; then
echo "E2E coverage collected successfully for shard ${{ matrix.shard }}"
else
echo "No E2E coverage data collected for shard ${{ matrix.shard }}"
fi

- name: Upload E2E coverage to Codecov
if: always()
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: var/coverage/e2e-clover-${{ matrix.shard }}.xml
flags: e2e
fail_ci_if_error: false
verbose: true

- name: Upload test artifacts
if: failure()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: e2e-artifacts-shard-${{ matrix.shard }}
path: |
playwright-report/
test-results/
var/coverage/
retention-days: 7

- name: Stop E2E stack
if: always()
env:
COMPOSE_PROFILES: e2e
run: docker compose down
13 changes: 12 additions & 1 deletion .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ jobs:
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build and push
- name: Build and push production image
uses: docker/bake-action@5be5f02ff8819ecd3092ea6b2e6261c31774f2b4 # v6.10.0
with:
source: .
Expand All @@ -61,3 +61,14 @@ jobs:
set: |
*.cache-from=type=gha
*.cache-to=type=gha,mode=max

- name: Build and push E2E image
uses: docker/bake-action@5be5f02ff8819ecd3092ea6b2e6261c31774f2b4 # v6.10.0
with:
source: .
targets: app-e2e
files: docker-bake.hcl
push: ${{ github.event_name != 'pull_request' }}
set: |
*.cache-from=type=gha
*.cache-to=type=gha,mode=max
38 changes: 38 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@
# =============================================================================
# COMPOSER - Stage to copy composer binary from
# =============================================================================
FROM ${COMPOSER_IMAGE} AS composer

Check warning on line 23 in Dockerfile

View workflow job for this annotation

GitHub Actions / docker

Default value for global ARG results in an empty or invalid base image name

InvalidDefaultArgInFrom: Default value for ARG ${COMPOSER_IMAGE} results in empty or invalid base image name More info: https://docs.docker.com/go/dockerfile/rule/invalid-default-arg-in-from/

Check warning on line 23 in Dockerfile

View workflow job for this annotation

GitHub Actions / docker

Default value for global ARG results in an empty or invalid base image name

InvalidDefaultArgInFrom: Default value for ARG ${COMPOSER_IMAGE} results in empty or invalid base image name More info: https://docs.docker.com/go/dockerfile/rule/invalid-default-arg-in-from/

# =============================================================================
# BASE - Runtime with PHP extensions
# =============================================================================
FROM ${PHP_BASE_IMAGE} AS base

Check warning on line 28 in Dockerfile

View workflow job for this annotation

GitHub Actions / docker

Default value for global ARG results in an empty or invalid base image name

InvalidDefaultArgInFrom: Default value for ARG ${PHP_BASE_IMAGE} results in empty or invalid base image name More info: https://docs.docker.com/go/dockerfile/rule/invalid-default-arg-in-from/

Check warning on line 28 in Dockerfile

View workflow job for this annotation

GitHub Actions / docker

Default value for global ARG results in an empty or invalid base image name

InvalidDefaultArgInFrom: Default value for ARG ${PHP_BASE_IMAGE} results in empty or invalid base image name More info: https://docs.docker.com/go/dockerfile/rule/invalid-default-arg-in-from/

# Install system dependencies and PHP extensions in single layer
RUN set -ex \
Expand Down Expand Up @@ -110,7 +110,10 @@
COPY --chown=app:app . .

# Finish composer install (autoloader, scripts)
# Set APP_ENV=prod to prevent loading dev-only bundles during cache warmup
# (MakerBundle etc. are not installed with --no-dev but bundles.php tries to load them in dev mode)
ENV CAPTAINHOOK_DISABLE=true
ENV APP_ENV=prod
RUN composer dump-autoload --optimize --classmap-authoritative \
&& composer run-script post-install-cmd --no-interaction || true

Expand Down Expand Up @@ -175,6 +178,41 @@
ENV APP_ENV=dev
ENV APP_DEBUG=1

# =============================================================================
# E2E - Development image with Playwright and browsers pre-installed
# =============================================================================
FROM dev AS e2e

# Install Playwright system dependencies
RUN set -ex \
&& apt-get update \
&& apt-get install -y --no-install-recommends \
# Playwright chromium dependencies
libnss3 \
libnspr4 \
libatk1.0-0 \
libatk-bridge2.0-0 \
libcups2 \
libdrm2 \
libdbus-1-3 \
libxkbcommon0 \
libatspi2.0-0 \
libxcomposite1 \
libxdamage1 \
libxfixes3 \
libxrandr2 \
libgbm1 \
libasound2 \
libpango-1.0-0 \
libcairo2 \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*

# Install Playwright and browsers
RUN npx playwright install chromium --with-deps

ENV APP_ENV=test

# =============================================================================
# PRODUCTION - Minimal secure image
# =============================================================================
Expand Down
3 changes: 3 additions & 0 deletions compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,9 @@ services:
- APP_FROZEN_TIME=2024-01-15 12:00:00
# Use separate E2E database to avoid conflicts with dev
- DATABASE_URL=mysql://timetracker:timetracker@db-e2e:3306/timetracker?serverVersion=mariadb-12.1.0&charset=utf8mb4
# Coverage collection (set COVERAGE_ENABLED=1 XDEBUG_MODE=coverage to enable)
- COVERAGE_ENABLED=${COVERAGE_ENABLED:-0}
- XDEBUG_MODE=${XDEBUG_MODE:-off}
volumes:
- .:/var/www/html
- ./public:/var/www/html/public
Expand Down
4 changes: 2 additions & 2 deletions docker-bake.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,10 @@ target "app-tools" {
]
}

# E2E test image
# E2E test image (with Playwright and browsers pre-installed)
target "app-e2e" {
inherits = ["_common"]
target = "dev"
target = "e2e"
tags = [
"${REGISTRY}/${IMAGE_NAME}:e2e",
]
Expand Down
3 changes: 2 additions & 1 deletion playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ export default defineConfig({
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
// In CI with sharding, use 2 workers per shard for parallelism
workers: process.env.CI ? 2 : undefined,
reporter: [
['html', { open: 'never' }],
['list'],
Expand Down
Loading