From b4b7aefece709e3ef99f30317f45e85b1b3c9674 Mon Sep 17 00:00:00 2001 From: root Date: Sat, 20 Jun 2026 19:16:38 -0400 Subject: [PATCH] ci: add Gitea CI/CD workflow for Laravel API Pipeline includes validate (composer + pint), test (phpunit), build (frontend assets), security audit, and manual deploy stages. --- .gitea/workflows/build.yml | 279 +++++++++++++++++++++++++++++++++++++ 1 file changed, 279 insertions(+) create mode 100644 .gitea/workflows/build.yml diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml new file mode 100644 index 00000000..8db8cce3 --- /dev/null +++ b/.gitea/workflows/build.yml @@ -0,0 +1,279 @@ +# Gitea CI/CD for Laravel 12 API +# +# Triggers: +# - Push to any branch +# - Pull requests +# - Tags +# +# Stages: validate → test → build → security + +name: API CI/CD + +on: + push: + branches: [main, master, develop, 'feature/**', 'fix/**'] + pull_request: + branches: [main, master, develop] + push: + tags: ['v*'] + +env: + APP_ENV: testing + APP_DEBUG: false + APP_KEY: "" + LOG_CHANNEL: stderr + LOG_LEVEL: debug + DB_CONNECTION: sqlite + DB_DATABASE: ${{ github.workspace }}/database/database.sqlite + CACHE_DRIVER: array + SESSION_DRIVER: array + QUEUE_CONNECTION: sync + MAIL_MAILER: array + FILESYSTEM_DISK: local + COMPOSER_CACHE_DIR: ${{ github.workspace }}/.composer-cache + +jobs: + # ────────────────────────────────────────────── + # VALIDATE: composer validate + Laravel Pint + # ────────────────────────────────────────────── + validate: + name: Validate (composer + pint) + runs-on: ubuntu-latest + container: + image: php:8.3-cli + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install system dependencies + run: | + 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 + + - name: Install Composer + run: | + 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 + + - name: Cache Composer dependencies + uses: actions/cache@v4 + with: + path: | + vendor/ + .composer-cache/ + key: ${{ runner.os }}-composer-${{ hashFiles('composer.lock') }} + restore-keys: | + ${{ runner.os }}-composer- + + - name: Install PHP dependencies + run: composer install --prefer-dist --no-interaction --no-progress + + - name: Composer validate + run: composer validate --strict + + - name: Laravel Pint (code style check) + run: | + composer require --dev laravel/pint --ignore-platform-reqs || true + vendor/bin/pint --test || echo "Pint found style issues (non-blocking)" + + # ────────────────────────────────────────────── + # TEST: PHPUnit (Unit + Feature with SQLite) + # ────────────────────────────────────────────── + test: + name: Test (PHPUnit) + runs-on: ubuntu-latest + container: + image: php:8.3-cli + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install system dependencies + run: | + 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 + + - name: Install Composer + run: | + 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 + + - name: Cache Composer dependencies + uses: actions/cache@v4 + with: + path: | + vendor/ + .composer-cache/ + key: ${{ runner.os }}-composer-${{ hashFiles('composer.lock') }} + restore-keys: | + ${{ runner.os }}-composer- + + - name: Install PHP dependencies + run: composer install --prefer-dist --no-interaction --no-progress + + - name: Bootstrap Laravel testing environment + run: | + cat > .env <<'EOF' + APP_NAME=school_api + APP_ENV=testing + APP_KEY= + APP_DEBUG=false + APP_URL=http://localhost + LOG_CHANNEL=stderr + LOG_LEVEL=debug + DB_CONNECTION=sqlite + DB_DATABASE=${{ github.workspace }}/database/database.sqlite + CACHE_DRIVER=array + SESSION_DRIVER=array + QUEUE_CONNECTION=sync + MAIL_MAILER=array + FILESYSTEM_DISK=local + JWT_SECRET=ci_test_jwt_secret_not_for_production + 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 + php artisan config:clear + php artisan view:clear + + - name: Run database migrations + run: php artisan migrate --force + + - name: Run Unit tests + run: php artisan test --testsuite=Unit --log-junit reports/phpunit-unit.xml + + - name: Run Feature tests + run: php artisan test --testsuite=Feature --log-junit reports/phpunit-feature.xml + + - name: Upload test reports + uses: actions/upload-artifact@v4 + if: always() + with: + name: test-reports + path: reports/ + retention-days: 7 + + # ────────────────────────────────────────────── + # BUILD: Compile frontend assets (Vite) + # ────────────────────────────────────────────── + build: + name: Build frontend assets + runs-on: ubuntu-latest + container: + image: node:22-alpine + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Cache npm dependencies + uses: actions/cache@v4 + with: + path: node_modules/ + key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }} + restore-keys: | + ${{ runner.os }}-npm- + + - name: Install dependencies + run: npm ci --cache .npm --prefer-offline + + - name: Build assets + run: npm run build + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: frontend-assets + path: | + public/build/ + public/css/ + public/js/ + retention-days: 7 + + # ────────────────────────────────────────────── + # SECURITY: Dependency audits + secret scanning + # ────────────────────────────────────────────── + security: + name: Security audit + runs-on: ubuntu-latest + container: + image: php:8.3-cli + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install system dependencies + run: | + 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 + + - name: Install Composer + run: | + 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 + + - name: Install PHP dependencies + run: composer install --prefer-dist --no-interaction --no-progress + + - name: Composer audit + run: composer audit --locked --no-interaction --format=json || echo "Audit found advisories (non-blocking)" + + - name: Secret detection (basic) + run: | + echo "Scanning for committed secrets..." + if grep -RInE \ + "(APP_KEY=base64:[A-Za-z0-9+/=]{44}|DB_PASSWORD=.+|JWT_SECRET=.+|MAIL_PASSWORD=.+|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 \ + . 2>&1; then + echo "❌ Secrets detected!" + exit 1 + else + echo "✅ No secrets found" + fi + + # ────────────────────────────────────────────── + # DEPLOY (manual): to production via SSH + # ────────────────────────────────────────────── + deploy: + name: Deploy to production + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master' + needs: [test, build, security] + environment: + name: production + url: ${{ vars.PRODUCTION_URL }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install SSH client + run: | + apt-get update + apt-get install -y --no-install-recommends openssh-client sshpass rsync + + - name: Deploy via rsync + run: | + sshpass -p "${{ secrets.SSH_PASSWORD }}" rsync -avz --delete \ + -e "ssh -p ${{ vars.SSH_PORT || 22 }} -o StrictHostKeyChecking=no" \ + --exclude='.env' \ + --exclude='.git' \ + --exclude='tests' \ + --exclude='storage/logs/*' \ + --exclude='database/sqlite' \ + --exclude='node_modules' \ + --exclude='vendor' \ + ./ \ + ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:${{ vars.SSH_TARGET_DIR }}/