Skip to content

Commit 1f3126a

Browse files
committed
Optimize Docker test setup with BuildKit caching and Strauss detection
Add two performance optimizations for local Docker testing: 1. Strauss fork Docker detection - Add isRunningInDocker() to PluginPackager to detect container environment - Use /tmp for Strauss fork in Docker (no cross-drive issues on Linux) - Skip fork cleanup in Docker since /tmp is ephemeral - Maintains Windows compatibility (still uses target directory locally) - Saves ~30 seconds per Docker test run 2. BuildKit cache mounts - Enable DOCKER_BUILDKIT=1 in run-docker-tests.sh - Add cache mounts for /var/cache/apt (downloaded packages) - Add cache mount for /root/.composer/cache (PHPUnit dependencies) - Intentionally exclude /var/lib/apt/lists to ensure fresh package indexes - Cache auto-creates on first run, persists across builds - Saves ~45-60 seconds on subsequent runs Expected total savings: ~1-1.5 minutes per Docker test run after initial cache population. No impact on GitHub Actions (uses separate GHA layer caching).
1 parent 317751b commit 1f3126a

File tree

3 files changed

+67
-21
lines changed

3 files changed

+67
-21
lines changed

bin/run-docker-tests.sh

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ fi
2727
# Prevents /app from being converted to C:/Program Files/Git/app
2828
export MSYS_NO_PATHCONV=1
2929

30+
# Enable Docker BuildKit for cache mount support
31+
# BuildKit provides automatic caching of apt packages and composer dependencies
32+
# Cache is created automatically if missing, reused on subsequent builds
33+
export DOCKER_BUILDKIT=1
34+
3035
echo "🚀 Starting Local Docker Tests (matching CI configuration)"
3136
echo "=================================================="
3237

infrastructure/src/Tooling/PluginPackager.php

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -596,17 +596,41 @@ private function provideStraussBinary( string $targetDir ) :string {
596596
return Path::join( $libDir, 'strauss.phar' );
597597
}
598598

