name: CI on: push: branches: - develop - main pull_request: branches: - develop - main workflow_dispatch: inputs: build_only: description: 'Build Docker image only (skip deploy)' type: boolean default: true jobs: build-assets: if: github.event_name != 'pull_request' runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup PHP & Composer Deps uses: ./.github/actions/setup-php-deps - name: Setup Bun & Node Deps uses: ./.github/actions/setup-bun-deps - name: Build Assets run: bun run build - name: Upload Build Artifact uses: actions/upload-artifact@v4 with: name: build-assets path: public/build retention-days: 1 tests: runs-on: ubuntu-latest services: mysql: image: mysql:8.0 env: MYSQL_ROOT_PASSWORD: password MYSQL_DATABASE: testing ports: - 3306:3306 options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 steps: - name: Checkout uses: actions/checkout@v4 - name: Setup PHP & Composer Deps uses: ./.github/actions/setup-php-deps - name: Copy Environment File run: cp .env.example .env - name: Generate Application Key run: php artisan key:generate - name: Tests # --parallel splits the suite across workers via paratest. Each # worker creates its own "testing_test_" database; the mysql # service runs as root which has CREATE on *.* so this Just Works # without extra grants. Performance suite is excluded because # concurrent load skews its timing-based assertions. run: ./vendor/bin/pest --exclude-testsuite=Browser,Performance --parallel --processes=4 env: TESTCONTAINERS: false DB_CONNECTION: mysql DB_HOST: 127.0.0.1 DB_PORT: 3306 DB_DATABASE: testing DB_USERNAME: root DB_PASSWORD: password browser-tests: if: ${{ !cancelled() && github.event_name == 'pull_request' }} runs-on: ubuntu-latest needs: browser-tests-matrix steps: - name: Aggregate shard results run: | if [ "${{ needs.browser-tests-matrix.result }}" != "success" ]; then echo "One or more browser-tests shards failed: ${{ needs.browser-tests-matrix.result }}" exit 1 fi echo "All browser-tests shards passed." browser-tests-matrix: if: github.event_name == 'pull_request' runs-on: ubuntu-latest strategy: fail-fast: false matrix: shard: [1, 2, 3, 4, 5] services: mysql: image: mysql:8.0 env: MYSQL_ROOT_PASSWORD: password MYSQL_DATABASE: testing ports: - 3306:3306 options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 steps: - name: Checkout uses: actions/checkout@v4 - name: Setup PHP & Composer Deps uses: ./.github/actions/setup-php-deps - name: Setup Bun & Node Deps uses: ./.github/actions/setup-bun-deps - name: Cache Playwright browsers id: playwright-cache uses: actions/cache@v4 with: path: ~/.cache/ms-playwright key: playwright-${{ runner.os }}-${{ hashFiles('bun.lock') }} restore-keys: playwright-${{ runner.os }}- - name: Install Playwright Browsers run: npx playwright install --with-deps chromium if: steps.playwright-cache.outputs.cache-hit != 'true' - name: Install Playwright system deps run: npx playwright install-deps chromium if: steps.playwright-cache.outputs.cache-hit == 'true' - name: Build Assets run: bun run build - name: Copy Environment File run: cp .env.example .env - name: Generate Application Key run: php artisan key:generate - name: Browser Tests run: ./vendor/bin/pest --testsuite=Browser --ci --shard=${{ matrix.shard }}/6 env: TESTCONTAINERS: false DB_CONNECTION: mysql DB_HOST: 127.0.0.1 DB_PORT: 3306 DB_DATABASE: testing DB_USERNAME: root DB_PASSWORD: password update-browser-shards: if: github.event_name == 'workflow_dispatch' && inputs.build_only == false runs-on: ubuntu-latest services: mysql: image: mysql:8.0 env: MYSQL_ROOT_PASSWORD: password MYSQL_DATABASE: testing ports: - 3306:3306 options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 steps: - name: Checkout uses: actions/checkout@v4 - name: Setup PHP & Composer Deps uses: ./.github/actions/setup-php-deps - name: Setup Bun & Node Deps uses: ./.github/actions/setup-bun-deps - name: Cache Playwright browsers id: playwright-cache uses: actions/cache@v4 with: path: ~/.cache/ms-playwright key: playwright-${{ runner.os }}-${{ hashFiles('bun.lock') }} restore-keys: playwright-${{ runner.os }}- - name: Install Playwright Browsers run: npx playwright install --with-deps chromium if: steps.playwright-cache.outputs.cache-hit != 'true' - name: Install Playwright system deps run: npx playwright install-deps chromium if: steps.playwright-cache.outputs.cache-hit == 'true' - name: Build Assets run: bun run build - name: Copy Environment File run: cp .env.example .env - name: Generate Application Key run: php artisan key:generate - name: Update Browser Shards run: ./vendor/bin/pest --testsuite=Browser --ci --update-shards env: TESTCONTAINERS: false DB_CONNECTION: mysql DB_HOST: 127.0.0.1 DB_PORT: 3306 DB_DATABASE: testing DB_USERNAME: root DB_PASSWORD: password - name: Upload Browser Shards uses: actions/upload-artifact@v4 with: name: browser-shards path: tests/.pest/shards.json retention-days: 1 static-analysis: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup PHP & Composer Deps uses: ./.github/actions/setup-php-deps with: composer-args: '-q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist' - name: Run PHPStan run: vendor/bin/phpstan analyse --memory-limit=512M --no-progress linter: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup PHP & Composer Deps uses: ./.github/actions/setup-php-deps with: composer-args: '-q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist' - name: Setup Bun & Node Deps uses: ./.github/actions/setup-bun-deps - name: Cache Pint results uses: actions/cache@v4 with: path: .pint.cache key: pint-${{ runner.os }}-${{ hashFiles('composer.lock', 'pint.json') }} restore-keys: pint-${{ runner.os }}- - name: Run Pint run: vendor/bin/pint --test --parallel --cache-file=.pint.cache - name: Format Frontend run: bun run format - name: Lint Frontend run: bun run lint - name: Frontend Tests run: bun run test performance-tests: runs-on: ubuntu-latest services: mysql: image: mysql:8.0 env: MYSQL_ROOT_PASSWORD: password MYSQL_DATABASE: testing ports: - 3306:3306 options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 steps: - name: Checkout uses: actions/checkout@v4 - name: Setup PHP & Composer Deps uses: ./.github/actions/setup-php-deps - name: Copy Environment File run: cp .env.example .env - name: Generate Application Key run: php artisan key:generate - name: Performance Tests run: ./vendor/bin/pest --testsuite=Performance env: TESTCONTAINERS: false DB_CONNECTION: mysql DB_HOST: 127.0.0.1 DB_PORT: 3306 DB_DATABASE: testing DB_USERNAME: root DB_PASSWORD: password changes: runs-on: ubuntu-latest outputs: code: ${{ steps.filter.outputs.code }} steps: - uses: actions/checkout@v4 - uses: dorny/paths-filter@v3 id: filter with: filters: | code: - 'app/**' - 'bootstrap/**' - 'config/**' - 'database/**' - 'public/**' - 'resources/**' - 'routes/**' - 'storage/**' - 'artisan' - 'composer.json' - 'composer.lock' - 'package.json' - 'package-lock.json' - 'bun.lock' - 'bun.lockb' - 'vite.config.*' - 'tsconfig*.json' - 'eslint.config.*' - '.prettierrc*' - 'phpstan.neon*' - 'phpunit.xml*' - 'pint.json' - 'Dockerfile' - 'Dockerfile.*' - 'docker/**' - '.dockerignore' - '.env.example' - '.env.production.example' - 'docker-compose.production.yml' - 'docker-compose.production.local.yml' - 'templates/coolify/**' - '.github/workflows/ci.yml' build-image: runs-on: ubuntu-latest timeout-minutes: 45 needs: [tests, linter, static-analysis, performance-tests, changes] if: ((github.ref == 'refs/heads/main' && github.event_name == 'push') || github.event_name == 'workflow_dispatch') && needs.changes.outputs.code == 'true' env: SENTRY_RELEASE: whisper-money@${{ github.sha }} permissions: contents: read packages: write steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: Install Sentry CLI run: curl -sL https://sentry.io/get-cli/ | bash - name: Create Sentry release env: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_ORG: ${{ secrets.SENTRY_ORG }} SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} run: | if ! sentry-cli releases info "$SENTRY_RELEASE" >/dev/null 2>&1; then sentry-cli releases new "$SENTRY_RELEASE" fi sentry-cli releases set-commits "$SENTRY_RELEASE" --auto --ignore-missing sentry-cli releases finalize "$SENTRY_RELEASE" - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login to GitHub Container Registry uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Extract metadata id: meta uses: docker/metadata-action@v5 with: images: ghcr.io/${{ github.repository }} tags: | type=sha,prefix= type=raw,value=latest - name: Extract package version id: package-version run: node -e "console.log('version=' + require('./package.json').version)" >> "$GITHUB_OUTPUT" - name: Extract production metadata id: production-meta uses: docker/metadata-action@v5 with: images: ghcr.io/${{ github.repository }} tags: | type=sha,prefix=,suffix=-production type=raw,value=production type=raw,value=v${{ steps.package-version.outputs.version }}-production - name: Build and push development image uses: docker/build-push-action@v6 timeout-minutes: 20 with: context: . platforms: linux/amd64 push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} build-args: | SENTRY_RELEASE=${{ env.SENTRY_RELEASE }} cache-from: type=gha cache-to: type=gha,mode=max - name: Build and push production image uses: docker/build-push-action@v6 timeout-minutes: 25 with: context: . file: Dockerfile.production platforms: linux/amd64 push: true tags: ${{ steps.production-meta.outputs.tags }} labels: ${{ steps.production-meta.outputs.labels }} build-args: | SENTRY_RELEASE=${{ env.SENTRY_RELEASE }} cache-from: type=gha,scope=production cache-to: type=gha,mode=max,scope=production deploy: runs-on: ubuntu-latest needs: [build-image, changes] if: github.ref == 'refs/heads/main' && github.event_name == 'push' && needs.changes.outputs.code == 'true' env: SENTRY_RELEASE: whisper-money@${{ github.sha }} concurrency: group: production-deploy-main cancel-in-progress: true steps: - name: Checkout uses: actions/checkout@v4 - name: Check if this is the latest main commit id: deploy_guard run: | latest_main_sha=$(git ls-remote origin refs/heads/main | cut -f1) echo "Current workflow SHA: ${{ github.sha }}" echo "Latest main SHA: $latest_main_sha" if [ -z "$latest_main_sha" ]; then echo "Unable to determine the latest main SHA." exit 1 fi if [ "$latest_main_sha" != "${{ github.sha }}" ]; then echo "This workflow is not for the latest commit on main. Skipping deploy." echo "should_deploy=false" >> "$GITHUB_OUTPUT" exit 0 fi echo "This workflow is for the latest commit on main. Proceeding with deploy." echo "should_deploy=true" >> "$GITHUB_OUTPUT" - name: Trigger deployment if: steps.deploy_guard.outputs.should_deploy == 'true' run: | max_attempts=5 attempt=1 while [ $attempt -le $max_attempts ]; do echo "Attempt $attempt of $max_attempts..." response=$(curl -s -w "\n%{http_code}" \ --connect-timeout 30 \ --max-time 120 \ -H "Authorization: Bearer ${{ secrets.DEPLOYMENT_TOKEN }}" \ "http://147.93.126.54:8000/api/v1/deploy?uuid=ww00sswosco8w80k08c0occ8&force=false") || true http_code=$(echo "$response" | tail -n1) body=$(echo "$response" | sed '$d') echo "Response: $body" echo "HTTP Status: ${http_code:-timeout}" if [ -n "$http_code" ] && [ "$http_code" -ge 200 ] && [ "$http_code" -lt 300 ]; then echo "Deployment triggered successfully!" exit 0 fi echo "Deployment request failed with status $http_code" if [ $attempt -lt $max_attempts ]; then delay=$((30 * (2 ** (attempt - 1)))) echo "Retrying in ${delay}s..." sleep $delay fi attempt=$((attempt + 1)) done echo "All $max_attempts attempts failed. Giving up." exit 1 - name: Install Sentry CLI if: steps.deploy_guard.outputs.should_deploy == 'true' run: curl -sL https://sentry.io/get-cli/ | bash - name: Mark Sentry release deployed if: steps.deploy_guard.outputs.should_deploy == 'true' env: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_ORG: ${{ secrets.SENTRY_ORG }} SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} run: sentry-cli releases deploys "$SENTRY_RELEASE" new -e production - name: Deployment skipped if: steps.deploy_guard.outputs.should_deploy == 'false' run: echo "Skipped deployment because a newer commit is already on main."