diff --git a/.docker/Dockerfile b/.docker/Dockerfile deleted file mode 100644 index 155d50b6b2..0000000000 --- a/.docker/Dockerfile +++ /dev/null @@ -1,128 +0,0 @@ -# Build stage -FROM --platform=$TARGETPLATFORM php:8.3-fpm-alpine AS builder - -# Add QEMU for cross-platform builds -COPY --from=tonistiigi/binfmt:latest /usr/bin/qemu-* /usr/bin/ - -# Install build dependencies -RUN apk add --no-cache --virtual .build-deps \ - $PHPIZE_DEPS \ - gcc \ - g++ \ - openssl-dev \ - make \ - libxml2-dev \ - oniguruma-dev \ - openldap-dev \ - zstd-dev \ - libzip-dev \ - freetype-dev \ - libpng-dev \ - libjpeg-turbo-dev - -# Set cross-compilation flags if needed -ARG TARGETPLATFORM -RUN case "${TARGETPLATFORM}" in \ - linux/arm64*) export CFLAGS='-march=armv8-a' CXXFLAGS='-march=armv8-a' ;; \ - esac - -# Install and configure PHP extensions -RUN set -ex; \ - # Configure extensions - docker-php-ext-configure gd --with-freetype --with-jpeg; \ - # Install extensions one by one to prevent memory issues - docker-php-ext-install mysqli && \ - docker-php-ext-install pdo_mysql && \ - docker-php-ext-install bcmath && \ - docker-php-ext-install mbstring && \ - docker-php-ext-install exif && \ - docker-php-ext-install pcntl && \ - docker-php-ext-install opcache && \ - docker-php-ext-install ldap && \ - docker-php-ext-install zip && \ - pecl install redis && docker-php-ext-enable redis && \ - docker-php-ext-install gd && \ - rm -rf /tmp/* /var/cache/apk/* - - - -# Production stage -FROM --platform=$TARGETPLATFORM php:8.3-fpm-alpine - -# Add production dependencies -RUN apk add --no-cache \ - tini \ - nginx \ - mysql-client \ - openssl \ - supervisor \ - freetype \ - libpng \ - zstd-libs \ - libjpeg-turbo \ - libzip \ - openldap \ - icu-libs && \ - rm -rf /var/cache/apk/* /tmp/* - -# Copy built extensions from builder -COPY --from=builder /usr/local/lib/php/extensions/ /usr/local/lib/php/extensions/ -COPY --from=builder /usr/local/etc/php/conf.d/ /usr/local/etc/php/conf.d/ - -# Add non-root user -ARG PUID=1000 -ARG PGID=1000 - -# Set working directory -WORKDIR /var/www/html - -# Install Leantime -ARG LEAN_VERSION -RUN set -ex; \ - curl -fsSL --retry 3 https://github.com/Leantime/leantime/releases/download/v${LEAN_VERSION}/Leantime-v${LEAN_VERSION}.tar.gz -o leantime.tar.gz && \ - tar xzf leantime.tar.gz --strip-components 1 && \ - rm leantime.tar.gz && \ - chown -R www-data:www-data . - -# Set Permissions -RUN set -ex; \ - # Modify existing www-data user/group - deluser www-data; \ - addgroup -g ${PGID} www-data; \ - adduser -u ${PUID} -G www-data -h /home/www-data -s /bin/sh -D www-data; \ - # Create required directories - mkdir -p /var/www/html/userfiles \ - /var/www/html/public/userfiles \ - /var/www/html/bootstrap/cache \ - /var/www/html/storage/logs \ - /var/www/html/storage/framework/cache \ - /var/www/html/storage/framework/sessions \ - /var/www/html/storage/framework/views \ - /var/www/html/app/Plugins \ - /run /var/log/nginx /var/lib/nginx; \ - chown -R www-data:www-data /var/www/html /run /var/log/nginx /var/lib/nginx && \ - chmod 775 /var/www/html/userfiles \ - /var/www/html/public/userfiles \ - /var/www/html/bootstrap/cache \ - /var/www/html/storage/logs \ - /var/www/html/storage/framework/cache \ - /var/www/html/storage/framework/sessions \ - /var/www/html/storage/framework/views \ - /var/www/html/app/Plugins; - -# Copy configuration files -COPY config/custom.ini /usr/local/etc/php/conf.d/ -COPY config/nginx.conf /etc/nginx/nginx.conf -COPY config/php-fpm.conf /usr/local/etc/php-fpm.d/www.conf -COPY config/supervisord.conf /etc/supervisor/conf.d/supervisord.conf -COPY --chmod=0755 start.sh /start.sh - -# Switch to non-root user -USER www-data - -# Add healthcheck -HEALTHCHECK --interval=30s --timeout=10s --retries=3 \ - CMD curl -f http://localhost:8080 || exit 1 - -EXPOSE 8080 -ENTRYPOINT ["/sbin/tini", "--", "/start.sh"] diff --git a/.docker/config/custom.ini b/.docker/config/custom.ini deleted file mode 100644 index b6eaf95020..0000000000 --- a/.docker/config/custom.ini +++ /dev/null @@ -1,23 +0,0 @@ -memory_limit = 1G -max_execution_time = 300 -opcache.enable = 1 -opcache.jit=1255 -opcache.jit_buffer_size=128M - -; Optimized opcache settings -opcache.memory_consumption=128 -opcache.interned_strings_buffer=8 -opcache.max_accelerated_files=4000 -opcache.revalidate_freq=60 -opcache.fast_shutdown=1 - -; Upload settings -upload_max_filesize = 100M -post_max_size = 100M - -; Session handling -session.gc_maxlifetime = 28800 - -; Error Handline -display_errors = off -log_errors = on \ No newline at end of file diff --git a/.docker/config/nginx.conf b/.docker/config/nginx.conf deleted file mode 100644 index 20d3d6a4c2..0000000000 --- a/.docker/config/nginx.conf +++ /dev/null @@ -1,86 +0,0 @@ -user www-data www-data; -worker_processes auto; -pid /run/nginx.pid; -error_log /dev/stderr info; - -events { - worker_connections 1024; -} - -http { - include /etc/nginx/mime.types; - default_type application/octet-stream; - - access_log /dev/stdout; - - sendfile on; - tcp_nopush on; - tcp_nodelay on; - keepalive_timeout 65; - types_hash_max_size 2048; - client_max_body_size 100M; - - # SSL - ssl_protocols TLSv1.2 TLSv1.3; - ssl_prefer_server_ciphers on; - ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; - - # Security headers - add_header X-Frame-Options "DENY" always; - add_header X-XSS-Protection "1; mode=block; report=/xss-report" always; - add_header X-Content-Type-Options "nosniff" always; - add_header Referrer-Policy "no-referrer-when-downgrade" always; - add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always; - add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; - - server { - listen 8080; - server_name _; - root /var/www/html/public; - index index.php; - - location / { - try_files $uri $uri/ /index.php?$query_string; - } - - location ~ \.php$ { - fastcgi_split_path_info ^(.+\.php)(/.+)$; - fastcgi_pass 127.0.0.1:9000; - fastcgi_index index.php; - include fastcgi_params; - fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; - fastcgi_param PATH_INFO $fastcgi_path_info; - fastcgi_read_timeout 300; - } - - location ~ /\.ht { - deny all; - } - - # additional config - # favicon.ico - location = /favicon.ico { - log_not_found off; - access_log off; - } - - # robots.txt - location = /robots.txt { - log_not_found off; - access_log off; - } - - location ^~ /dist/ { - expires 7d; - access_log off; - } - - # Enable gzip compression - gzip on; - gzip_vary on; - gzip_min_length 1024; - gzip_proxied expired no-cache no-store private auth; - gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml application/javascript; - gzip_disable "MSIE [1-6]\."; - } -} diff --git a/.docker/config/php-fpm.conf b/.docker/config/php-fpm.conf deleted file mode 100644 index 47b307edb0..0000000000 --- a/.docker/config/php-fpm.conf +++ /dev/null @@ -1,28 +0,0 @@ -[global] -error_log = /proc/self/fd/2 -daemonize = no - -[www] -user = www-data -group = www-data - -listen = 127.0.0.1:9000 -listen.owner = www-data -listen.group = www-data -listen.mode = 0660 - -pm = dynamic -pm.max_children = 50 -pm.start_servers = 5 -pm.min_spare_servers = 5 -pm.max_spare_servers = 35 -pm.max_requests = 500 - -clear_env = no - -catch_workers_output = yes -decorate_workers_output = no - -chdir = /var/www/html -php_admin_value[error_log] = /proc/self/fd/2 -php_admin_flag[log_errors] = on diff --git a/.docker/config/supervisord.conf b/.docker/config/supervisord.conf deleted file mode 100644 index ec289dfb21..0000000000 --- a/.docker/config/supervisord.conf +++ /dev/null @@ -1,32 +0,0 @@ -[supervisord] -nodaemon=true -logfile=/dev/stdout -loglevel=info -logfile_maxbytes=0 -pidfile=/run/supervisord.pid - -[program:php-fpm] -command=php-fpm -F -stdout_logfile=/dev/stdout -stdout_logfile_maxbytes=0 -stderr_logfile=/dev/stderr -stderr_logfile_maxbytes=0 -autostart=true -autorestart=true -priority=5 -startretries=10 - -[program:nginx] -command=nginx -g 'daemon off;' -autostart=true -autorestart=true -priority=10 - -[program:scheduler] -command=php /var/www/html/bin/leantime schedule:work -autostart=true -autorestart=true -user=www-data -stdout_logfile=/dev/fd/1 -stdout_logfile_maxbytes=0 -redirect_stderr=true diff --git a/.docker/docker-compose.yml b/.docker/docker-compose.yml deleted file mode 100644 index e597c18f8f..0000000000 --- a/.docker/docker-compose.yml +++ /dev/null @@ -1,64 +0,0 @@ -version: '3.3' - -services: - leantime_db: - image: mysql:8.4 - container_name: mysql_leantime - volumes: - - db_data:/var/lib/mysql - restart: unless-stopped - env_file: ./.env # Environment file with settings - networks: - - leantime-net - command: --character-set-server=UTF8MB4 --collation-server=UTF8MB4_unicode_ci - healthcheck: - test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] - interval: 30s - timeout: 10s - retries: 3 - - leantime: - image: leantime/leantime:3.4.12 - restart: unless-stopped - env_file: ./.env # Environment file with settings - # Add security options - security_opt: - - no-new-privileges:true - # Add capabilities - cap_add: - # - CAP_NET_BIND_SERVICE - - CAP_CHOWN - - CAP_SETGID - - CAP_SETUID - ports: - - "${LEAN_PORT:-8080}:8080" - networks: - - leantime-net - volumes: - - public_userfiles:/var/www/html/public/userfiles # Volume to store public files, logo etc - - userfiles:/var/www/html/userfiles # Original volume name for compatibility - - plugins:/var/www/html/app/Plugins # Plugin storage - - logs:/var/www/html/storage/logs # Log storage - depends_on: - leantime_db: - condition: service_healthy - - # Add a helper container for volume permissions - # Run via docker compose --profile mysql_helper up -d - mysql_helper: - image: mysql:8.4 - command: chown -R mysql:mysql /var/lib/mysql - volumes: - - db_data:/var/lib/mysql - user: root - profiles: [ "helper" ] - -volumes: - db_data: - userfiles: # New volume for public files - public_userfiles: - plugins: - logs: - -networks: - leantime-net: diff --git a/.docker/start.sh b/.docker/start.sh deleted file mode 100644 index a1a63d38ac..0000000000 --- a/.docker/start.sh +++ /dev/null @@ -1,51 +0,0 @@ -#!/bin/sh - -if [[ -n "${LEAN_DB_PASSWORD_FILE}" ]]; then - LEAN_DB_PASSWORD=$(cat "${LEAN_DB_PASSWORD_FILE}") - export LEAN_DB_PASSWORD -fi - -if [[ -n "${LEAN_EMAIL_SMTP_PASSWORD_FILE}" ]]; then - LEAN_EMAIL_SMTP_PASSWORD=$(cat "${LEAN_EMAIL_SMTP_PASSWORD_FILE}") - export LEAN_EMAIL_SMTP_PASSWORD -fi - -if [[ -n "${LEAN_S3_SECRET_FILE}" ]]; then - LEAN_S3_SECRET=$(cat "${LEAN_S3_SECRET_FILE}") - export LEAN_S3_SECRET -fi - -if [[ -n "${LEAN_SESSION_PASSWORD_FILE}" ]]; then - LEAN_SESSION_PASSWORD=$(cat "${LEAN_SESSION_PASSWORD_FILE}") - export LEAN_SESSION_PASSWORD -fi - -if [[ -n "${LEAN_REDIS_PASSWORD_FILE}" ]]; then - LEAN_REDIS_PASSWORD=$(cat "${LEAN_REDIS_PASSWORD_FILE}") - export LEAN_REDIS_PASSWORD -fi - -if [[ -n "${LEAN_DB_HOST_FILE}" ]]; then - LEAN_DB_HOST=$(cat "${LEAN_DB_HOST_FILE}") - export LEAN_DB_HOST -fi - -if [[ -n "${LEAN_DB_DATABASE_FILE}" ]]; then - LEAN_DB_DATABASE=$(cat "${LEAN_DB_DATABASE_FILE}") - export LEAN_DB_DATABASE -fi - -if [[ -n "${LEAN_DB_USER_FILE}" ]]; then - LEAN_DB_USER=$(cat "${LEAN_DB_USER_FILE}") - export LEAN_DB_USER -fi - -if [[ -n "${LEAN_EMAIL_SMTP_USERNAME_FILE}" ]]; then - LEAN_EMAIL_SMTP_USERNAME=$(cat "${LEAN_EMAIL_SMTP_USERNAME_FILE}") - export LEAN_EMAIL_SMTP_USERNAME -fi - -# Ensure supervisord can write its pid file -mkdir -p /run - -/usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..ddd46c988c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,93 @@ +*.md +CHANGELOG.md +README.md +SECURITY.md +CODE_OF_CONDUCT.md +CONTRIBUTING.md +DEPLOYMENT-SETUP.md +CLAUDE.md +LICENSE + +makefile +codeception.yml +crowdin.yml + +phpcs.xml +phpdoc.xml +phpdoc-api.xml +phpstan.neon +web.config.sample +nginx.example.conf +nginx-subfolder.example.conf + +node_modules/ +vendor/ + +logs/ +storage/logs/* +storage/framework/cache/* +storage/framework/sessions/* +storage/framework/views/* +storage/framework/composerPaths.php +storage/framework/viewPaths.php +!storage/logs/.gitkeep +!storage/framework/cache/.gitkeep +!storage/framework/sessions/.gitkeep +!storage/framework/views/.gitkeep + +userfiles/* +public/userfiles/* +!userfiles/.gitkeep +!public/userfiles/.gitkeep + +classmap.json +blocklist.json + +tests/ +codeception.yml + +helm/ + +.git/ +.gitignore +.gitattributes +.github/ +.vscode/ +.idea/ +*.swp +*.swo +*~ +.DS_Store +Thumbs.db +.editorconfig + +.env +.env.* +config/.env +!config/sample.env + +docker-compose*.yml +Dockerfile* +.dockerignore +docker.env +docker.env.example + +.github/ +*.yml + +*.log +*.cache +tmp/ +temp/ +*.tmp + +*.bak +*.backup +backupdb/ + +treeoutput*.txt + +bootstrap/cache/* +!bootstrap/cache/.gitkeep +public/dist/ +public/mix-manifest.json \ No newline at end of file diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml deleted file mode 100644 index fec7879c1b..0000000000 --- a/.github/workflows/deploy.yml +++ /dev/null @@ -1,225 +0,0 @@ -name: Deploy Leantime - -on: - push: - branches: - - develop - - master - workflow_dispatch: - -jobs: - deploy: - name: Deploy to Server - runs-on: ubuntu-latest - - steps: - - name: Setup SSH - env: - SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} - SSH_HOST: ${{ secrets.SSH_HOST }} - run: | - mkdir -p ~/.ssh - echo "$SSH_PRIVATE_KEY" > ~/.ssh/deploy_key - chmod 600 ~/.ssh/deploy_key - ssh-keyscan -H $SSH_HOST >> ~/.ssh/known_hosts - chmod 644 ~/.ssh/known_hosts - - - name: Deploy to Staging - if: github.ref == 'refs/heads/develop' - env: - SSH_HOST: ${{ secrets.SSH_HOST }} - SSH_USER: ${{ secrets.SSH_USER }} - STAGING_PATH: ${{ secrets.STAGING_PATH }} - STAGING_ENV_FILE: ${{ secrets.STAGING_ENV_FILE }} - run: | - echo "Deploying to STAGING" - - # Deploy using git pull - ssh -i ~/.ssh/deploy_key -o StrictHostKeyChecking=no $SSH_USER@$SSH_HOST << ENDSSH - set -e # Exit on error - - echo "Changing to deployment directory..." - cd $STAGING_PATH - - echo "Fetching latest changes..." - git fetch origin develop - - echo "Creating backup tag..." - BACKUP_TAG="backup-staging-$(date +%Y%m%d_%H%M%S)" - git tag -f $BACKUP_TAG HEAD - echo "Backup tag created: $BACKUP_TAG" - - echo "Pulling latest code from develop branch..." - git pull origin develop - - echo "Installing Composer dependencies..." - composer install --no-dev --optimize-autoloader --no-interaction - - echo "Installing NPM dependencies..." - npm ci - - echo "Building frontend assets..." - npx mix --production - - echo "Generating blocklist..." - node generateBlocklist.mjs - - echo "Clearing cache..." - # Clear cache files - sudo find bootstrap/cache -type f ! -name '.gitignore' -delete 2>/dev/null || true - sudo find storage/framework/cache -type f ! -name '.gitignore' -delete 2>/dev/null || true - sudo find storage/framework/sessions -type f ! -name '.gitignore' -delete 2>/dev/null || true - sudo find storage/framework/views -type f ! -name '.gitignore' -delete 2>/dev/null || true - - echo "Setting proper file permissions..." - # Set ownership for application files - sudo chown -R www-data:www-data $STAGING_PATH - - # Secure .env file permissions - sudo chmod 600 config/.env 2>/dev/null || true - - # Make writable directories - sudo chmod -R 775 storage bootstrap/cache userfiles public/userfiles 2>/dev/null || true - - echo "Reloading services..." - # Reload PHP-FPM to clear opcache - sudo systemctl reload php8.2-fpm - - # Reload nginx - sudo systemctl reload nginx - - echo "Staging deployment completed successfully!" - echo "Commit: $(git rev-parse --short HEAD)" - echo "Rollback available: git reset --hard $BACKUP_TAG" - ENDSSH - - - name: Health Check Staging - if: github.ref == 'refs/heads/develop' - env: - STAGING_URL: ${{ secrets.STAGING_URL }} - run: | - echo " Running staging health check..." - sleep 3 - - response=$(curl -s -o /dev/null -w "%{http_code}" $STAGING_URL/healthCheck.php 2>/dev/null || echo "000") - - if [ "$response" = "200" ]; then - echo " Staging health check passed! (HTTP $response)" - else - echo " Warning: Health check returned HTTP $response" - echo "Please verify the staging deployment manually at $STAGING_URL" - fi - - - name: Deploy to Production - if: github.ref == 'refs/heads/master' - env: - SSH_HOST: ${{ secrets.SSH_HOST }} - SSH_USER: ${{ secrets.SSH_USER }} - PRODUCTION_PATH: ${{ secrets.PRODUCTION_PATH }} - run: | - echo "Deploying to PRODUCTION using Git Pull..." - - # Deploy using git pull on server - ssh -i ~/.ssh/deploy_key -o StrictHostKeyChecking=no $SSH_USER@$SSH_HOST << ENDSSH - set -e # Exit on error - - echo "Changing to deployment directory..." - cd $PRODUCTION_PATH - - echo "Fetching latest changes..." - git fetch -v origin master - - echo "Creating backup tag..." - BACKUP_TAG="backup-production-$(date +%Y%m%d_%H%M%S)" - git tag -f $BACKUP_TAG HEAD - echo "Backup tag created: $BACKUP_TAG" - - echo "Pulling latest code from master branch..." - git pull origin master - - echo "Installing Composer dependencies..." - composer install --no-dev --optimize-autoloader --no-interaction - - echo "Installing NPM dependencies..." - npm ci - - echo "Building frontend assets..." - npx mix --production - - echo "Generating blocklist..." - node generateBlocklist.mjs - - echo "Clearing cache..." - # Clear cache files - sudo find bootstrap/cache -type f ! -name '.gitignore' -delete 2>/dev/null || true - sudo find storage/framework/cache -type f ! -name '.gitignore' -delete 2>/dev/null || true - sudo find storage/framework/sessions -type f ! -name '.gitignore' -delete 2>/dev/null || true - sudo find storage/framework/views -type f ! -name '.gitignore' -delete 2>/dev/null || true - - echo "Setting proper file permissions..." - # Set ownership for application files - sudo chown -R www-data:www-data $PRODUCTION_PATH - - # Secure .env file permissions - sudo chmod 600 config/.env 2>/dev/null || true - - # Make writable directories - sudo chmod -R 775 storage bootstrap/cache userfiles public/userfiles 2>/dev/null || true - - echo "Reloading services..." - # Reload PHP-FPM to clear opcache - sudo systemctl reload php8.2-fpm - - # Reload nginx - sudo systemctl reload nginx - - echo "Production deployment completed successfully!" - echo "Commit: $(git rev-parse --short HEAD)" - echo "Rollback available: git reset --hard $BACKUP_TAG" - ENDSSH - - - name: Health Check Production - if: github.ref == 'refs/heads/master' - env: - PRODUCTION_URL: ${{ secrets.PRODUCTION_URL }} - run: | - echo " Running production health check..." - sleep 5 - - response=$(curl -s -o /dev/null -w "%{http_code}" $PRODUCTION_URL/healthCheck.php 2>/dev/null || echo "000") - - if [ "$response" = "200" ]; then - echo " Production health check passed! (HTTP $response)" - else - echo " ERROR: Production health check failed! (HTTP $response)" - echo " Please verify the deployment manually at $PRODUCTION_URL" - exit 1 - fi - - - name: Deployment Summary - if: always() - run: | - echo "" - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - if [ "${{ github.ref }}" = "refs/heads/develop" ]; then - echo " STAGING Deployment Summary" - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo "Branch: develop" - echo "Environment: Staging" - echo "Server: ${{ secrets.SSH_HOST }}" - echo "Path: ${{ secrets.STAGING_PATH }}" - echo "URL: ${{ secrets.STAGING_URL }}" - else - echo " PRODUCTION Deployment Summary" - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo "Branch: master" - echo "Environment: Production" - echo "Server: ${{ secrets.SSH_HOST }}" - echo "Path: ${{ secrets.PRODUCTION_PATH }}" - echo "URL: ${{ secrets.PRODUCTION_URL }}" - fi - echo "Commit: ${{ github.sha }}" - echo "Actor: ${{ github.actor }}" - echo "Status: ${{ job.status }}" - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - diff --git a/.github/workflows/docker-production-deployment.yml b/.github/workflows/docker-production-deployment.yml new file mode 100644 index 0000000000..36e3e52f52 --- /dev/null +++ b/.github/workflows/docker-production-deployment.yml @@ -0,0 +1,305 @@ +name: Deploy Leantime to Production + +on: + push: + branches: + - master + workflow_dispatch: + +env: + IMAGE_NAME: leantime-prod + IMAGE_TAG: prod-${{ github.sha }} + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build Docker image + uses: docker/build-push-action@v5 + with: + context: . + push: false + load: true + platforms: linux/arm64 + cache-from: type=gha + cache-to: type=gha,mode=max + tags: | + ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} + + - name: Install docker-pussh plugin + run: | + mkdir -p ~/.docker/cli-plugins + curl -sSL https://raw.githubusercontent.com/psviderski/unregistry/main/docker-pussh \ + -o ~/.docker/cli-plugins/docker-pussh + chmod +x ~/.docker/cli-plugins/docker-pussh + echo "Plugin installed" + + - name: Setup SSH key + run: | + mkdir -p ~/.ssh + echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/production_key + chmod 600 ~/.ssh/production_key + ssh-keyscan -p ${{ secrets.SERVER_SSH_PORT || 22 }} ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts + + - name: Push image to server + run: | + echo "Pushing image to server..." + docker pussh ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} \ + ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:${{ secrets.SERVER_SSH_PORT || 22 }} \ + -i ~/.ssh/production_key + echo "Image pushed successfully!" + + - name: Prepare server directory + run: | + ssh -p ${{ secrets.SERVER_SSH_PORT || 22 }} \ + -i ~/.ssh/production_key \ + ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} \ + "sudo chown -R \$USER:\$USER ${{ secrets.DOCKER_APP_PROD_PATH }} && \ + sudo chmod -R 755 ${{ secrets.DOCKER_APP_PROD_PATH }}" + + - name: Upload docker-compose.production.yml to server + run: | + scp -P ${{ secrets.SERVER_SSH_PORT || 22 }} \ + -i ~/.ssh/production_key \ + docker-compose.production.yml \ + ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:${{ secrets.DOCKER_APP_PROD_PATH }} + + - name: Deploy to Production + id: deploy + continue-on-error: true + run: | + ssh -p ${{ secrets.SERVER_SSH_PORT || 22 }} \ + -i ~/.ssh/production_key \ + ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} bash << 'ENDSSH' + set -e + + IMAGE_NAME="${{ env.IMAGE_NAME }}" + NEW_IMAGE_TAG="${{ env.IMAGE_TAG }}" + APP_DIR="${{ secrets.DOCKER_APP_PROD_PATH }}" + CONTAINER_NAME="leantime_app_prod" + TIMESTAMP=$(date +%Y%m%d_%H%M%S) + + echo "======================================" + echo "[INFO] Starting deployment to production..." + echo "Directory: ${APP_DIR}" + echo "Image: ${IMAGE_NAME}:${NEW_IMAGE_TAG}" + echo "======================================" + + cd "${APP_DIR}" + + if [ ! -f ".env" ]; then + echo "[ERROR] .env file not found! Cannot deploy." + exit 1 + fi + + CURRENT_IMAGE_TAG=$(docker inspect ${CONTAINER_NAME} --format '{{.Config.Image}}' 2>/dev/null | cut -d':' -f2) + echo "${CURRENT_IMAGE_TAG}" > "${APP_DIR}/.previous_image_tag.txt" + + if [ -f "docker-compose.production.yml" ]; then + cp docker-compose.production.yml "docker-compose.production.yml.backup.${TIMESTAMP}" + echo "[INFO] Backed up docker-compose.production.yml" + fi + + if [ -f ".env" ]; then + cp .env ".env.backup.${TIMESTAMP}" + echo "[INFO] Backed up .env" + fi + + echo "[INFO] Removing app container..." + docker compose -f docker-compose.production.yml rm -f -s leantime || true + + echo "[DEBUG] Updating image to: ${IMAGE_NAME}:${NEW_IMAGE_TAG}" + sed -i "s|image: ${IMAGE_NAME}:.*|image: ${IMAGE_NAME}:${NEW_IMAGE_TAG}|" docker-compose.production.yml + echo "[DEBUG] Updated docker-compose.production.yml" + + echo "[INFO] Deploying new version..." + docker compose -f docker-compose.production.yml up -d leantime + + echo "[INFO] Waiting for application to become healthy..." + HEALTH_PASSED=false + for i in {1..30}; do + # Check if container is running first + if ! docker ps | grep -q "${CONTAINER_NAME}"; then + echo "[ERROR] Container ${CONTAINER_NAME} is not running!" + docker compose -f docker-compose.production.yml logs --tail=50 leantime + exit 1 + fi + + if docker exec "${CONTAINER_NAME}" curl -f http://localhost:8080 >/dev/null 2>&1; then + echo "[SUCCESS] Application is healthy! (attempt $i)" + HEALTH_PASSED=true + break + fi + + echo "[INFO] Waiting for application to become healthy... (attempt $i/30)" + sleep 2 + done + + if [ "${HEALTH_PASSED}" = false ]; then + echo "[ERROR] Application failed to start within 60 seconds" + docker compose -f docker-compose.production.yml logs --tail=50 leantime + exit 1 + fi + + echo "${IMAGE_NAME}:${NEW_IMAGE_TAG}" > .current_image_tag + + echo "======================================" + echo "[SUCCESS] Deployment successful!" + echo "======================================" + docker compose -f docker-compose.production.yml ps + ENDSSH + + - name: Rollback on Failure + if: steps.deploy.outcome == 'failure' + run: | + ssh -p ${{ secrets.SERVER_SSH_PORT || 22 }} \ + -i ~/.ssh/production_key \ + ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} bash << 'ENDSSH' + set -e + + IMAGE_NAME="${{ env.IMAGE_NAME }}" + APP_DIR="${{ secrets.DOCKER_APP_PROD_PATH }}" + CONTAINER_NAME="leantime_app_prod" + + echo "[ERROR] Deployment failed! Initiating rollback..." + cd "${APP_DIR}" + + PREVIOUS_IMAGE_TAG=$(cat "${APP_DIR}/.previous_image_tag.txt" 2>/dev/null || echo "") + + if [ -z "${PREVIOUS_IMAGE_TAG}" ] || [ "${PREVIOUS_IMAGE_TAG}" = "none" ]; then + echo "[WARN] No previous image to rollback to" + docker compose -f docker-compose.production.yml stop leantime + exit 1 + fi + + echo "[INFO] Rolling back to: ${IMAGE_NAME}:${PREVIOUS_IMAGE_TAG}" + + LATEST_ENV_BACKUP=$(ls -t .env.backup.* 2>/dev/null | head -n 1) + if [ -n "${LATEST_ENV_BACKUP}" ]; then + cp "${LATEST_ENV_BACKUP}" .env + echo "[INFO] Restored .env from backup" + fi + + echo "[INFO] Updating image to: ${IMAGE_NAME}:${PREVIOUS_IMAGE_TAG}" + sed -i "s|image: ${IMAGE_NAME}:.*|image: ${IMAGE_NAME}:${PREVIOUS_IMAGE_TAG}|" docker-compose.production.yml + echo "[INFO] Updated docker-compose.production.yml to previous version" + + echo "[INFO] Removing failed container..." + docker compose -f docker-compose.production.yml rm -f -s leantime + + echo "[INFO] Starting previous version..." + docker compose -f docker-compose.production.yml up -d leantime + + echo "[INFO] Waiting for rollback to complete..." + ROLLBACK_SUCCESS=false + for i in {1..30}; do + if docker ps | grep -q "${CONTAINER_NAME}"; then + if docker exec "${CONTAINER_NAME}" curl -f http://localhost:8080 >/dev/null 2>&1; then + echo "[INFO] Application is healthy after rollback (attempt $i)" + ROLLBACK_SUCCESS=true + break + fi + fi + echo "[INFO] Waiting for rollback... (attempt $i/30)" + sleep 2 + done + + if [ "${ROLLBACK_SUCCESS}" = false ]; then + echo "[ERROR] Rollback failed! Manual intervention required." + docker compose -f docker-compose.production.yml logs --tail=50 leantime + exit 1 + fi + + echo "[SUCCESS] Rollback successful!" + docker compose -f docker-compose.production.yml ps + ENDSSH + + - name: Post-Rollback Cleanup + if: steps.deploy.outcome == 'failure' + run: | + ssh -p ${{ secrets.SERVER_SSH_PORT || 22 }} \ + -i ~/.ssh/production_key \ + ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} bash << 'ENDSSH' + + echo "[INFO] Cleaning up failed deployment image..." + docker rmi -f ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} 2>/dev/null || true + docker image prune -f + ENDSSH + + - name: Final Verification + if: success() + run: | + ssh -p ${{ secrets.SERVER_SSH_PORT || 22 }} \ + -i ~/.ssh/production_key \ + ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} bash << 'ENDSSH' + APP_DIR="${{ secrets.DOCKER_APP_PROD_PATH }}" + CONTAINER_NAME="leantime_app_prod" + + cd "${APP_DIR}" + sync + sleep 2 + if [ ! -r "docker-compose.production.yml" ]; then + echo "[ERROR] docker-compose.production.yml not readable!" + exit 1 + fi + + echo "[INFO] Final deployment verification..." + docker compose -f docker-compose.production.yml ps + + echo "" + echo "[INFO] Container status:" + docker ps | grep "${CONTAINER_NAME}" || echo "Container not found" + + echo "" + echo "[INFO] Testing endpoint..." + curl -I https://pearlog-prod.pearshadow.com || echo "Endpoint test failed" + + echo "" + echo "[SUCCESS] Verification complete!" + ENDSSH + + - name: Cleanup Old Images + if: success() + run: | + ssh -p ${{ secrets.SERVER_SSH_PORT || 22 }} \ + -i ~/.ssh/production_key \ + ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} bash << 'ENDSSH' + set -e + + IMAGE_NAME="${{ env.IMAGE_NAME }}" + APP_DIR="${{ secrets.DOCKER_APP_PROD_PATH }}" + + echo "[INFO] Cleaning up old Docker images..." + cd "${APP_DIR}" + + docker images "${IMAGE_NAME}" --format "{{.Repository}}:{{.Tag}} {{.ID}}" | \ + grep "prod-" | \ + tail -n +4 | \ + awk '{print $2}' | \ + xargs -r docker rmi -f 2>/dev/null || true + + docker image prune -f + + echo "[INFO] Cleaning up old backups..." + find "${APP_DIR}" -name "*.backup.*" -type f -mtime +7 -delete + echo "[INFO] Backups older than 7 days removed" + + echo "[INFO] Cleanup complete!" + docker images "${IMAGE_NAME}" + ENDSSH + + - name: Notify on Failure + if: failure() + run: | + echo "======================================" + echo "Deployment to production failed!" + echo "Check logs for details." + echo "If rollback occurred, the previous version should still be running." + echo "======================================" \ No newline at end of file diff --git a/.github/workflows/docker-staging-deployment.yml b/.github/workflows/docker-staging-deployment.yml new file mode 100644 index 0000000000..932f6c4569 --- /dev/null +++ b/.github/workflows/docker-staging-deployment.yml @@ -0,0 +1,305 @@ +name: Deploy Leantime to Staging + +on: + push: + branches: + - develop + workflow_dispatch: + +env: + IMAGE_NAME: leantime-staging + IMAGE_TAG: staging-${{ github.sha }} + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build Docker image + uses: docker/build-push-action@v5 + with: + context: . + push: false + load: true + platforms: linux/arm64 + cache-from: type=gha + cache-to: type=gha,mode=max + tags: | + ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} + + - name: Install docker-pussh plugin + run: | + mkdir -p ~/.docker/cli-plugins + curl -sSL https://raw.githubusercontent.com/psviderski/unregistry/main/docker-pussh \ + -o ~/.docker/cli-plugins/docker-pussh + chmod +x ~/.docker/cli-plugins/docker-pussh + echo "Plugin installed" + + - name: Setup SSH key + run: | + mkdir -p ~/.ssh + echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/staging_key + chmod 600 ~/.ssh/staging_key + ssh-keyscan -p ${{ secrets.SERVER_SSH_PORT || 22 }} ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts + + - name: Push image to server + run: | + echo "Pushing image to server..." + docker pussh ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} \ + ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:${{ secrets.SERVER_SSH_PORT || 22 }} \ + -i ~/.ssh/staging_key + echo "Image pushed successfully!" + + - name: Prepare server directory + run: | + ssh -p ${{ secrets.SERVER_SSH_PORT || 22 }} \ + -i ~/.ssh/staging_key \ + ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} \ + "sudo chown -R \$USER:\$USER ${{ secrets.DOCKER_APP_STAGING_PATH }} && \ + sudo chmod -R 755 ${{ secrets.DOCKER_APP_STAGING_PATH }}" + + - name: Upload docker-compose.staging.yml to server + run: | + scp -P ${{ secrets.SERVER_SSH_PORT || 22 }} \ + -i ~/.ssh/staging_key \ + docker-compose.staging.yml \ + ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:${{ secrets.DOCKER_APP_STAGING_PATH }} + + - name: Deploy to Staging + id: deploy + continue-on-error: true + run: | + ssh -p ${{ secrets.SERVER_SSH_PORT || 22 }} \ + -i ~/.ssh/staging_key \ + ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} bash << 'ENDSSH' + set -e + + IMAGE_NAME="${{ env.IMAGE_NAME }}" + NEW_IMAGE_TAG="${{ env.IMAGE_TAG }}" + APP_DIR="${{ secrets.DOCKER_APP_STAGING_PATH }}" + CONTAINER_NAME="leantime_app_local" + TIMESTAMP=$(date +%Y%m%d_%H%M%S) + + echo "======================================" + echo "[INFO] Starting deployment to staging..." + echo "Directory: ${APP_DIR}" + echo "Image: ${IMAGE_NAME}:${NEW_IMAGE_TAG}" + echo "======================================" + + cd "${APP_DIR}" + + if [ ! -f ".env" ]; then + echo "[ERROR] .env file not found! Cannot deploy." + exit 1 + fi + + CURRENT_IMAGE_TAG=$(docker inspect ${CONTAINER_NAME} --format '{{.Config.Image}}' 2>/dev/null | cut -d':' -f2) + echo "${CURRENT_IMAGE_TAG}" > "${APP_DIR}/.previous_image_tag.txt" + + if [ -f "docker-compose.staging.yml" ]; then + cp docker-compose.staging.yml "docker-compose.staging.yml.backup.${TIMESTAMP}" + echo "[INFO] Backed up docker-compose.staging.yml" + fi + + if [ -f ".env" ]; then + cp .env ".env.backup.${TIMESTAMP}" + echo "[INFO] Backed up .env" + fi + + echo "[INFO] Removing app container..." + docker compose -f docker-compose.staging.yml rm -f -s leantime || true + + echo "[DEBUG] Updating image to: ${IMAGE_NAME}:${NEW_IMAGE_TAG}" + sed -i "s|image: leantime-staging:.*|image: ${IMAGE_NAME}:${NEW_IMAGE_TAG}|" docker-compose.staging.yml + echo "[DEBUG] Updated docker-compose.staging.yml" + + echo "[INFO] Deploying new version..." + docker compose -f docker-compose.staging.yml up -d leantime + + echo "[INFO] Waiting for application to become healthy..." + HEALTH_PASSED=false + for i in {1..30}; do + # Check if container is running first + if ! docker ps | grep -q "${CONTAINER_NAME}"; then + echo "[ERROR] Container ${CONTAINER_NAME} is not running!" + docker compose -f docker-compose.staging.yml logs --tail=50 leantime + exit 1 + fi + + if docker exec "${CONTAINER_NAME}" curl -f http://localhost:8080 >/dev/null 2>&1; then + echo "[SUCCESS] Application is healthy! (attempt $i)" + HEALTH_PASSED=true + break + fi + + echo "[INFO] Waiting for application to become healthy... (attempt $i/30)" + sleep 2 + done + + if [ "${HEALTH_PASSED}" = false ]; then + echo "[ERROR] Application failed to start within 60 seconds" + docker compose -f docker-compose.staging.yml logs --tail=50 leantime + exit 1 + fi + + echo "${IMAGE_NAME}:${NEW_IMAGE_TAG}" > .current_image_tag + + echo "======================================" + echo "[SUCCESS] Deployment successful!" + echo "======================================" + docker compose -f docker-compose.staging.yml ps + ENDSSH + + - name: Rollback on Failure + if: steps.deploy.outcome == 'failure' + run: | + ssh -p ${{ secrets.SERVER_SSH_PORT || 22 }} \ + -i ~/.ssh/staging_key \ + ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} bash << 'ENDSSH' + set -e + + IMAGE_NAME="${{ env.IMAGE_NAME }}" + APP_DIR="${{ secrets.DOCKER_APP_STAGING_PATH }}" + CONTAINER_NAME="leantime_app_local" + + echo "[ERROR] Deployment failed! Initiating rollback..." + cd "${APP_DIR}" + + PREVIOUS_IMAGE_TAG=$(cat "${APP_DIR}/.previous_image_tag.txt" 2>/dev/null || echo "") + + if [ -z "${PREVIOUS_IMAGE_TAG}" ] || [ "${PREVIOUS_IMAGE_TAG}" = "none" ]; then + echo "[WARN] No previous image to rollback to" + docker compose -f docker-compose.staging.yml stop leantime + exit 1 + fi + + echo "[INFO] Rolling back to: ${IMAGE_NAME}:${PREVIOUS_IMAGE_TAG}" + + LATEST_ENV_BACKUP=$(ls -t .env.backup.* 2>/dev/null | head -n 1) + if [ -n "${LATEST_ENV_BACKUP}" ]; then + cp "${LATEST_ENV_BACKUP}" .env + echo "[INFO] Restored .env from backup" + fi + + echo "[INFO] Updating image to: ${IMAGE_NAME}:${PREVIOUS_IMAGE_TAG}" + sed -i "s|image: leantime-staging:.*|image: ${IMAGE_NAME}:${PREVIOUS_IMAGE_TAG}|" docker-compose.staging.yml + echo "[INFO] Updated docker-compose.staging.yml to previous version" + + echo "[INFO] Removing failed container..." + docker compose -f docker-compose.staging.yml rm -f -s leantime + + echo "[INFO] Starting previous version..." + docker compose -f docker-compose.staging.yml up -d leantime + + echo "[INFO] Waiting for rollback to complete..." + ROLLBACK_SUCCESS=false + for i in {1..30}; do + if docker ps | grep -q "${CONTAINER_NAME}"; then + if docker exec "${CONTAINER_NAME}" curl -f http://localhost:8080 >/dev/null 2>&1; then + echo "[INFO] Application is healthy after rollback (attempt $i)" + ROLLBACK_SUCCESS=true + break + fi + fi + echo "[INFO] Waiting for rollback... (attempt $i/30)" + sleep 2 + done + + if [ "${ROLLBACK_SUCCESS}" = false ]; then + echo "[ERROR] Rollback failed! Manual intervention required." + docker compose -f docker-compose.staging.yml logs --tail=50 leantime + exit 1 + fi + + echo "[SUCCESS] Rollback successful!" + docker compose -f docker-compose.staging.yml ps + ENDSSH + + - name: Post-Rollback Cleanup + if: steps.deploy.outcome == 'failure' + run: | + ssh -p ${{ secrets.SERVER_SSH_PORT || 22 }} \ + -i ~/.ssh/staging_key \ + ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} bash << 'ENDSSH' + + echo "[INFO] Cleaning up failed deployment image..." + docker rmi -f ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} 2>/dev/null || true + docker image prune -f + ENDSSH + + - name: Final Verification + if: success() + run: | + ssh -p ${{ secrets.SERVER_SSH_PORT || 22 }} \ + -i ~/.ssh/staging_key \ + ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} bash << 'ENDSSH' + APP_DIR="${{ secrets.DOCKER_APP_STAGING_PATH }}" + CONTAINER_NAME="leantime_app_local" + + cd "${APP_DIR}" + sync + sleep 2, + if [ ! -r "docker-compose.staging.yml" ]; then + echo "[ERROR] docker-compose.staging.yml not readable!" + exit 1 + fi + + echo "[INFO] Final deployment verification..." + docker compose -f docker-compose.staging.yml ps + + echo "" + echo "[INFO] Container status:" + docker ps | grep "${CONTAINER_NAME}" || echo "Container not found" + + echo "" + echo "[INFO] Testing endpoint..." + curl -I https://staging-pearlog.pearshadow.com || echo "Endpoint test failed" + + echo "" + echo "[SUCCESS] Verification complete!" + ENDSSH + + - name: Cleanup Old Images + if: success() + run: | + ssh -p ${{ secrets.SERVER_SSH_PORT || 22 }} \ + -i ~/.ssh/staging_key \ + ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} bash << 'ENDSSH' + set -e + + IMAGE_NAME="${{ env.IMAGE_NAME }}" + APP_DIR="${{ secrets.DOCKER_APP_STAGING_PATH }}" + + echo "[INFO] Cleaning up old Docker images..." + cd "${APP_DIR}" + + docker images "${IMAGE_NAME}" --format "{{.Repository}}:{{.Tag}} {{.ID}}" | \ + grep "staging-" | \ + tail -n +4 | \ + awk '{print $2}' | \ + xargs -r docker rmi -f 2>/dev/null || true + + docker image prune -f + + echo "[INFO] Cleaning up old backups..." + find "${APP_DIR}" -name "*.backup.*" -type f -mtime +7 -delete + echo "[INFO] Backups older than 7 days removed" + + echo "[INFO] Cleanup complete!" + docker images "${IMAGE_NAME}" + ENDSSH + + - name: Notify on Failure + if: failure() + run: | + echo "======================================" + echo "Deployment to staging failed!" + echo "Check logs for details." + echo "If rollback occurred, the previous version should still be running." + echo "======================================" \ No newline at end of file diff --git a/.gitignore b/.gitignore index 8c39c258ee..a9fc388eac 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,8 @@ backupdb/* public/js/*.min.js public/js/*.map public/css/*.css +public/dist/* +.docker/ # PHPStorm specific git ignore to be able to share inspection configs and run tools # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..f818cf71aa --- /dev/null +++ b/Dockerfile @@ -0,0 +1,431 @@ +FROM php:8.2-fpm-alpine AS composer-builder + +WORKDIR /build + +COPY --from=composer:2 /usr/bin/composer /usr/bin/composer + +RUN apk add --no-cache \ + freetype \ + libjpeg-turbo \ + libpng \ + libwebp \ + libzip \ + openldap \ + && apk add --no-cache --virtual .build-deps \ + freetype-dev \ + libjpeg-turbo-dev \ + libpng-dev \ + libwebp-dev \ + libzip-dev \ + openldap-dev \ + && docker-php-ext-configure gd \ + --with-freetype \ + --with-jpeg \ + --with-webp \ + && docker-php-ext-install -j$(nproc) \ + gd \ + bcmath \ + pdo_mysql \ + mysqli \ + zip \ + ldap \ + pcntl \ + && apk del .build-deps + +COPY composer.json composer.lock ./ + +RUN composer install \ + --no-dev \ + --optimize-autoloader \ + --no-interaction \ + --no-progress \ + --prefer-dist + +FROM node:18-alpine AS frontend + +WORKDIR /app + +COPY package.json package-lock.json ./ + +RUN npm ci + +COPY webpack.mix.js .babelrc ./ +COPY tailwind.config.js ./ + +COPY app ./app +COPY public ./public + +COPY generateBlocklist.mjs ./ + +RUN mkdir -p ./storage/framework/cache ./bootstrap/cache && \ + rm -rf ./bootstrap/cache/*.php || true + +RUN npx mix --production + +RUN if [ -f "generateBlocklist.mjs" ]; then \ + node generateBlocklist.mjs; \ + else \ + echo '{}' > blocklist.json; \ + fi + +RUN echo "=== Frontend Build Verification ===" && \ + echo "=== Contents of public/dist/ ===" && \ + ls -lah public/dist/ && \ + echo "" && \ + echo "=== JavaScript files ===" && \ + ls -lah public/dist/js/ && \ + echo "" && \ + echo "=== CSS files ===" && \ + ls -lah public/dist/css/ && \ + echo "" && \ + echo "=== mix-manifest.json ===" && \ + cat public/dist/mix-manifest.json && \ + echo "" && \ + if [ ! -d "public/dist/js" ]; then \ + echo "ERROR: Frontend build failed - public/dist/js not found!"; \ + exit 1; \ + fi && \ + if [ ! -d "public/dist/css" ]; then \ + echo "ERROR: Frontend build failed - public/dist/css not found!"; \ + exit 1; \ + fi && \ + echo "✓ Frontend assets built successfully" + + +FROM php:8.2-fpm-alpine + +RUN apk add --no-cache \ + nginx \ + supervisor \ + mysql-client \ + freetype \ + libjpeg-turbo \ + libpng \ + libwebp \ + libzip \ + icu-libs \ + libxml2 \ + oniguruma \ + openldap \ + font-noto-emoji \ + && apk add --no-cache --virtual .build-deps \ + freetype-dev \ + libjpeg-turbo-dev \ + libpng-dev \ + libwebp-dev \ + libzip-dev \ + icu-dev \ + libxml2-dev \ + oniguruma-dev \ + openldap-dev \ + && docker-php-ext-configure gd \ + --with-freetype \ + --with-jpeg \ + --with-webp \ + && docker-php-ext-install -j$(nproc) \ + gd \ + pdo_mysql \ + mysqli \ + zip \ + intl \ + opcache \ + bcmath \ + exif \ + mbstring \ + xml \ + ldap \ + pcntl \ + && apk del .build-deps \ + && rm -rf /var/cache/apk/* + +COPY --chown=www-data:www-data </dev/null || echo "0") && \ + echo "✓ compiled-app.js size: $APP_SIZE bytes" && \ + CSS_COUNT=$(find public/dist/css -name "*.css" 2>/dev/null | wc -l) && \ + echo "✓ CSS files found: $CSS_COUNT" && \ + if [ "$APP_SIZE" -lt "100000" ]; then \ + echo "ERROR: compiled-app.js is too small ($APP_SIZE bytes)!"; \ + exit 1; \ + fi && \ + if [ "$CSS_COUNT" -eq "0" ]; then \ + echo "ERROR: No CSS files found!"; \ + exit 1; \ + fi && \ + echo "✓ All assets verified successfully!" + +RUN mkdir -p \ + storage/framework/cache \ + storage/framework/sessions \ + storage/framework/views \ + storage/logs \ + bootstrap/cache \ + userfiles \ + public/userfiles \ + && chown -R www-data:www-data \ + storage \ + bootstrap/cache \ + userfiles \ + public/userfiles \ + && chmod -R 775 \ + storage \ + bootstrap/cache \ + userfiles \ + public/userfiles + +COPY --chown=root:root <<'EOF' /entrypoint.sh +#!/bin/sh +set -e + +mkdir -p /var/lib/nginx/tmp/client_body +chown -R www-data:www-data /var/lib/nginx +chmod -R 755 /var/lib/nginx + +chown -R www-data:www-data /var/www/html/userfiles /var/www/html/public/userfiles +chmod -R 775 /var/www/html/userfiles /var/www/html/public/userfiles + +echo "Starting Leantime container..." + +if [ -n "$LEAN_DB_HOST" ]; then + echo "Waiting for database at $LEAN_DB_HOST:${LEAN_DB_PORT:-3306}..." + timeout=60 + while ! nc -z "$LEAN_DB_HOST" "${LEAN_DB_PORT:-3306}" 2>/dev/null; do + timeout=$((timeout - 1)) + if [ $timeout -le 0 ]; then + echo "ERROR: Database connection timeout" + exit 1 + fi + echo "Waiting for database... ($timeout seconds remaining)" + sleep 1 + done + echo "Database is ready!" +fi + +if [ ! -f /var/www/html/config/.env ]; then + echo "Creating config/.env file..." + + cat > /var/www/html/config/.env </dev/null || true +find /var/www/html/storage/framework/cache -type d -empty -delete 2>/dev/null || true + +find /var/www/html/storage/framework/sessions -type f ! -name '.gitignore' -delete 2>/dev/null || true +find /var/www/html/storage/framework/sessions -type d -empty -delete 2>/dev/null || true + +find /var/www/html/storage/framework/views -type f ! -name '.gitignore' -delete 2>/dev/null || true +find /var/www/html/storage/framework/views -type d -empty -delete 2>/dev/null || true + +echo "Cache cleared successfully!" + +echo "=== Pre-startup Asset Check ===" +ls -lah /var/www/html/public/dist/js/compiled-app.*.min.js || echo "WARNING: compiled-app.js not found!" +ls -lah /var/www/html/public/dist/css/ || echo "WARNING: CSS directory not found!" +echo "================================" + +echo "Leantime initialization complete!" +echo "Starting services..." + +exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf +EOF + +RUN sed -i 's/\r$//' /entrypoint.sh && \ + chmod +x /entrypoint.sh + +RUN mkdir -p /var/log/supervisor + +EXPOSE 8080 + +HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:8080/healthCheck.php || exit 1 + +ENTRYPOINT ["/entrypoint.sh"] \ No newline at end of file diff --git a/app/Domain/Api/Controllers/Notifications.php b/app/Domain/Api/Controllers/Notifications.php index c6b6903f22..64430492b3 100644 --- a/app/Domain/Api/Controllers/Notifications.php +++ b/app/Domain/Api/Controllers/Notifications.php @@ -45,7 +45,8 @@ public function post(array $params): Response public function patch(array $params): Response { if (isset($params['action']) && $params['action'] == 'read') { - $this->notificationService->markNotificationRead($params['id'], session('userdata.id')); + $type = $params['type'] ?? null; + $this->notificationService->markNotificationRead($params['id'], session('userdata.id'), $type); } return new Response; diff --git a/app/Domain/Menu/Templates/headMenu.blade.php b/app/Domain/Menu/Templates/headMenu.blade.php index 442781fd9c..266940181d 100644 --- a/app/Domain/Menu/Templates/headMenu.blade.php +++ b/app/Domain/Menu/Templates/headMenu.blade.php @@ -63,31 +63,16 @@ class="dropdown-toggle profileHandler notificationHandler" data-tippy-content='{{ __('popover.notifications') }}' > - @if($newNotificationCount>0) - {{ $newNotificationCount }} + @if($totalNewNotifications>0) + {{ $totalNewNotifications }} @endif + + + +
  • + + + @if($totalNewMentions > 0) + {{ $totalNewMentions }} + @endif + + +