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
24 changes: 18 additions & 6 deletions .github/workflows/docker-buildx.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ on:
workflow_dispatch:
schedule:
- cron: "5 4 1,15 * *"
push:
branches: [main]
pull_request:
workflow_run:
workflows: ["Test Drupal Installation"]
branches: [main]
types:
- completed

env:
DOCKERFILE_DIR: php8
Expand All @@ -17,11 +18,18 @@ concurrency:

jobs:
buildx:
if: github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
strategy:
matrix:
php_version: ["8.2", "8.3", "8.4", "8.5"]
variant: ["apache-trixie", "apache-bookworm", "fpm-alpine", "frankenphp-trixie"]
variant:
[
"apache-trixie",
"apache-bookworm",
"fpm-alpine",
"frankenphp-trixie",
]
steps:
- name: Checkout
uses: actions/checkout@v6
Expand All @@ -42,7 +50,9 @@ jobs:
with:
username: hussainweb
password: ${{ secrets.DOCKERHUB_TOKEN }}
if: github.event_name != 'pull_request'
if: |
(github.event_name == 'workflow_run' && github.event.workflow_run.head_branch == 'main') ||
(github.event_name != 'workflow_run' && github.ref == 'refs/heads/main')
- name: Set Dockerfile directory
if: ${{ matrix.php_version == '8.2' || matrix.php_version == '8.3' || matrix.php_version == '8.4' || matrix.php_version == '8.5' }}
run: echo "DOCKERFILE_DIR=php8" >> $GITHUB_ENV
Expand Down Expand Up @@ -72,7 +82,9 @@ jobs:
uses: docker/build-push-action@v6
with:
context: ${{ env.DOCKERFILE_DIR }}/${{ matrix.variant }}/
push: ${{ github.event_name != 'pull_request' }}
push: |
${{ (github.event_name == 'workflow_run' && github.event.workflow_run.head_branch == 'main') ||
(github.event_name != 'workflow_run' && github.ref == 'refs/heads/main') }}
tags: ${{ steps.tags.outputs.tags }}
build-args: |
PHP_VERSION=${{ matrix.php_version }}
Expand Down
92 changes: 92 additions & 0 deletions .github/workflows/test-drupal.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
name: Test Drupal Installation

on:
pull_request:
push:
branches:
- main