599+
/**
600+
* Detect if running inside a Docker container.
601+
* Used to optimize file operations (e.g., use /tmp for ephemeral data).
602+
*/
603+
private function isRunningInDocker() :bool {
604+
// Check for Docker environment file (standard Docker indicator)
605+
if ( file_exists( '/.dockerenv' ) ) {
606+
return true;
607+
}
608+
// Check for SHIELD_TEST_MODE environment variable (set in our Dockerfile)
609+
if ( getenv( 'SHIELD_TEST_MODE' ) === 'docker' ) {
610+
return true;
611+
}
612+
return false;
613+
}
614+
599615
/**
600616
* Clone Strauss fork and prepare it for use.
601617
* Returns path to bin/strauss executable.
602618
* @throws RuntimeException if clone or setup fails
603619
*/
604620
private function cloneAndPrepareStraussFork( string $targetDir ) :string {
605-
// Clone to a temp directory WITHIN the target directory to ensure same drive on Windows.
606-
// This avoids cross-drive path resolution issues where Strauss (on C:) can't properly
607-
// calculate relative paths for a project on D:.
608621
$forkHash = substr( md5( $this->straussForkRepo ), 0, 12 );
609-
$forkDir = Path::join( $targetDir, '_strauss-fork-'.$forkHash );
622+
623+
// In Docker/Linux: use /tmp (no cross-drive issues, ephemeral so no cleanup needed)
624+
// On Windows: use target directory to avoid cross-drive path resolution issues
625+
if ( $this->isRunningInDocker() ) {
626+
$forkDir = '/tmp/_strauss-fork-'.$forkHash;
627+
}
628+
else {
629+
// Clone to a temp directory WITHIN the target directory to ensure same drive on Windows.
630+
// This avoids cross-drive path resolution issues where Strauss (on C:) can't properly
631+
// calculate relative paths for a project on D:.
632+
$forkDir = Path::join( $targetDir, '_strauss-fork-'.$forkHash );
633+
}
610634
$binPath = Path::join( $forkDir, 'bin', 'strauss' );
611635

612636
// Skip clone if already exists and has bin/strauss
@@ -619,7 +643,9 @@ private function cloneAndPrepareStraussFork( string $targetDir ) :string {
619643
$this->log( sprintf( 'Cloning Strauss fork: %s', $this->straussForkRepo ) );
620644

621645
if ( is_dir( $forkDir ) ) {
622-
$this->removeDirectorySafelyForTemp( $forkDir, $targetDir );
646+
// Pass appropriate base path for safety check
647+
$allowedBase = $this->isRunningInDocker() ? '/tmp' : $targetDir;
648+
$this->removeDirectorySafelyForTemp( $forkDir, $allowedBase );
623649
}
624650

625651
// Clone to parent directory since git clone creates the target
@@ -886,11 +912,12 @@ private function cleanupPackageFiles( string $targetDir ) :void {
886912
$fs = new Filesystem();
887913
$libDir = Path::join( $targetDir, 'src', 'lib' );
888914

889-
// Remove Strauss fork directory if it exists
915+
// Remove Strauss fork directory if it exists (only when not in Docker)
916+
// In Docker, we use /tmp which is ephemeral - no cleanup needed
890917
// NOTE: We use removeDirectoryRecursive() instead of Symfony's $fs->remove() because
891918
// Symfony renames directories to .!xxx temp names before deletion, and if deletion fails,
892919
// these temp directories are left behind causing issues on subsequent runs.
893-
if ( $this->straussForkRepo !== null ) {
920+
if ( $this->straussForkRepo !== null && !$this->isRunningInDocker() ) {
894921
$forkHash = substr( md5( $this->straussForkRepo ), 0, 12 );
895922
$forkDir = Path::join( $targetDir, '_strauss-fork-'.$forkHash );
896923
if ( is_dir( $forkDir ) ) {
@@ -904,6 +931,9 @@ private function cleanupPackageFiles( string $targetDir ) :void {
904931
}
905932
}
906933
}
934+
elseif ( $this->straussForkRepo !== null && $this->isRunningInDocker() ) {
935+
$this->log( 'Skipping Strauss fork cleanup (Docker /tmp is ephemeral)' );
936+
}
907937

908938
// Directories to remove (duplicates after Strauss prefixing)
909939
$this->log( 'Removing duplicate libraries from main vendor...' );

tests/docker/Dockerfile

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
1+
# syntax=docker/dockerfile:1
12
# Shield Security - Multi-Stage Test Runner
23
# Optimized for matrix testing with multi-PHP support and caching
34
# Part of Docker Matrix Testing Optimization (Phase 3, Task 3.1)
5+
#
6+
# BuildKit Cache Optimization:
7+
# This Dockerfile uses BuildKit cache mounts to speed up repeated builds.
8+
# Cache is automatic - created if missing, reused if present.
9+
# Requires: DOCKER_BUILDKIT=1 (set in run-docker-tests.sh)
410

511
# Build arguments for matrix customization
612
# Note: PHP_VERSION default should match DEFAULT_PHP in .github/config/matrix.conf
@@ -17,16 +23,19 @@ FROM ubuntu:22.04 AS base-deps
1723
ENV DEBIAN_FRONTEND=noninteractive
1824
ENV TZ=UTC
1925

20-
# Install system dependencies
21-
RUN apt-get update && apt-get install -y \
26+
# Install system dependencies with apt cache mount for faster rebuilds
27+
# Only cache /var/cache/apt (downloaded .deb files), NOT /var/lib/apt/lists (package indexes)
28+
# This ensures apt-get update always fetches fresh package indexes to avoid staleness
29+
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
30+
rm -rf /var/lib/apt/lists/* && \
31+
apt-get update && apt-get install -y \
2232
curl \
2333
git \
2434
unzip \
2535
subversion \
2636
default-mysql-client \
2737
software-properties-common \
28-
gpg-agent \
29-
&& rm -rf /var/lib/apt/lists/*
38+
gpg-agent
3039

3140
# Install Composer globally (cached layer)
3241
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
@@ -45,11 +54,13 @@ ARG PHP_VERSION
4554
ENV DEBIAN_FRONTEND=noninteractive
4655
ENV TZ=UTC
4756

48-
# Add PHP repository
49-
RUN add-apt-repository ppa:ondrej/php && apt-get update
57+
# Add PHP repository with cache mount (only cache downloaded packages, not indexes)
58+
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
59+
add-apt-repository ppa:ondrej/php && apt-get update
5060

51-
# Install PHP and required extensions
52-
RUN apt-get install -y \
61+
# Install PHP and required extensions with cache mount
62+
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
63+
apt-get install -y \
5364
php${PHP_VERSION} \
5465
php${PHP_VERSION}-cli \
5566
php${PHP_VERSION}-mysql \
@@ -62,8 +73,7 @@ RUN apt-get install -y \
6273
php${PHP_VERSION}-bcmath \
6374
php${PHP_VERSION}-soap \
6475
php${PHP_VERSION}-sqlite3 \
65-
php${PHP_VERSION}-dev \
66-
&& rm -rf /var/lib/apt/lists/*
76+
php${PHP_VERSION}-dev
6777

6878
# Create symlinks for php commands
6979
RUN update-alternatives --install /usr/bin/php php /usr/bin/php${PHP_VERSION} 100
@@ -80,17 +90,18 @@ RUN echo "memory_limit=512M" > /etc/php/${PHP_VERSION}/cli/conf.d/99-testing.ini
8090
FROM php-runtime AS test-framework
8191
ARG PHP_VERSION
8292

83-
# Install PHPUnit based on PHP version compatibility
93+
# Install PHPUnit based on PHP version compatibility with composer cache mount
8494
# PHPUnit 9.6: PHP >=7.3 (works with 7.4, 8.0, 8.1)
8595
# PHPUnit 11.x: PHP >=8.2 (works with 8.2, 8.3, 8.4)
8696
# Note: PHPUnit 10.x is NOT supported by yoast/phpunit-polyfills 4.x
8797
# yoast/phpunit-polyfills 4.x: Supports PHPUnit ^7.5|^8.0|^9.0|^11.0|^12.0
88-
RUN if [ "$(echo ${PHP_VERSION} | cut -d. -f1)" = "7" ] || [ "${PHP_VERSION}" = "8.0" ] || [ "${PHP_VERSION}" = "8.1" ]; then \
89-
composer global require --no-interaction --no-cache \
98+
RUN --mount=type=cache,target=/root/.composer/cache \
99+
if [ "$(echo ${PHP_VERSION} | cut -d. -f1)" = "7" ] || [ "${PHP_VERSION}" = "8.0" ] || [ "${PHP_VERSION}" = "8.1" ]; then \
100+
composer global require --no-interaction \
90101
phpunit/phpunit:^9.6 \
91102
yoast/phpunit-polyfills:^4.0; \
92103
else \
93-
composer global require --no-interaction --no-cache \
104+
composer global require --no-interaction \
94105
phpunit/phpunit:^11.5 \
95106
yoast/phpunit-polyfills:^4.0; \
96107
fi

0 commit comments

Comments
 (0)