5e35fefd69
API CI/CD / Validate (composer + pint) (push) Successful in 2m47s
API CI/CD / Test (PHPUnit) (push) Failing after 3m8s
API CI/CD / Build frontend assets (push) Failing after 5m22s
API CI/CD / Security audit (push) Failing after 34s
API CI/CD / Deploy to shared hosting (PHP) (push) Has been skipped
254 lines
6.5 KiB
YAML
254 lines
6.5 KiB
YAML
# GitLab CI for Laravel 12 API
|
|
workflow:
|
|
rules:
|
|
- if: '$CI_PIPELINE_SOURCE == "push"'
|
|
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
|
- if: '$CI_COMMIT_TAG'
|
|
|
|
stages:
|
|
- validate
|
|
- test
|
|
- build
|
|
- security
|
|
- deploy
|
|
|
|
variables:
|
|
APP_ENV: testing
|
|
APP_DEBUG: "false"
|
|
LOG_CHANNEL: stderr
|
|
CACHE_DRIVER: array
|
|
SESSION_DRIVER: array
|
|
QUEUE_CONNECTION: sync
|
|
DB_CONNECTION: sqlite
|
|
DB_DATABASE: "$CI_PROJECT_DIR/database/database.sqlite"
|
|
COMPOSER_CACHE_DIR: "$CI_PROJECT_DIR/.composer-cache"
|
|
NPM_CONFIG_CACHE: "$CI_PROJECT_DIR/.npm-cache"
|
|
|
|
cache:
|
|
key:
|
|
files:
|
|
- composer.lock
|
|
- package-lock.json
|
|
paths:
|
|
- .composer-cache/
|
|
- .npm-cache/
|
|
- vendor/
|
|
- node_modules/
|
|
|
|
.php_laravel_job:
|
|
image: php:8.3-cli
|
|
before_script:
|
|
- apt-get update
|
|
- apt-get install -y --no-install-recommends git unzip curl libzip-dev libpng-dev libonig-dev libxml2-dev libsqlite3-dev
|
|
- docker-php-ext-install mbstring zip gd pdo_sqlite
|
|
- php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
|
|
- php composer-setup.php --install-dir=/usr/local/bin --filename=composer
|
|
- rm composer-setup.php
|
|
- composer install --prefer-dist --no-interaction --no-progress
|
|
# Install Pint if not present
|
|
- composer require --dev laravel/pint --ignore-platform-reqs || true
|
|
# Generate .env with real keys
|
|
- |
|
|
cat > .env <<EOF
|
|
APP_NAME=school_api
|
|
APP_ENV=testing
|
|
APP_KEY=base64:MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI=
|
|
APP_DEBUG=false
|
|
APP_URL=http://localhost
|
|
|
|
LOG_CHANNEL=stderr
|
|
LOG_LEVEL=debug
|
|
|
|
DB_CONNECTION=sqlite
|
|
DB_DATABASE=${CI_PROJECT_DIR}/database/database.sqlite
|
|
|
|
CACHE_DRIVER=array
|
|
SESSION_DRIVER=array
|
|
QUEUE_CONNECTION=sync
|
|
MAIL_MAILER=array
|
|
BROADCAST_CONNECTION=log
|
|
FILESYSTEM_DISK=local
|
|
|
|
JWT_SECRET=testing_jwt_secret_ci_not_for_production_32b
|
|
EOF
|
|
- mkdir -p database storage/framework/cache storage/framework/sessions storage/framework/views storage/logs bootstrap/cache reports
|
|
- touch database/database.sqlite
|
|
- php artisan key:generate --force
|
|
- php artisan jwt:secret --force || true # If JWT package installed
|
|
- php artisan config:clear
|
|
- php artisan view:clear
|
|
|
|
composer_validate:
|
|
extends: .php_laravel_job
|
|
stage: validate
|
|
script:
|
|
- mkdir -p reports
|
|
- composer validate --strict | tee reports/composer-validate.log
|
|
- php -v | tee reports/php-version.log
|
|
- php artisan --version | tee reports/artisan-version.log
|
|
artifacts:
|
|
when: always
|
|
expire_in: 7 days
|
|
paths:
|
|
- reports/
|
|
- storage/logs/
|
|
|
|
laravel_pint:
|
|
extends: .php_laravel_job
|
|
stage: validate
|
|
script:
|
|
- mkdir -p reports
|
|
- vendor/bin/pint --test --format=json | tee reports/pint.json
|
|
artifacts:
|
|
when: always
|
|
expire_in: 7 days
|
|
paths:
|
|
- reports/
|
|
- storage/logs/
|
|
allow_failure: true
|
|
|
|
phpunit:
|
|
extends: .php_laravel_job
|
|
stage: test
|
|
parallel:
|
|
matrix:
|
|
- TEST_SUITE: [Unit, Feature]
|
|
script:
|
|
- mkdir -p reports
|
|
- php artisan migrate --force | tee reports/migrate.log
|
|
- php artisan test --testsuite=$TEST_SUITE --coverage-text --log-junit reports/phpunit.xml | tee reports/phpunit.log
|
|
artifacts:
|
|
when: always
|
|
expire_in: 7 days
|
|
reports:
|
|
junit: reports/phpunit.xml
|
|
coverage_report:
|
|
coverage_format: cobertura
|
|
path: reports/coverage.xml
|
|
paths:
|
|
- reports/
|
|
- storage/logs/
|
|
coverage: '/^\s*Lines:\s*\d+\.\d+%/'
|
|
cache:
|
|
paths:
|
|
- .composer-cache/
|
|
- .npm-cache/
|
|
- vendor/
|
|
- node_modules/
|
|
key: ${CI_COMMIT_REF_SLUG}
|
|
|
|
build_frontend:
|
|
image: node:22-alpine
|
|
stage: build
|
|
before_script:
|
|
- npm ci --cache .npm --prefer-offline
|
|
script:
|
|
- mkdir -p reports
|
|
- npm run build 2>&1 | tee reports/npm-build.log
|
|
artifacts:
|
|
when: always
|
|
expire_in: 7 days
|
|
paths:
|
|
- public/build/
|
|
- public/css/
|
|
- public/js/
|
|
- reports/
|
|
cache:
|
|
paths:
|
|
- .npm/
|
|
|
|
composer_audit:
|
|
extends: .php_laravel_job
|
|
stage: security
|
|
script:
|
|
- mkdir -p reports
|
|
- composer audit --locked --no-interaction --format=json | tee reports/composer-audit.json
|
|
artifacts:
|
|
when: always
|
|
expire_in: 7 days
|
|
paths:
|
|
- reports/
|
|
- storage/logs/
|
|
allow_failure: true
|
|
|
|
npm_audit:
|
|
image: node:22-alpine
|
|
stage: security
|
|
before_script:
|
|
- npm ci
|
|
script:
|
|
- mkdir -p reports
|
|
- npm audit --json --audit-level=high | tee reports/npm-audit.json || true
|
|
artifacts:
|
|
when: always
|
|
expire_in: 7 days
|
|
paths:
|
|
- reports/
|
|
allow_failure: true
|
|
|
|
secret_detection_basic:
|
|
image: alpine:3.20
|
|
stage: security
|
|
before_script:
|
|
- apk add --no-cache git grep
|
|
script:
|
|
- mkdir -p reports
|
|
- |
|
|
echo "Scanning for committed secrets..." | tee reports/secret-detection.log
|
|
if grep -RInE "(APP_KEY=base64:[A-Za-z0-9+/=]{44}|DB_PASSWORD=.+|JWT_SECRET=.+|MAIL_PASSWORD=.+|PAYPAL_(CLIENT|SECRET|MODE)|AKIA[0-9A-Z]{16}|-----BEGIN (RSA|OPENSSH|PRIVATE) KEY-----)" \
|
|
--exclude-dir=.git \
|
|
--exclude-dir=vendor \
|
|
--exclude-dir=node_modules \
|
|
--exclude=.env.example \
|
|
--exclude=.env.prod.example \
|
|
--exclude=reports \
|
|
. 2>&1 | tee -a reports/secret-detection.log; then
|
|
echo "❌ Secrets detected! Check reports/secret-detection.log"
|
|
exit 1
|
|
else
|
|
echo "✅ No secrets found"
|
|
fi
|
|
artifacts:
|
|
when: always
|
|
expire_in: 7 days
|
|
paths:
|
|
- reports/
|
|
allow_failure: false
|
|
|
|
# Deploy to shared hosting (optional - add if needed)
|
|
deploy:
|
|
stage: deploy
|
|
image: alpine:latest
|
|
before_script:
|
|
- apk add --no-cache openssh-client sshpass rsync
|
|
script:
|
|
- echo "Deploying to production..."
|
|
- sshpass -p "$SSH_PASSWORD" rsync -avz --delete \
|
|
-e "ssh -p $SSH_PORT -o StrictHostKeyChecking=no" \
|
|
--exclude='.env' \
|
|
--exclude='storage/logs/*' \
|
|
--exclude='.git' \
|
|
--exclude='tests' \
|
|
--exclude='database/sqlite' \
|
|
./ $SSH_USER@$SSH_HOST:$SSH_TARGET_DIR/
|
|
environment:
|
|
name: production
|
|
url: $PRODUCTION_URL
|
|
only:
|
|
- main
|
|
- master
|
|
when: manual
|
|
needs:
|
|
- phpunit
|
|
- composer_audit
|
|
|
|
# Include GitLab native templates (ensure you have license)
|
|
include:
|
|
- template: Jobs/SAST.gitlab-ci.yml
|
|
- template: Jobs/Secret-Detection.gitlab-ci.yml
|
|
- template: Jobs/Dependency-Scanning.gitlab-ci.yml
|
|
|
|
# Optional: Run SAST only on main branch
|
|
sast:
|
|
rules:
|
|
- if: '$CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "master"' |