jobs:
test-drupal:
name: Test on ${{ matrix.variant }} (PHP ${{ matrix.php_version }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php_version: ["8.4", "8.5"]
variant: ["apache-trixie", "fpm-alpine", "frankenphp-trixie"]

steps:
- uses: actions/checkout@v4

- name: Build Docker image
run: |
docker build \
--build-arg PHP_VERSION=${{ matrix.php_version }} \
-t drupal-test:${{ matrix.php_version }}-${{ matrix.variant }} \
./php8/${{ matrix.variant }}

- name: Create test directory structure
run: |
mkdir -p drupal-root
mkdir -p test-results

- name: Download Drupal
run: |
composer create-project drupal/recommended-project drupal-root --no-interaction --no-dev
cd drupal-root
composer require drush/drush --no-interaction

- name: Set up environment variables
run: |
echo "PHP_VERSION=${{ matrix.php_version }}" >> $GITHUB_ENV
echo "VARIANT=${{ matrix.variant }}" >> $GITHUB_ENV
if [ "${{ matrix.variant }}" = "frankenphp-trixie" ]; then
echo "WEB_ROOT=/app" >> $GITHUB_ENV
else
echo "WEB_ROOT=/var/www/html" >> $GITHUB_ENV
fi
if [ "${{ matrix.variant }}" = "fpm-alpine" ]; then
echo "COMPOSE_FILE=tests/docker-compose.fpm.yml" >> $GITHUB_ENV
else
echo "COMPOSE_FILE=tests/docker-compose.yml" >> $GITHUB_ENV
fi
echo "CONTAINER_NAME=drupal" >> $GITHUB_ENV

- name: Start Docker containers
run: |
docker compose up -d
sleep 10
docker compose ps

- name: Install Drupal
run: |
chmod +x tests/install-drupal.sh
# The script uses 'docker compose exec -T drupal', which matches our service name 'drupal'
./tests/install-drupal.sh ${{ matrix.variant }} "11.x"

- name: Run Drupal verification tests
run: |
chmod +x tests/verify-drupal.sh
./tests/verify-drupal.sh ${{ matrix.variant }}

- name: Capture logs
if: always()
run: |
echo "=== Docker Compose Logs ==="
docker compose logs
echo "=== Container Status ==="
docker compose ps
docker compose logs > test-results/docker-logs-${{ matrix.variant }}.txt

- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results-${{ matrix.php_version }}-${{ matrix.variant }}
path: test-results/
if-no-files-found: ignore

- name: Clean up
if: always()
run: docker compose down -v
16 changes: 16 additions & 0 deletions tests/docker-compose.fpm.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
services:
drupal:
image: drupal-test:${PHP_VERSION}-${VARIANT}
container_name: drupal
volumes:
- ../drupal-root:/var/www/html
nginx:
image: nginx:alpine
container_name: nginx
ports:
- "8080:80"
volumes:
- ../drupal-root:/var/www/html
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- drupal
8 changes: 8 additions & 0 deletions tests/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
services:
drupal:
image: drupal-test:${PHP_VERSION}-${VARIANT}
container_name: drupal
volumes:
- ../drupal-root:${WEB_ROOT}
ports:
- "8080:80"
87 changes: 87 additions & 0 deletions tests/install-drupal.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
#!/bin/bash
set -e

VARIANT=$1
DRUPAL_VERSION=$2
Comment on lines +4 to +5
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

DRUPAL_VERSION parameter is accepted but never used.

$2 is assigned to DRUPAL_VERSION on line 5 and echoed on line 8, but it's never passed to any drush or composer command. The caller passes "11.x" from the workflow (line 66 of test-drupal.yml), which has no effect.

Either remove the parameter or use it to pin the Drupal version during installation.

🤖 Prompt for AI Agents
In `@tests/install-drupal.sh` around lines 4 - 5, The script accepts a
DRUPAL_VERSION argument (variable DRUPAL_VERSION) but never uses it; update the
installation commands (where composer or drush bootstrap/install is invoked,
e.g., the composer create-project or drush site:install steps) to include the
DRUPAL_VERSION variable so the installed Drupal is pinned (for example by
passing composer create-project drupal/recommended-project:"$DRUPAL_VERSION" or
the equivalent version specifier), or if you don't need version pinning remove
the DRUPAL_VERSION parameter and related echo to avoid a no-op argument; ensure
you reference the DRUPAL_VERSION variable (and keep VARIANT handling unchanged).


echo "===================================="
echo "Installing Drupal ${DRUPAL_VERSION} on ${VARIANT}"
echo "===================================="

# Use service name instead of container name
SERVICE="drupal"

# Set the web root path based on variant
if [[ "$VARIANT" == *"frankenphp"* ]]; then
WEBROOT="/app"
else
WEBROOT="/var/www/html"
fi

# Wait for the container to be fully ready with health checks
echo "Waiting for container to be ready..."
for i in {1..12}; do
if docker compose exec -T $SERVICE sh -c 'exit 0' 2>/dev/null; then
echo "Container is ready!"
break
fi
if [ $i -eq 12 ]; then
echo "Container failed to become ready after 60 seconds"
docker compose ps
docker compose logs
exit 1
fi
echo "Waiting for container... ($i/12)"
sleep 5
done

# Check if Drupal is already installed
INSTALLED=$(docker compose exec -T $SERVICE sh -c "if [ -f ${WEBROOT}/web/sites/default/settings.php ] && grep -q 'database' ${WEBROOT}/web/sites/default/settings.php 2>/dev/null; then echo 'yes'; else echo 'no'; fi" || echo "no")

if [ "$INSTALLED" = "yes" ]; then
echo "Drupal appears to be already installed. Skipping installation."
exit 0
fi

# Set proper permissions
echo "Setting up permissions..."
docker compose exec -T $SERVICE sh -c "mkdir -p ${WEBROOT}/web/sites/default/files && chmod -R 777 ${WEBROOT}/web/sites/default/files"
docker compose exec -T $SERVICE sh -c "chmod 777 ${WEBROOT}/web/sites/default"

# Install Drupal using drush with SQLite database file
echo "Installing Drupal using drush with SQLite..."
docker compose exec -T $SERVICE sh -c "cd ${WEBROOT} && vendor/bin/drush site:install minimal \
--db-url="sqlite://localhost/sites/default/files/.ht.sqlite" \
--site-name="Drupal Test Site" \
--account-name=admin \
--account-pass=admin \
--yes \
--no-interaction"
Comment on lines +53 to +59
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Nested double quotes break the sh -c argument, dropping flags.

The inner double quotes on lines 54–55 ("sqlite://..." and "Drupal Test Site") prematurely close/reopen the outer sh -c "..." string. After shell word-splitting, sh -c receives only a partial command as its first argument — everything after the unquoted space in Drupal Test Site is lost (including --account-name, --account-pass, --yes, and --no-interaction).

Without --yes, drush will prompt for confirmation and likely fail/hang under -T (no TTY). The site name would also be truncated to just "Drupal".

Shellcheck (SC2140) flagged this same issue.

Proposed fix — use single quotes for inner values
 docker compose exec -T $SERVICE sh -c "cd ${WEBROOT} && vendor/bin/drush site:install minimal \
-    --db-url="sqlite://localhost/sites/default/files/.ht.sqlite" \
-    --site-name="Drupal Test Site" \
+    --db-url='sqlite://localhost/sites/default/files/.ht.sqlite' \
+    --site-name='Drupal Test Site' \
     --account-name=admin \
     --account-pass=admin \
     --yes \
     --no-interaction"
🧰 Tools
🪛 Shellcheck (0.11.0)

[warning] 54-54: Word is of the form "A"B"C" (B indicated). Did you mean "ABC" or "A"B"C"?

(SC2140)

🤖 Prompt for AI Agents
In `@tests/install-drupal.sh` around lines 53 - 59, The sh -c string contains
nested double quotes that break the argument parsing for the drush command;
update the docker compose exec invocation (the line building the sh -c argument
that cd's to ${WEBROOT} and runs vendor/bin/drush site:install) so that inner
string literals (site DB URL and --site-name value) are either single-quoted or
properly escaped to avoid closing the outer double-quoted sh -c argument; ensure
the full command (including --account-name, --account-pass, --yes,
--no-interaction) is passed as a single argument to sh -c and verify variables
SERVICE and WEBROOT remain unchanged.


# Verify installation
echo "Verifying Drupal installation..."
DRUSH_STATUS=$(docker compose exec -T $SERVICE sh -c "cd ${WEBROOT} && vendor/bin/drush status --format=json" || echo "{}")

echo "Drush status output:"
echo "$DRUSH_STATUS"

# Check if bootstrap was successful
if echo "$DRUSH_STATUS" | grep -q "bootstrap"; then
echo "✓ Drupal installation completed successfully"
else
echo "✗ Drupal installation may have issues"
exit 1
fi

# Set permissions back to safer values but keep files directory writable
echo "Securing permissions..."
# sites/default should not be writable by web server (755)
docker compose exec -T $SERVICE sh -c "chmod 755 ${WEBROOT}/web/sites/default"
# Keep files directory fully writable (777) for testing - SQLite needs directory write access
docker compose exec -T $SERVICE sh -c "chmod -R 777 ${WEBROOT}/web/sites/default/files"

echo "===================================="
echo "Drupal installation complete"
echo "Admin user: admin"
echo "Admin pass: admin"
echo "===================================="
129 changes: 129 additions & 0 deletions tests/nginx.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
server {
listen 80;
server_name localhost;
root /var/www/html/web;

index index.php index.html index.htm;

# Logging
access_log /var/log/nginx/drupal_access.log;
error_log /var/log/nginx/drupal_error.log;

# Drupal specific configurations
location = /favicon.ico {
log_not_found off;
access_log off;
}

location = /robots.txt {
allow all;
log_not_found off;
access_log off;
}

# Very rarely should these ever be accessed outside of your lan
location ~* \.(txt|log)$ {
deny all;
}

location ~ \..*/.*\.php$ {
return 403;
}

location ~ ^/sites/.*/private/ {
return 403;
}

# Block access to scripts in site files directory
location ~ ^/sites/[^/]+/files/.*\.php$ {
deny all;
}

# Allow "Well-Known URIs" as per RFC 5785
location ~* ^/.well-known/ {
allow all;
}

# Block access to "hidden" files and directories whose names begin with a
# period. This includes directories used by version control systems such
# as Subversion or Git to store control files.
location ~ (^|/)\. {
return 403;
}

location / {
# try_files $uri @rewrite; # For Drupal <= 6
try_files $uri /index.php?$query_string; # For Drupal >= 7
}

location @rewrite {
rewrite ^/(.*)$ /index.php?q=$1;
}

# Don't allow direct access to PHP files in the vendor directory.
location ~ /vendor/.*\.php$ {
deny all;
return 404;
}

# Protect files and directories from prying eyes.
location ~* \.(engine|inc|install|make|module|profile|po|sh|.*sql|theme|twig|tpl(\.php)?|xtmpl|yml)(~|\.sw[op]|\.bak|\.orig|\.save)?$|^(\.(?!well-known).*|Entries.*|Repository|Root|Tag|Template|composer\.(json|lock)|web\.config)$|^#.*#$|\.php(~|\.sw[op]|\.bak|\.orig|\.save)$ {
deny all;
return 404;
}

location ~ '\.php$|^/update.php' {
fastcgi_split_path_info ^(.+?\.php)(|/.*)$;

# Ensure the php file exists. Mitigates CVE-2019-11043
try_files $fastcgi_script_name =404;

# Block httpoxy attacks. See https://httpoxy.org/.
fastcgi_param HTTP_PROXY "";

fastcgi_pass drupal:9000;
fastcgi_index index.php;

include fastcgi_params;

# SCRIPT_FILENAME parameter is used for PHP FPM determining
# the script name. If it is not set in fastcgi_params file,
# i.e. /etc/nginx/fastcgi_params or in the parent contexts,
# please comment off following line:
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_param QUERY_STRING $query_string;

fastcgi_intercept_errors on;

# PHP 7 socket
# fastcgi_pass unix:/var/run/php/php7.0-fpm.sock;

# PHP 7 TCP
# fastcgi_pass 127.0.0.1:9000;

fastcgi_buffer_size 128k;
fastcgi_buffers 4 256k;
fastcgi_busy_buffers_size 256k;
fastcgi_temp_file_write_size 256k;
fastcgi_read_timeout 240;
}

# Fighting with Styles? This little gem is amazing.
location ~ ^/sites/.*/files/styles/ {
try_files $uri @rewrite;
}

# Handle private files through Drupal. Private file's path can come
# with a language prefix.
location ~ ^(/[a-z\-]+)?/system/files/ {
try_files $uri /index.php?$query_string;
}

# Enforce clean URLs
# Removes index.php from urls like www.example.com/index.php/my-page --> www.example.com/my-page
# Could be done with 301 for permanent or other redirect codes.
if ($request_uri ~* "^(.*/)index\.php/(.*)") {
return 307 $1$2;
}
}
Loading