Update README to change chunk file suffix #108
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Playwright tests 🎭 | |
| # The only browser-driven suite in CI — it replaced the Behat workflow, whose | |
| # environment provisioning steps (up through MySQL) this one inherits. | |
| # | |
| # There is deliberately no Chrome/Chromedriver/Selenium/Java/Xvfb stack here: | |
| # tests/playwright/features/actionInstall.feature installs Chamilo by clicking | |
| # through the legacy web installer at /main/install/index.php, so Playwright's | |
| # own browser handles both installing the app and testing it — one | |
| # browser-automation stack for the whole job, not two. | |
| on: [ push, pull_request ] | |
| jobs: | |
| build: | |
| name: PHP ${{ matrix.php-versions }} Test on ${{ matrix.operating-system }} | |
| runs-on: ubuntu-24.04 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| operating-system: [ ubuntu-latest ] | |
| # 8.4/8.5 temporarily disabled: focusing on getting the full suite green on | |
| # 8.3 first, then re-enabling the rest — that split also makes any future | |
| # failure easier to read as "version-specific" instead of muddying it with | |
| # whatever's still being worked out on 8.3 alone. Re-enable by uncommenting. | |
| php-versions: [ '8.3' ] # , '8.4', '8.5' | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v7 | |
| # As of PHP 8.5, opcache is compiled into core (php-src RFC "Make OPcache a | |
| # non-optional part of PHP") and no longer ships as its own loadable | |
| # extension/package — there's no php8.5-opcache in ondrej/php, and | |
| # requesting the "opcache" extension by name would fail. Both spots below | |
| # that reference it (this apt package list, and the extensions: list two | |
| # steps down) skip it for 8.5 accordingly. Computed here in plain bash | |
| # rather than a GitHub Actions `${{ cond && a || b }}` expression — a first | |
| # attempt at that pattern broke on the classic JS-ternary-via-&&/|| | |
| # pitfall (the "true" branch value was '', which is falsy, so `||` fell | |
| # through to the fallback branch instead — i.e. it did the opposite of | |
| # what was intended, and 8.5 tried to install php8.5-opcache anyway, | |
| # failing the whole apt-get install command). Extend the version check if | |
| # a future PHP version keeps this behavior, since it isn't range-proofed. | |
| - name: Setup apache | |
| run: | | |
| PHP_VERSION="${{ matrix.php-versions }}" | |
| OPCACHE_PACKAGE="php${PHP_VERSION}-opcache" | |
| if [ "$PHP_VERSION" = "8.5" ]; then | |
| OPCACHE_PACKAGE="" | |
| fi | |
| sudo apt-get install software-properties-common | |
| sudo add-apt-repository ppa:ondrej/php | |
| sudo apt-get update | |
| sudo apt-get install -y nodejs npm apache2 \ | |
| php${PHP_VERSION} \ | |
| php${PHP_VERSION}-common \ | |
| php${PHP_VERSION}-cli \ | |
| php${PHP_VERSION}-mysql \ | |
| php${PHP_VERSION}-bcmath \ | |
| php${PHP_VERSION}-curl \ | |
| php${PHP_VERSION}-gd \ | |
| php${PHP_VERSION}-intl \ | |
| php${PHP_VERSION}-ldap \ | |
| php${PHP_VERSION}-mbstring \ | |
| $OPCACHE_PACKAGE \ | |
| php${PHP_VERSION}-soap \ | |
| php${PHP_VERSION}-xml \ | |
| php${PHP_VERSION}-zip \ | |
| libapache2-mod-php${PHP_VERSION} | |
| sudo a2enmod rewrite actions | |
| sudo chmod 777 -R $HOME | |
| - name: Setup PHP, with composer and extensions | |
| uses: shivammathur/setup-php@v2 | |
| with: | |
| php-version: ${{ matrix.php-versions }} | |
| extensions: bcmath, ctype, curl, dom, gd, iconv, intl, json, ldap, mbstring, pdo_mysql, mysqli, soap, xml, zip${{ matrix.php-versions != '8.5' && ', opcache' || '' }} | |
| ini-values: post_max_size=256M, max_execution_time=600, memory_limit=4096M, date.timezone=Europe/Paris, opcache.enable=1, opcache.memory_consumption=64M | |
| - name: Get composer cache directory | |
| id: composer-cache | |
| run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT | |
| - name: Install dependencies with composer | |
| run: composer install --no-progress | |
| - name: Install assets | |
| run: php bin/console assets:install | |
| - name: Install Yarn | |
| run: | | |
| sudo npm i -g npm | |
| node --version | |
| npm --version | |
| sudo corepack enable | |
| yarn set version stable | |
| yarn --version | |
| - name: Install JS dependencies | |
| run: yarn install | |
| - name: Yarn run encore | |
| run: yarn run encore production | |
| - name: Start mysql service | |
| run: sudo /etc/init.d/mysql start | |
| - name: Install Playwright browsers | |
| run: yarn playwright install --with-deps chromium | |
| # Deliberately last, right before Apache is actually needed, not | |
| # right after `apt-get install apache2` back in "Setup apache". | |
| # A previous run showed 000-default.conf was still the stock | |
| # /var/www/html vhost by this point in the job, despite "Setup | |
| # apache" copying gh-apache into it and re-enabling the site | |
| # without error — something in between (most likely | |
| # shivammathur/setup-php@v2, which manages the Apache/PHP | |
| # integration) was resetting it. Applying the vhost swap here | |
| # instead, as the last write before first use, sidesteps that | |
| # regardless of which exact step was clobbering it. | |
| - name: Configure apache vhost | |
| run: | | |
| sudo a2dissite 000-default.conf | |
| sudo cp -f ${{ github.workspace }}/.github/gh-apache /etc/apache2/sites-available/000-default.conf | |
| sudo a2ensite 000-default.conf | |
| sudo systemctl restart apache2 | |
| - name: Check apache is up | |
| run: | | |
| sudo chmod 777 -R ${{ github.workspace }} | |
| curl http://localhost/main/install/index.php | |
| - name: Install chamilo | |
| env: | |
| BASE_URL: http://localhost | |
| run: | | |
| yarn test:playwright:install | |
| sed -i "s/APP_ENV='dev'/APP_ENV='prod'/g" .env | |
| sed -i "s/APP_DEBUG='1'/APP_DEBUG='0'/g" .env | |
| if grep -q "APP_ENV='prod'" .env; then | |
| printf 'framework:\n http_cache: true\n' > config/packages/prod/framework.yaml | |
| fi | |
| sudo php bin/console cache:clear | |
| sudo chmod -R 777 var | |
| # Seeds the fixed test accounts (mmosquera, acostea, ptook, etc.) | |
| # that the rest of the suite logs in as via "I am a teacher" / | |
| # "I am a student" / etc. — kept as its own step, run once, | |
| # separate from "Playwright tests" below, same reasoning as | |
| # keeping "Install chamilo" separate. | |
| - name: Seed test users | |
| env: | |
| BASE_URL: http://localhost | |
| run: yarn test:playwright:seed | |
| # Creates the "TEMP" course (which becomes cid=3 on this fresh | |
| # install) as a one-time setup fixture, same reasoning as "Seed | |
| # test users" above: course_user_registration.feature and every | |
| # remaining course-tool feature file (toolAgenda, toolForum, | |
| # toolLp, etc. — 14 in total as of this writing) hardcode cid=3, | |
| # assuming "TEMP" already exists. Left as its own step, run once, | |
| # BEFORE the main parallel "Playwright tests" step below, so | |
| # every other feature file can rely on it regardless of which | |
| # worker picks them up or what order they run in — a real CI | |
| # failure showed course_user_registration.feature's first | |
| # scenario reaching that course before course.feature's own | |
| # course-creation scenario (in a different file, different worker) | |
| # had even run. | |
| # | |
| # WHY cid=3 AND NOT cid=1 (changed 2026-08-19): the installer's | |
| # own DemoCoursesFixtures (fixture group "install", loaded by the | |
| # plain `doctrine:fixtures:load` this workflow's "Install chamilo" | |
| # step runs) now creates TWO demo courses before any test-suite | |
| # seeding happens at all — "AI Act" (cid=1) and "Using Chamilo" | |
| # (cid=2), added to the product on 2026-08-08/2026-08-11, i.e. | |
| # AFTER this Playwright suite was originally written against the | |
| # older "TEMP is the first course, so it gets id 1" assumption. | |
| # That assumption is now permanently false, so the whole suite was | |
| # migrated cid=1 -> cid=3 in one pass. Note this failed QUIETLY | |
| # before the migration: cid=1 is a real, working course, so | |
| # cid=1-based scenarios kept passing while silently exercising the | |
| # AI Act demo course instead of TEMP. | |
| # | |
| # This step must create EXACTLY ONE course for cid=3 to be | |
| # deterministic. `--grep "Create a course before testing"` used to | |
| # match TWO scenarios — course.feature's ("TEMP") and | |
| # toolExerciseTeacher.feature's ("EXTEACH") — so on a multi-worker | |
| # pool whichever finished first took id 3 and the other took id 4, | |
| # a genuine coin flip (observed both ways: TEMP won on real CI, | |
| # EXTEACH won locally at 43.7s vs 54.6s). Fixed at the source by | |
| # renaming that other scenario to "Create the exercise teacher | |
| # course before testing", so this grep no longer matches it and | |
| # EXTEACH is created inline as its own file's first scenario | |
| # instead (it is referenced only by course CODE, never by cid, so | |
| # its id is irrelevant — see that file's comment). The | |
| # --workers=1 in package.json's test:playwright:seed-course is | |
| # kept purely as a guard: if a future feature file ever adds a | |
| # scenario matching this grep again, single-worker + the config's | |
| # `fullyParallel: false` keeps creation order deterministic | |
| # (sorted by file path, so "course.feature" still goes first) | |
| # instead of silently reshuffling every cid=3 URL in the suite. | |
| - name: Seed test course | |
| env: | |
| BASE_URL: http://localhost | |
| run: yarn test:playwright:seed-course | |
| # Same reasoning as "Seed test course" above, for the OTHER course | |
| # fixture: sessionAccess.feature (new) selects this course by its | |
| # code, "TEMPPRIVATE", to attach to the sessions it creates. | |
| # course.feature's own "Create a private course before testing" | |
| # scenario is what creates it, but wasn't part of the dedicated | |
| # seed step (only "Create a course before testing"/TEMP was) — the | |
| # exact same cross-file race gotcha already fixed once for TEMP/ | |
| # cid=1 would otherwise apply here too, just for a course "TEMPPRIVATE" | |
| # is looked up by code instead of by an assumed id. | |
| - name: Seed private course | |
| env: | |
| BASE_URL: http://localhost | |
| run: yarn test:playwright:seed-private-course | |
| # Real, confirmed root cause: GroupSettingsSchema.php's schema | |
| # default for 'allow_group_categories' is 'false'. toolGroup. | |
| # feature's entire category/group listing depends on it being | |
| # 'true' for the whole file's run. adminSettings.feature's own | |
| # "Update 'allow_group_categories' setting" scenario sets it to | |
| # 'Yes', but that Feature is tagged @settings and its BeforeAll/ | |
| # AfterAll pair only restores it back to whatever was snapshotted | |
| # BEFORE that scenario ran — 'false' on a fresh install — once | |
| # its own scenarios finish. Since different feature files still | |
| # run concurrently across workers, toolGroup.feature only saw | |
| # 'true' during the narrow window while adminSettings.feature's | |
| # own scenario had it temporarily flipped — a real CI run showed | |
| # toolGroup.feature's very first scenario seeing genuinely zero | |
| # categories rendered, no server error, right after creating one. | |
| # Setting it here, once, before the main batch starts, closes | |
| # that race for the whole run (it also makes adminSettings. | |
| # feature's own snapshot see 'true' as the "current" value, so | |
| # its restore-at-the-end keeps it 'true' too, not 'false'). | |
| - name: Seed platform settings | |
| env: | |
| BASE_URL: http://localhost | |
| run: yarn test:playwright:seed-settings | |
| # NOTE (2026-08-23): a fifth seed step ("Seed course | |
| # subscriptions", subscribing acostea/fapple to TEMP) briefly lived | |
| # here and was REMOVED again on purpose. It is redundant now that | |
| # playwright.config.ts pins `workers: 1`: file order is fixed, and | |
| # course_user_registration.feature sorts before toolGroup.feature, | |
| # so the subscriptions exist by the time anything needs them. | |
| # | |
| # It was also actively harmful. Hoisting those scenarios out of | |
| # their own file broke that file's internal narrative (it | |
| # subscribes amann, unsubscribes him, then re-subscribes him as a | |
| # fixture): with amann pre-subscribed, its first scenario had | |
| # nobody left to select, because subscribe_user.php's picker | |
| # excludes already-subscribed users. And it could never have fixed | |
| # the failures it targeted anyway — a seed guarantees a fixture | |
| # exists at the START, but course TEMP's user list is written by | |
| # four different feature files, so under parallel execution another | |
| # file could still unsubscribe fapple mid-run (measured: it did). | |
| # Ordering, not more seeding, is the actual fix. See | |
| # playwright.config.ts's `workers: 1` comment for the full history. | |
| # The step result is decided by check-results.mjs, NOT by | |
| # `playwright test`'s exit code. That is deliberate: the exit code is | |
| # non-zero for anything that went wrong anywhere in the process, | |
| # including work that is not a test, so it cannot distinguish "a test | |
| # regressed" from "a cleanup hook was slow". Twice in two days this | |
| # job went red on runs that were 410/410 green, because playwright-bdd | |
| # defers every tagged AfterAll into one worker-teardown phase that | |
| # blew its 90s budget AFTER the last test had already passed. | |
| # | |
| # A cleanup step running once every test has finished cannot | |
| # invalidate those tests, and two attempts at making that teardown fit | |
| # its budget both failed — so the durable fix is to stop asking the | |
| # exit code a question it cannot answer. The checker still fails the | |
| # build on any failed test, and on a TRUNCATED run (--min-tests), | |
| # which is the loophole a naive `|| true` would leave open. Non-test | |
| # errors are printed as warnings so they stay visible and fixable. | |
| - name: Playwright tests | |
| env: | |
| BASE_URL: http://localhost | |
| run: | | |
| yarn test:playwright || echo "::warning::playwright exited non-zero — deferring to the result check below" | |
| node tests/playwright/scripts/check-results.mjs \ | |
| --report var/test-results/playwright/results.json \ | |
| --min-tests 439 \ | |
| --label "main batch" | |
| # MUST come before the SpecialCase1 step below, not at the end of | |
| # the job with the other uploads: `playwright test` wipes its own | |
| # outputDir at the START of every invocation, so the SpecialCase1 | |
| # run would delete this batch's HTML report, traces and | |
| # screenshots before any end-of-job upload step could collect | |
| # them. Two separately-named artifacts is the only way to keep | |
| # both batches' evidence. | |
| - name: Upload main-batch Playwright report on failure | |
| if: failure() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: playwright-report-main-php${{ matrix.php-versions }} | |
| path: | | |
| var/test-results/playwright/report | |
| var/test-results/playwright/results | |
| var/test-results/playwright/results.json | |
| retention-days: 7 | |
| if-no-files-found: ignore | |
| # SpecialCase1 runs in its own SEQUENTIAL step, excluded from the | |
| # parallel batch above via the @specialcase1 tag (see | |
| # package.json's --grep/--grep-invert pair). Three separate | |
| # reasons, any one of which would justify it: | |
| # | |
| # 1. ORDERING. specialCase1Sessions.feature hard-depends on | |
| # specialCase1PlatformSettings.feature having run first — the | |
| # session extra fields (extra_domaine/extra_theme_fr/ | |
| # extra_theme_de/extra_ecouter) its 4 session scenarios fill in | |
| # only exist because "Add minimal session extra fields" creates | |
| # them, and its teacher-creation scenario needs | |
| # admins_can_set_users_pass, which that same file enables. In | |
| # the parallel batch there was NO ordering guarantee between | |
| # two different files at all. With --workers=1 and the config's | |
| # `fullyParallel: false`, files run in sorted order, and | |
| # "specialCase1PlatformSettings" sorts before | |
| # "specialCase1Sessions" — so the dependency is satisfied by | |
| # construction. | |
| # 2. COLLATERAL DAMAGE. specialCase1PlatformSettings mutates ~100 | |
| # platform settings, several of which are global and UI-visible | |
| # on EVERY page (cookie_warning renders a fixed bottom banner | |
| # that intercepts pointer events; allow_registration/ | |
| # allow_terms_conditions change the header on every page). Its | |
| # own "Tear down" scenario restores them, but only at the END — | |
| # so while it ran, every concurrently-executing file saw the | |
| # mutated platform. Running it alone closes that window | |
| # entirely. | |
| # 3. WORKER STARVATION. Its scenarios are tagged @long-scenario | |
| # (15-minute budget) and genuinely take minutes each. Dropping | |
| # several of those into the shared worker pool measurably | |
| # starved unrelated files: the CI run that first enabled them | |
| # produced fresh 90s timeouts in companyReports.feature and | |
| # accessCompanyReports.feature, which had passed immediately | |
| # before, plus a session-bleed failure in | |
| # admin/fileIntegrity.feature. | |
| # | |
| # `if: always()` so this still runs when the batch above fails — | |
| # SpecialCase1 derives from a real customer project, so its signal | |
| # must not be hidden behind an unrelated failure elsewhere. Placed | |
| # AFTER the batch (not before) so the batch sees pristine, | |
| # never-yet-mutated platform settings. | |
| # Same exit-code-vs-results reasoning as the main batch above. | |
| # --min-tests is this batch's own floor (11 scenarios), not the | |
| # suite-wide one — a shared floor would let a truncated SpecialCase1 | |
| # run slip through. | |
| # | |
| # NOTE: test:playwright:specialcase1 deliberately passes | |
| # `--retries=0 --trace=retain-on-failure`, overriding the config's | |
| # `retries: 1` / `trace: "on-first-retry"`. Retries are WRONG for this | |
| # batch, and measured to be: a retry gets a fresh browser but the SAME | |
| # database, and these scenarios are heavily non-idempotent (they create | |
| # sessions, courses and extra fields). Observed on 2026-08-26 — "Add | |
| # minimal session extra fields" failed attempt 0 on the terms-and- | |
| # conditions interstitial, then attempt 1 failed with a COMPLETELY | |
| # different error ("Add courses to this session" not found) because | |
| # attempt 0 had already created the session. One clear failure became | |
| # two confusing ones. | |
| # `--trace=retain-on-failure` is the necessary partner: with retries | |
| # off, "on-first-retry" would never capture a trace at all, so a | |
| # SpecialCase1 failure would arrive with no evidence. | |
| # The main batch keeps retries — its scenarios largely clean up after | |
| # themselves, and there the retry is what absorbs the roving | |
| # `browserContext._wrapApiCall ... traceName` Playwright-internal error. | |
| - name: Playwright SpecialCase1 tests | |
| if: always() | |
| env: | |
| BASE_URL: http://localhost | |
| run: | | |
| yarn test:playwright:specialcase1 || echo "::warning::playwright exited non-zero — deferring to the result check below" | |
| node tests/playwright/scripts/check-results.mjs \ | |
| --report var/test-results/playwright/results.json \ | |
| --min-tests 11 \ | |
| --label "SpecialCase1" | |
| # Whatever is left in outputDir at the end of the job — i.e. the | |
| # SpecialCase1 step's own output, since it runs last and wiped the | |
| # main batch's (uploaded separately above under | |
| # playwright-report-main-*). Kept on the original artifact name so | |
| # existing habits/tooling still find something here. | |
| - name: Upload Playwright report on failure | |
| if: failure() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: playwright-report-php${{ matrix.php-versions }} | |
| path: | | |
| var/test-results/playwright/report | |
| var/test-results/playwright/results | |
| var/test-results/playwright/results.json | |
| retention-days: 7 | |
| if-no-files-found: ignore | |
| - name: Upload provisioning logs on failure | |
| if: failure() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: provisioning-logs-php${{ matrix.php-versions }} | |
| path: /var/log/apache2/error.log | |
| retention-days: 7 | |
| if-no-files-found: ignore |