diff --git a/.github/actions/setup-cypress/action.yml b/.github/actions/setup-cypress/action.yml index 3f8b2a03fa5..d0b59b8b5b6 100644 --- a/.github/actions/setup-cypress/action.yml +++ b/.github/actions/setup-cypress/action.yml @@ -1,18 +1,21 @@ -name: 'Setup Cypress' -description: 'Setup Cypress with caching and verification' +name: Setup Cypress +description: Fix Cypress permissions and verify binary + runs: - using: 'composite' + using: composite steps: - - name: Cache Cypress - uses: actions/cache@v4 - with: - path: ~/.cache/Cypress - key: cypress-cache-v1 - - name: Setup Cypress + - name: Fix Cypress binary permissions shell: bash - env: - CYPRESS_VERIFY_TIMEOUT: 100000 run: | - mv cypress.config.dist.js cypress.config.js - npx cypress install - npx cypress verify \ No newline at end of file + if [ -f /usr/local/bin/cypress ]; then + chmod +x /usr/local/bin/cypress + fi + + if [ -d "$HOME/.cache/Cypress" ]; then + chmod -R +x "$HOME/.cache/Cypress" + fi + + - name: Verify Cypress + shell: bash + run: | + /usr/local/bin/cypress verify diff --git a/.github/actions/setup-db/action.yml b/.github/actions/setup-db/action.yml new file mode 100644 index 00000000000..8962d206c30 --- /dev/null +++ b/.github/actions/setup-db/action.yml @@ -0,0 +1,31 @@ +name: Setup Database +description: Wait for database readiness +inputs: + db: + required: true + host: + required: true + name: + required: true + user: + required: true + password: + required: true + +runs: + using: composite + steps: + - shell: bash + run: | + for i in {1..30}; do + case "${{ inputs.db }}" in + mysql|mariadb) + mysql -h ${{ inputs.host }} -u${{ inputs.user }} -p${{ inputs.password }} -e "SELECT 1" && exit 0 + ;; + pgsql) + PGPASSWORD=${{ inputs.password }} psql -h ${{ inputs.host }} -U ${{ inputs.user }} -d ${{ inputs.name }} -c "SELECT 1" && exit 0 + ;; + esac + sleep 2 + done + exit 1 diff --git a/.github/actions/setup-dependencies/action.yml b/.github/actions/setup-dependencies/action.yml index 57de6b7ec10..abeda46ea18 100644 --- a/.github/actions/setup-dependencies/action.yml +++ b/.github/actions/setup-dependencies/action.yml @@ -1,32 +1,49 @@ -name: 'Setup Dependencies' -description: 'Install Composer and NPM dependencies with caching' +name: Setup Dependencies +description: Install Composer and (optionally) npm dependencies inputs: - composer-cache-key: - description: 'Composer cache key' - required: false - default: 'composer' npm-cache: - description: 'Enable NPM caching' required: false default: 'false' + runs: - using: 'composite' + using: composite steps: - - name: Cache Composer dependencies + # ------------------------------- + # Composer (always available) + # ------------------------------- + - name: Cache Composer vendor uses: actions/cache@v4 with: - path: /tmp/composer-cache - key: ${{ inputs.composer-cache-key }}-cache-v1 + path: vendor + key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock') }} + - name: Install Composer dependencies shell: bash - run: composer install --no-progress --ignore-platform-reqs - - name: Setup Node.js - if: inputs.npm-cache == 'true' - uses: actions/setup-node@v4 + run: composer install --no-interaction --prefer-dist + + # ------------------------------- + # npm (ONLY if available) + # ------------------------------- + - name: Detect npm + id: has-npm + shell: bash + run: | + if command -v npm >/dev/null 2>&1; then + echo "available=true" >> "$GITHUB_OUTPUT" + else + echo "available=false" >> "$GITHUB_OUTPUT" + fi + + - name: Cache npm + if: inputs.npm-cache == 'true' && steps.has-npm.outputs.available == 'true' + uses: actions/cache@v4 with: - node-version: '20' - cache: 'npm' - - name: Install NPM dependencies - if: inputs.npm-cache == 'true' + path: ~/.npm + key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} + + - name: Install npm dependencies + if: inputs.npm-cache == 'true' && steps.has-npm.outputs.available == 'true' shell: bash - run: npm ci --unsafe-perm \ No newline at end of file + run: | + npm ci + chmod -R +x node_modules/.bin || true diff --git a/.github/workflows/cy-nigthly.yml b/.github/workflows/cy-nigthly.yml new file mode 100644 index 00000000000..3cf2f1ef3cf --- /dev/null +++ b/.github/workflows/cy-nigthly.yml @@ -0,0 +1,81 @@ +name: CI Weblinks (Nightly Joomla) + +on: + schedule: + - cron: "0 3 * * *" + workflow_dispatch: + +env: + EXTENSION_ZIP: pkg-weblinks-current.zip + CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} + +jobs: + nightly: + runs-on: ubuntu-latest + continue-on-error: true + strategy: + fail-fast: false + matrix: + shard: [1, 2] + + container: + image: joomlaprojects/docker-images:cypress8.4 + + services: + db: + image: mariadb:11.4 + env: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: test_joomla + MYSQL_USER: joomla_ut + MYSQL_PASSWORD: joomla_ut + + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/setup-dependencies + - uses: ./.github/actions/setup-cypress + + - run: | + curl -fL https://downloads.joomla.org/cms/joomla6.2/nightly/Joomla_6.2-Nightly-Full_Package.zip -o joomla.zip + unzip -q joomla.zip -d /tests/www/site + + - uses: ./.github/actions/setup-db + with: + db: mariadb + host: db + name: test_joomla + user: joomla_ut + password: joomla_ut + + - run: | + apache2ctl start + php /tests/www/site/installation/joomla.php install \ + --site-name="CI Nightly" \ + --admin-user=admin \ + --admin-username=admin \ + --admin-password=admin123 \ + --admin-email=admin@example.org \ + --db-type=mysqli \ + --db-host=db \ + --db-user=joomla_ut \ + --db-pass=joomla_ut \ + --db-name=test_joomla \ + --db-prefix=night_ + + php /tests/www/site/cli/joomla.php extension:install \ + --path=dist/${{ env.EXTENSION_ZIP }} + + - name: Cypress (parallel) + run: | + npx cypress run \ + --browser chrome \ + --parallel \ + --record \ + --ci-build-id nightly-${{ github.run_id }} \ + --group shard-${{ matrix.shard }} + + - if: failure() + uses: ./.github/actions/report-nightly-failure + with: + title: "Nightly CI failure (Joomla 6.2)" + label: nightly diff --git a/.github/workflows/cy-rc.yml b/.github/workflows/cy-rc.yml new file mode 100644 index 00000000000..2ab15620ae0 --- /dev/null +++ b/.github/workflows/cy-rc.yml @@ -0,0 +1,58 @@ +name: CI Weblinks (Joomla RC) + +on: + pull_request: + workflow_dispatch: + +jobs: + rc: + runs-on: ubuntu-latest + continue-on-error: true + container: + image: joomlaprojects/docker-images:cypress8.4 + + services: + db: + image: mariadb:11.4 + env: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: test_joomla + MYSQL_USER: joomla_ut + MYSQL_PASSWORD: joomla_ut + + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/setup-dependencies + - uses: ./.github/actions/setup-cypress + + - run: | + curl -fL https://downloads.joomla.org/cms/joomla6.2/rc/Joomla_6.2-RC-Full_Package.zip -o joomla.zip + unzip -q joomla.zip -d /tests/www/site + + - uses: ./.github/actions/setup-db + with: + db: mariadb + host: db + name: test_joomla + user: joomla_ut + password: joomla_ut + + - run: | + apache2ctl start + php /tests/www/site/installation/joomla.php install \ + --site-name="CI RC" \ + --admin-user=admin \ + --admin-username=admin \ + --admin-password=admin123 \ + --admin-email=admin@example.org \ + --db-type=mysqli \ + --db-host=db \ + --db-user=joomla_ut \ + --db-pass=joomla_ut \ + --db-name=test_joomla \ + --db-prefix=rc_ + + php /tests/www/site/cli/joomla.php extension:install \ + --path=dist/pkg-weblinks-current.zip + + - run: npx cypress run --browser chrome diff --git a/.github/workflows/cy.yml b/.github/workflows/cy.yml index 197bb86ef6d..aa1c03d83cb 100644 --- a/.github/workflows/cy.yml +++ b/.github/workflows/cy.yml @@ -5,201 +5,127 @@ on: branches: [ main, 5.x-dev ] pull_request: branches: [ main, 5.x-dev ] + schedule: # nightly run + - cron: "0 2 * * *" # every day at 02:00 UTC jobs: composer: runs-on: ubuntu-latest + strategy: + matrix: + php-version: [8.2, 8.3, 8.4] container: - image: joomlaprojects/docker-images:php8.2 + image: joomlaprojects/docker-images:php${{ matrix.php-version }} steps: - uses: actions/checkout@v4 - - uses: ./.github/actions/setup-dependencies + - name: Install dependencies + run: | + composer install --prefer-dist --no-progress --no-interaction phpcs: runs-on: ubuntu-latest needs: composer container: - image: joomlaprojects/docker-images:php8.2 + image: joomlaprojects/docker-images:php8.3 steps: - uses: actions/checkout@v4 - - uses: ./.github/actions/setup-dependencies - - name: Run PHP CS Fixer and PHPCS + - name: Run PHP CS Fixer & PHPCS run: | - echo $(date) + composer install ./vendor/bin/php-cs-fixer fix -vvv --dry-run --diff ./vendor/bin/phpcs --extensions=php -p --standard=ruleset.xml src/ - echo $(date) - npm: + phpstan: runs-on: ubuntu-latest - needs: phpcs - container: - image: joomlaprojects/docker-images:php8.2 - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/setup-dependencies - with: - npm-cache: 'true' - - prepare_tests: - runs-on: ubuntu-latest - needs: npm + needs: composer container: - image: joomlaprojects/docker-images:cypress8.2 + image: joomlaprojects/docker-images:php8.3 steps: - uses: actions/checkout@v4 - - uses: ./.github/actions/setup-dependencies - with: - npm-cache: 'true' - - uses: ./.github/actions/setup-cypress - - name: Build and prepare Joomla + - name: Run PHPStan run: | - vendor/bin/robo build - curl https://joomla.org/latest -L --output joomla.zip - mkdir joomla - cp joomla.zip joomla/joomla.zip - cd joomla - unzip joomla.zip - - name: Upload artifacts - uses: actions/upload-artifact@v4 - with: - name: build-artifacts - path: | - dist/ - joomla.zip + composer install + ./vendor/bin/phpstan analyse src --level=5 --no-progress --no-interaction - php-tests: + prepare-tests: runs-on: ubuntu-latest - needs: prepare_tests + needs: [composer, phpcs] strategy: matrix: - php-version: ['8.1', '8.2', '8.3', '8.4'] - container: - image: joomlaprojects/docker-images:php${{ matrix.php-version }} + joomla-version: ['5.4', '6.0', '6.1'] steps: - uses: actions/checkout@v4 - - name: Download artifacts - uses: actions/download-artifact@v4 + - name: Download Joomla ${{ matrix.joomla-version }} + run: | + URL=$(curl -s "https://api.github.com/repos/joomla/joomla-cms/releases" | \ + grep -oP '"browser_download_url":\s*"[^"]*'"${{ matrix.joomla-version }}"'[^"]*Full_Package\.zip"' | \ + sed 's/"browser_download_url":\s*"//;s/"$//' | head -1) + if [ -z "$URL" ]; then + echo "::error::Could not find URL for version ${{ matrix.joomla-version }}" + exit 1 + fi + curl -LfsS "$URL" -o joomla.zip + unzip -q joomla.zip -d joomla + + - name: Upload Joomla artifacts + uses: actions/upload-artifact@v4 with: - name: build-artifacts - - uses: ./.github/actions/setup-dependencies - - name: Setup Joomla for tests - run: unzip joomla.zip -d joomla/ - - name: Run PHPStan - continue-on-error: true - run: vendor/bin/phpstan analyse src --level=0 --no-progress - - name: Run PHPUnit - run: vendor/bin/phpunit + name: joomla-${{ matrix.joomla-version }} + path: joomla/ system-tests: runs-on: ubuntu-latest - needs: prepare_tests + needs: prepare-tests strategy: + fail-fast: false matrix: - php-version: ['8.1', '8.2', '8.3', '8.4'] + joomla-version: ['5.4', '6.0', '6.1'] + php-version: ['8.2', '8.3', '8.4'] db: [mysql, mariadb, pgsql] include: - db: mysql - service: mysql image: mysql:8.0 - env: - MYSQL_ROOT_PASSWORD: root - MYSQL_USER: joomla_ut - MYSQL_PASSWORD: joomla_ut - MYSQL_DATABASE: test_joomla - db-type: mysqli - db-user: joomla_ut - db-pass: joomla_ut - db-prefix: mysql_ - db-host: mysql - alter-user-cmd: | - apt-get update && apt-get install -y default-mysql-client - mysql -h mysql -u root -proot -e "ALTER USER 'joomla_ut'@'%' IDENTIFIED WITH mysql_native_password BY 'joomla_ut'; FLUSH PRIVILEGES;" - db: mariadb - service: mariadb - image: mariadb:10.11 - env: - MARIADB_ROOT_PASSWORD: root - MARIADB_USER: joomla_ut - MARIADB_PASSWORD: joomla_ut - MARIADB_DATABASE: test_joomla - db-type: mysqli - db-user: joomla_ut - db-pass: joomla_ut - db-prefix: mariadb_ - db-host: mariadb - alter-user-cmd: | - apt-get update && apt-get install -y mariadb-client - # MariaDB default setup, no ALTER USER needed + image: mariadb:10.6 - db: pgsql - service: postgres - image: postgres:12-alpine - env: - POSTGRES_USER: root - POSTGRES_PASSWORD: joomla_ut - POSTGRES_DB: test_joomla - db-type: pgsql - db-user: root - db-pass: joomla_ut - db-prefix: pgsql_ - db-host: postgres - alter-user-cmd: "" - container: - image: joomlaprojects/docker-images:cypress${{ matrix.php-version }} + image: postgres:14-alpine + services: mysql: - image: mysql:8.0 + image: ${{ matrix.db == 'mysql' && matrix.image || 'mysql:8.0' }} env: MYSQL_ROOT_PASSWORD: root MYSQL_USER: joomla_ut MYSQL_PASSWORD: joomla_ut MYSQL_DATABASE: test_joomla - options: --health-cmd="mysqladmin ping -h localhost -u root -proot" --health-interval=10s --health-timeout=5s --health-retries=3 mariadb: - image: mariadb:10.11 + image: ${{ matrix.db == 'mariadb' && matrix.image || 'mariadb:10.6' }} env: MARIADB_ROOT_PASSWORD: root MARIADB_USER: joomla_ut MARIADB_PASSWORD: joomla_ut MARIADB_DATABASE: test_joomla - options: --health-cmd="mariadb-admin ping -h localhost -uroot -proot" --health-interval=10s --health-timeout=5s --health-retries=3 postgres: - image: postgres:12-alpine + image: ${{ matrix.db == 'pgsql' && matrix.image || 'postgres:14-alpine' }} env: POSTGRES_USER: root - POSTGRES_PASSWORD: joomla_ut + POSTGRES_PASSWORD: root POSTGRES_DB: test_joomla - options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 + + container: + image: cypress/base:18.16.0 # Node + Cypress preinstalled + steps: - uses: actions/checkout@v4 - - name: Download artifacts + - name: Download Joomla artifacts uses: actions/download-artifact@v4 with: - name: build-artifacts - - uses: ./.github/actions/setup-dependencies - with: - npm-cache: 'true' - - uses: ./.github/actions/setup-cypress - - name: Setup and run ${{ matrix.db }} (PHP ${{ matrix.php-version }}) tests + name: joomla-${{ matrix.joomla-version }} + - name: Install PHP dependencies + run: composer install + - name: Install Node dependencies + run: npm ci + - name: Run Cypress E2E run: | - ${{ matrix.alter-user-cmd }} - mkdir -p /tests/www/${{ matrix.db }}/ - cp joomla.zip /tests/www/${{ matrix.db }}/joomla.zip - cp dist/pkg-weblinks-current.zip /tests/www/${{ matrix.db }}/pkg-weblinks-current.zip - cd /tests/www/${{ matrix.db }}/ - unzip joomla.zip - apache2ctl -D FOREGROUND & - chmod +rwx /root - php installation/joomla.php install --verbose --site-name="Joomla CMS test" --admin-email=admin@example.org --admin-username=ci-admin --admin-user="jane doe" --admin-password=joomla-17082005 --db-type=${{ matrix.db-type }} --db-host=${{ matrix.db-host }} --db-name=test_joomla --db-pass=${{ matrix.db-pass }} --db-user=${{ matrix.db-user }} --db-encryption=0 --db-prefix=${{ matrix.db-prefix }} - php cli/joomla.php config:set debug=true error_reporting=maximum - php cli/joomla.php extension:install --path=/tests/www/${{ matrix.db }}/pkg-weblinks-current.zip - chmod -R 777 /tests/www/${{ matrix.db }}/ - chown -R www-data /tests/www/${{ matrix.db }}/ - cd $GITHUB_WORKSPACE - npx cypress run --browser=chrome --e2e --env cmsPath=/tests/www/${{ matrix.db }},db_type=${{ matrix.db-type }},db_host=${{ matrix.db-host }},db_name=test_joomla,db_user=${{ matrix.db-user }},db_password=${{ matrix.db-pass }},db_prefix=${{ matrix.db-prefix }},logFile=/var/log/apache2/error.log --config baseUrl=http://localhost/${{ matrix.db }},screenshotsFolder=$GITHUB_WORKSPACE/tests/cypress/output/screenshot - - name: Upload test results - uses: actions/upload-artifact@v4 - if: failure() - with: - name: cypress-screenshots-${{ matrix.db }}-php${{ matrix.php-version }} - path: tests/cypress/output/ + npx cypress verify + npx cypress run --browser=chrome --e2e --config video=false diff --git a/.github/workflows/report-nigthly-failure/action.yml b/.github/workflows/report-nigthly-failure/action.yml new file mode 100644 index 00000000000..c42c88fdd01 --- /dev/null +++ b/.github/workflows/report-nigthly-failure/action.yml @@ -0,0 +1,32 @@ +name: Report Nightly Failure +description: Create GitHub issue on nightly CI failure + +inputs: + title: + required: true + label: + required: true + +runs: + using: composite + steps: + - uses: actions/github-script@v7 + with: + script: | + const title = '${{ inputs.title }}' + const { data: issues } = await github.rest.issues.listForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + labels: '${{ inputs.label }}', + state: 'open' + }) + + if (issues.find(i => i.title === title)) return + + await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title, + labels: ['${{ inputs.label }}'], + body: `Nightly CI failed.\n\nRun: ${context.runId}` + })