diff --git a/.env.example b/.env.example index 31c8c76..062f34d 100644 --- a/.env.example +++ b/.env.example @@ -2,7 +2,7 @@ PHP_VERSION=8.1 # WordPress Configuration -WORDPRESS_VERSION=6.4 +WORDPRESS_VERSION=6 WORDPRESS_DEBUG=1 WORDPRESS_DEBUG_LOG=1 WORDPRESS_DEBUG_DISPLAY=0 diff --git a/.github/workflows/docker-build-test.yml b/.github/workflows/docker-build-test.yml index 9505890..c406186 100644 --- a/.github/workflows/docker-build-test.yml +++ b/.github/workflows/docker-build-test.yml @@ -2,9 +2,9 @@ name: Docker Build & Test on: push: - branches: [ main ] + branches: [ main, multi-instance-support ] pull_request: - branches: [ main ] + branches: [ main, multi-instance-support ] schedule: # Run weekly on Monday at 00:00 UTC to catch dependency issues - cron: '0 0 * * 1' @@ -26,34 +26,27 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Create .env file - run: | - cp .env.example .env - sed -i "s/PHP_VERSION=8.1/PHP_VERSION=${{ matrix.php-version }}/" .env + - name: Make woocker script executable + run: chmod +x woocker - # For PHP 7.4, use wordpress:php7.4-apache (no version pinning) - if [ "${{ matrix.php-version }}" = "7.4" ]; then - echo "WORDPRESS_IMAGE_TAG=wordpress:php7.4-apache" >> .env - fi + - name: Create Test Instance + run: ./woocker create ci-test --php ${{ matrix.php-version }} - cat .env + - name: Start Instance + run: ./woocker start ci-test - - name: Generate SSL certificates + - name: Export Env Vars for CI + # We need to export variables so subsequent docker compose commands work run: | - mkdir -p ssl - openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ - -keyout ssl/key.pem \ - -out ssl/cert.pem \ - -subj "/C=US/ST=State/L=City/O=Development/CN=localhost" \ - -addext "subjectAltName=DNS:localhost,IP:127.0.0.1" - - - name: Build Docker images - run: docker compose build - timeout-minutes: 10 - - - name: Start containers - run: docker compose up -d - timeout-minutes: 5 + echo "INSTANCE_PATH=$(pwd)/instances/ci-test" >> $GITHUB_ENV + echo "PLUGINS_PATH=$(pwd)/plugins" >> $GITHUB_ENV + echo "COMPOSE_PROJECT_NAME=woocker_ci-test" >> $GITHUB_ENV + # Source the .env to get ports + set -a + source instances/ci-test/.env + set +a + echo "WORDPRESS_PORT=$WORDPRESS_PORT" >> $GITHUB_ENV + echo "PMA_PORT=$PMA_PORT" >> $GITHUB_ENV - name: Wait for services to be ready run: | @@ -63,7 +56,7 @@ jobs: echo "Waiting for WordPress..." sleep 10 - timeout 60 bash -c 'until curl -f http://localhost:8000 > /dev/null 2>&1; do sleep 2; done' + timeout 60 bash -c "until curl -f http://localhost:$WORDPRESS_PORT > /dev/null 2>&1; do sleep 2; done" echo "WordPress is ready!" - name: Check container status @@ -72,7 +65,7 @@ jobs: - name: Verify WordPress installation run: | # Check if WordPress responds - response=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8000) + response=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:$WORDPRESS_PORT) if [ "$response" -ne "200" ] && [ "$response" -ne "301" ] && [ "$response" -ne "302" ]; then echo "WordPress is not responding correctly (HTTP $response)" exit 1 @@ -114,7 +107,7 @@ jobs: - name: Check PHPMyAdmin run: | - response=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8080) + response=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:$PMA_PORT) if [ "$response" -ne "200" ]; then echo "PHPMyAdmin is not responding correctly (HTTP $response)" exit 1 @@ -131,30 +124,40 @@ jobs: - name: Stop containers if: always() - run: docker compose down -v + run: ./woocker remove ci-test --force - test-setup-script: - name: Test Setup Script + test-init-command: + name: Test Init Command runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 - - name: Make setup script executable - run: chmod +x setup.sh + - name: Make woocker script executable + run: chmod +x woocker + + - name: Create and Start Instance + run: | + ./woocker create ci-init + ./woocker start ci-init + + - name: Export Env Vars for CI + run: | + echo "INSTANCE_PATH=$(pwd)/instances/ci-init" >> $GITHUB_ENV + echo "PLUGINS_PATH=$(pwd)/plugins" >> $GITHUB_ENV + echo "COMPOSE_PROJECT_NAME=woocker_ci-init" >> $GITHUB_ENV + set -a + source instances/ci-init/.env + set +a + echo "WORDPRESS_PORT=$WORDPRESS_PORT" >> $GITHUB_ENV - - name: Run setup script - run: ./setup.sh + - name: Run Init Command + run: ./woocker init ci-init timeout-minutes: 15 - env: - CI: true - name: Verify WordPress is installed run: | - # Wait a bit for WordPress to fully initialize - sleep 5 - # Check if WordPress core is installed if docker compose exec -T wordpress wp core is-installed --allow-root; then echo "✓ WordPress is installed" @@ -207,18 +210,9 @@ jobs: exit 1 fi - - name: Verify VS Code config exists - run: | - if [ -f ".vscode/launch.json" ]; then - echo "✓ VS Code launch.json created" - else - echo "✗ VS Code launch.json not found" - exit 1 - fi - - name: Test WordPress site accessibility run: | - response=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8000) + response=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:$WORDPRESS_PORT) if [ "$response" -eq "200" ] || [ "$response" -eq "301" ] || [ "$response" -eq "302" ]; then echo "✓ WordPress site is accessible (HTTP $response)" else @@ -229,7 +223,6 @@ jobs: - name: View logs on failure if: failure() run: | - echo "=== Setup Script Output ===" echo "=== WordPress Logs ===" docker compose logs wordpress echo "=== Database Logs ===" @@ -237,4 +230,4 @@ jobs: - name: Cleanup if: always() - run: docker compose down -v + run: ./woocker remove ci-init --force diff --git a/.gitignore b/.gitignore index 3a5b692..9e07427 100644 --- a/.gitignore +++ b/.gitignore @@ -25,12 +25,19 @@ tests-output/ phpunit.xml.dist # WordPress - ignore everything except custom plugins +# Old structure (deprecated but kept for history if needed) wordpress/ -!wordpress/wp-content/ -!wordpress/wp-content/plugins/ -wordpress/wp-content/plugins/* -!wordpress/wp-content/plugins/.gitkeep -# Add your custom plugins here with !wordpress/wp-content/plugins/your-plugin-name/ + +# New Multi-Instance Structure +instances/ +!instances/.gitkeep + +# Shared Plugins - Track these! +plugins/ +!plugins/.gitkeep +# Ignore everything inside plugins except specific ones if needed, +# but usually we want to track shared plugins. +# If you want to ignore specific plugins, add them here. # Logs *.log diff --git a/Dockerfile b/Dockerfile index 4df331c..207b929 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,8 @@ ARG WORDPRESS_VERSION=6.4 ARG PHP_VERSION=8.1 -# For PHP 7.4, use wordpress:php7.4-apache (no version pinning) -# For PHP 8.0+, use wordpress:6.4-php8.x-apache (version pinned) -ARG WORDPRESS_IMAGE_TAG -FROM ${WORDPRESS_IMAGE_TAG:-wordpress:${WORDPRESS_VERSION}-php${PHP_VERSION}-apache} +ARG WORDPRESS_IMAGE=wordpress:${WORDPRESS_VERSION}-php${PHP_VERSION}-apache + +FROM ${WORDPRESS_IMAGE} # Install system dependencies RUN apt-get update && apt-get install -y \ @@ -35,11 +34,11 @@ RUN a2ensite default-ssl # PHP 8.3+: Xdebug 3.3.x RUN PHP_VERSION=$(php -r "echo PHP_MAJOR_VERSION.'.'.PHP_MINOR_VERSION;") && \ if [ "$PHP_VERSION" = "7.4" ]; then \ - pecl install xdebug-3.1.6; \ + pecl install xdebug-3.1.6; \ elif [ "$PHP_VERSION" = "8.3" ]; then \ - pecl install xdebug-3.3.2; \ + pecl install xdebug-3.3.2; \ else \ - pecl install xdebug-3.2.2; \ + pecl install xdebug-3.2.2; \ fi && \ docker-php-ext-enable xdebug diff --git a/QUICKSTART.md b/QUICKSTART.md index 5df780f..4b32b17 100644 --- a/QUICKSTART.md +++ b/QUICKSTART.md @@ -1,227 +1,62 @@ -# Quick Start Guide +# Woocker Quick Start -Get your WordPress + WooCommerce development environment running in 5 minutes! +## Multi-Instance Support -## One-Command Setup +Woocker now supports running multiple isolated WordPress instances simultaneously. -```bash -./setup.sh -``` - -The script automatically: -- ✅ Creates `.env` configuration file -- ✅ Generates SSL certificates (HTTPS support) -- ✅ Builds Docker containers with your chosen PHP version -- ✅ Installs WordPress -- ✅ Installs WooCommerce -- ✅ Activates Storefront theme -- ✅ Creates all product types (Simple, Variable, Grouped, External, Virtual, Downloadable) -- ✅ Creates 2 sample customers -- ✅ Configures VS Code for debugging - -## Access Your Site - -- **Frontend**: https://wooco.localhost:8443 -- **Admin**: https://wooco.localhost:8443/wp-admin -- **PHPMyAdmin**: http://localhost:8080 +### 1. Create an Instance -**Login**: admin / admin123 - -**Note:** -- HTTPS is enabled by default with auto-generated SSL certificates -- If you see a browser warning (self-signed cert), click "Advanced" → "Proceed" -- Install [mkcert](https://github.com/FiloSottile/mkcert) for trusted certificates (no warnings) -- You can change the hostname in `.env` file - -## Add Your Plugin +Use the `woocker` CLI to create a new instance. You can specify PHP, WordPress, and WooCommerce versions. ```bash -# Create plugin directory -mkdir -p wordpress/wp-content/plugins/my-plugin - -# Create main plugin file -cat > wordpress/wp-content/plugins/my-plugin/my-plugin.php < [options]" + echo "" + echo "Commands:" + echo " create Create a new instance" + echo " --php PHP version (default: 8.1)" + echo " --wp WordPress version (default: 6.4)" + echo " --woo WooCommerce version (default: 8.5.2)" + echo " start Start an instance" + echo " stop Stop an instance" + echo " init Initialize WordPress (install WP, Woo, Theme, Data)" + echo " remove Remove an instance" + echo " --containers Remove only containers" + echo " --files Remove only files" + echo " list List all instances" + echo "" +} + +# Port allocation strategy: Base + Instance Index? +# Or just store ports in .env and let user specify or auto-increment? +# Let's try auto-increment based on existing instances. +get_next_ports() { + local start_port=8000 + local start_ssl_port=8443 + local start_pma_port=8080 + + # Simple collision detection could be complex. + # For now, let's just pick random or hash? No, that's messy. + # Let's iterate through existing .env files to find used ports. + + local used_ports=() + if [ -d "$INSTANCES_DIR" ]; then + for env_file in "$INSTANCES_DIR"/*/.env; do + if [ -f "$env_file" ]; then + local p=$(grep "^WORDPRESS_PORT=" "$env_file" | cut -d= -f2) + used_ports+=($p) + local sp=$(grep "^WORDPRESS_SSL_PORT=" "$env_file" | cut -d= -f2) + used_ports+=($sp) + local pp=$(grep "^PMA_PORT=" "$env_file" | cut -d= -f2) + used_ports+=($pp) + fi + done + fi + + # Find next available block + local port=$start_port + local ssl_port=$start_ssl_port + local pma_port=$start_pma_port + + while true; do + local collision=false + for u in "${used_ports[@]}"; do + if [[ "$u" == "$port" || "$u" == "$ssl_port" || "$u" == "$pma_port" ]]; then + collision=true + break + fi + done + + if [ "$collision" = false ]; then + echo "$port $ssl_port $pma_port" + return + fi + + ((port++)) + ((ssl_port++)) + ((pma_port++)) + done +} + +cmd_create() { + local name=$1 + shift + + if [ -z "$name" ]; then + print_error "Instance name required" + usage + exit 1 + fi + + if [ -d "${INSTANCES_DIR}/${name}" ]; then + print_error "Instance '$name' already exists" + exit 1 + fi + + local php_ver="8.1" + local wp_ver="6.4" + local woo_ver="8.5.2" + + while [[ $# -gt 0 ]]; do + case $1 in + --php) php_ver="$2"; shift 2 ;; + --wp) wp_ver="$2"; shift 2 ;; + --woo) woo_ver="$2"; shift 2 ;; + *) print_error "Unknown option: $1"; exit 1 ;; + esac + done + + print_info "Creating instance '$name'..." + print_info "PHP: $php_ver, WP: $wp_ver, Woo: $woo_ver" + + mkdir -p "${INSTANCES_DIR}/${name}/wordpress" + mkdir -p "${INSTANCES_DIR}/${name}/db_data" + + # Get ports + read -r port ssl_port pma_port <<< $(get_next_ports) + + # Determine Docker Image Tag + local wp_image="wordpress:${wp_ver}-php${php_ver}-apache" + if [ "$php_ver" == "7.4" ]; then + # PHP 7.4 images don't have WP version pinning in recent tags, or use a specific one. + # Using the generic php7.4 tag which pulls the latest WP compatible with 7.4 + wp_image="wordpress:php7.4-apache" + fi + + # Create .env + cat > "${INSTANCES_DIR}/${name}/.env" </dev/null 2>&1; then + print_success "Database is ready" + break + fi + + if [ $attempt -eq $max_attempts ]; then + print_error "Database failed to start" + exit 1 + fi + + print_info "Waiting for database... (attempt $attempt/$max_attempts)" + sleep 2 + ((attempt++)) + done + + print_info "Waiting for WordPress to be ready..." + sleep 5 + print_success "WordPress is ready" +} + +install_wordpress() { + # Check if WordPress is already installed + if docker compose exec -T wordpress wp core is-installed --allow-root 2>/dev/null; then + print_warning "WordPress is already installed. Skipping installation..." + return + fi + + # Use WP_SITE_URL from .env if available, otherwise construct it + local site_url="${WP_SITE_URL}" + if [ -z "$site_url" ]; then + site_url="http://${WORDPRESS_HOSTNAME:-localhost}:${WORDPRESS_PORT:-8000}" + fi + + local site_title="${WP_SITE_TITLE:-WooCommerce Dev Site}" + local admin_user="${WP_ADMIN_USER:-admin}" + local admin_password="${WP_ADMIN_PASSWORD:-admin123}" + local admin_email="${WP_ADMIN_EMAIL:-admin@example.local}" + + print_info "Installing WordPress..." + docker compose exec -T wordpress wp core install \ + --url="${site_url}" \ + --title="${site_title}" \ + --admin_user="${admin_user}" \ + --admin_password="${admin_password}" \ + --admin_email="${admin_email}" \ + --skip-email \ + --allow-root + + print_success "WordPress installed successfully" +} + +install_plugins() { + local wc_version="${WOOCOMMERCE_VERSION:-8.5.2}" + + # Install WooCommerce + print_info "Installing WooCommerce ${wc_version}..." + docker compose exec -T wordpress wp plugin install "woocommerce" --version="${wc_version}" --activate --allow-root + print_success "WooCommerce installed and activated" + + # Run WooCommerce setup + print_info "Configuring WooCommerce..." + docker compose exec -T wordpress wp option update woocommerce_store_address "123 Test Street" --allow-root + docker compose exec -T wordpress wp option update woocommerce_store_city "Test City" --allow-root + docker compose exec -T wordpress wp option update woocommerce_default_country "US:CA" --allow-root + docker compose exec -T wordpress wp option update woocommerce_store_postcode "12345" --allow-root + docker compose exec -T wordpress wp option update woocommerce_currency "USD" --allow-root + docker compose exec -T wordpress wp option update woocommerce_product_type "both" --allow-root + docker compose exec -T wordpress wp option update woocommerce_onboarding_opt_in "no" --allow-root + print_success "WooCommerce configured" +} + +install_theme() { + local theme_version="${STOREFRONT_VERSION:-4.5.5}" + + print_info "Installing Storefront theme ${theme_version}..." + docker compose exec -T wordpress wp theme install "storefront" --version="${theme_version}" --activate --allow-root + print_success "Storefront theme installed and activated" +} + +setup_sample_data() { + print_info "Creating sample products and customers..." + docker compose exec -T wordpress bash /var/www/scripts/setup-sample-data.sh + print_success "Sample data created successfully" +} + +cmd_init() { + local name=$1 + if [ -z "$name" ]; then print_error "Name required"; exit 1; fi + if [ ! -d "${INSTANCES_DIR}/${name}" ]; then print_error "Instance not found"; exit 1; fi + + print_info "Initializing instance '$name'..." + + ( + set -a + source "${INSTANCES_DIR}/${name}/.env" + set +a + export INSTANCE_PATH="${INSTANCES_DIR}/${name}" + export PLUGINS_PATH="${PLUGINS_DIR}" + export COMPOSE_PROJECT_NAME="woocker_${name}" + + wait_for_services + install_wordpress + install_plugins + install_theme + setup_sample_data + ) + + print_success "Instance '$name' initialized!" + print_info "Access it at: http://localhost:$(grep "^WORDPRESS_PORT=" "${INSTANCES_DIR}/${name}/.env" | cut -d= -f2)" +} + +cmd_remove() { + local name=$1 + shift + + if [ -z "$name" ]; then print_error "Name required"; exit 1; fi + if [ ! -d "${INSTANCES_DIR}/${name}" ]; then print_error "Instance not found"; exit 1; fi + + local remove_containers=true + local remove_files=true + local force=false + + while [[ $# -gt 0 ]]; do + case $1 in + --containers) remove_files=false; shift ;; + --files) remove_containers=false; shift ;; + --force|-y) force=true; shift ;; + *) print_error "Unknown option: $1"; exit 1 ;; + esac + done + + # ... (logic for flags) ... + + if [ "$has_flags" = false ]; then + do_containers=true + do_files=true + fi + + if [ "$force" = false ]; then + print_warning "You are about to REMOVE instance '$name':" + if [ "$do_containers" = true ]; then echo " - Containers (and volumes)"; fi + if [ "$do_files" = true ]; then echo " - Files (${INSTANCES_DIR}/${name})"; fi + + read -p "Are you sure? (y/N): " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + print_info "Aborted." + return + fi + fi + + if [ "$do_containers" = true ]; then + print_info "Removing containers..." + ( + set -a + source "${INSTANCES_DIR}/${name}/.env" + set +a + export INSTANCE_PATH="${INSTANCES_DIR}/${name}" + export COMPOSE_PROJECT_NAME="woocker_${name}" + + # Use down -v to remove volumes (db data) associated with containers + docker compose down -v + ) + fi + + if [ "$do_files" = true ]; then + print_info "Removing files..." + rm -rf "${INSTANCES_DIR}/${name}" + fi + + print_success "Instance '$name' removed." +} + +cmd_list() { + print_info "Available Instances:" + for dir in "${INSTANCES_DIR}"/*; do + if [ -d "$dir" ]; then + local name=$(basename "$dir") + local port=$(grep "^WORDPRESS_PORT=" "$dir/.env" 2>/dev/null | cut -d= -f2) + local php=$(grep "^PHP_VERSION=" "$dir/.env" 2>/dev/null | cut -d= -f2) + echo "- $name (PHP $php, Port $port)" + fi + done +} + +case "$1" in + create) shift; cmd_create "$@" ;; + start) shift; cmd_start "$@" ;; + stop) shift; cmd_stop "$@" ;; + init) shift; cmd_init "$@" ;; + remove) shift; cmd_remove "$@" ;; + list) cmd_list ;; + *) usage ;; +esac