Repository: symfony/ux
Pull request #2764 (2025-05-22 16:42:58)

[CI] Re-organize workflows: split unit/functional/app tests, split code quality workflow, ...
| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | no <!-- please update src/**/CHANGELOG.md files -->
| Docs?         | no <!-- required for new features -->
| Issues        | Fix #... <!-- prefix each issue number with "Fix #", no need to create an issue if none exist, explain below instead -->
| License       | MIT

<!--
Replace this notice by a description of your feature/bugfix.
This will help reviewers and should be a good start for the documentation.

Additionally (see https://symfony.com/releases):
 - Always add tests and ensure they pass.
 - For new features, provide some code snippets to help understand usage.
 - Features and deprecations must be submitted against branch main.
 - Update/add documentation as required (we can help!)
 - Changelog entry should follow https://symfony.com/doc/current/contributing/code/conventions.html#writing-a-changelog-entry
 - Never break backward compatibility (see https://symfony.com/bc).
-->

Hoping that's the last PR on CI re-organization for a moment, promise :eyes: 

To resume:
| Before | After |
|--------|--------|
| <img width="844" alt="image" src="https://github.com/user-attachments/assets/9b5ec1e6-35b3-4a7a-a482-175db7fd450e" /> | <img width="841" alt="image" src="https://github.com/user-attachments/assets/5e7819ae-1d40-4527-bd33-15a34efff170" /> |


Patch:
diff --git a/.github/workflows/.utils.sh b/.github/workflows/.utils.sh
new file mode 100644
index 00000000..78b0d277
--- /dev/null
+++ b/.github/workflows/.utils.sh
@@ -0,0 +1,21 @@
+_run_task() {
+    local ok=0
+    local title="$1"
+    local start=$(date -u +%s)
+    OUTPUT=$(bash -xc "$2" 2>&1) || ok=$?
+    local end=$(date -u +%s)
+
+    if [[ $ok -ne 0 ]]; then
+      printf "\n%-70s%10s\n" $title $(($end-$start))s
+      echo "$OUTPUT"
+      echo "Job exited with: $ok"
+      echo -e "\n::error::KO $title\\n"
+    else
+      printf "::group::%-68s%10s\n" $title $(($end-$start))s
+      echo "$OUTPUT"
+      echo -e "\n\\e[32mOK\\e[0m $title\\n\\n::endgroup::"
+    fi
+
+    exit $ok
+}
+export -f _run_task
diff --git a/.github/workflows/code-quality.yaml b/.github/workflows/code-quality.yaml
new file mode 100644
index 00000000..84896b53
--- /dev/null
+++ b/.github/workflows/code-quality.yaml
@@ -0,0 +1,72 @@
+name: Code Quality
+
+on:
+    push:
+        paths-ignore:
+            - 'src/*/doc/**'
+            - 'src/**/*.md'
+            - 'ux.symfony.com/**'
+    pull_request:
+        paths-ignore:
+            - 'src/*/doc/**'
+            - 'src/**/*.md'
+            - 'ux.symfony.com/**'
+
+concurrency:
+    group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
+    cancel-in-progress: true
+
+jobs:
+    coding-style-js:
+        name: JavaScript Coding Style
+        runs-on: ubuntu-latest
+        steps:
+            - uses: actions/checkout@v4
+            - run: npm i -g corepack && corepack enable
+            - uses: actions/setup-node@v4
+              with:
+                cache: 'yarn'
+            - run: yarn --immutable
+            - run: yarn ci
+
+    phpstan:
+        name: PHPStan
+        runs-on: ubuntu-latest
+        strategy:
+            fail-fast: false
+            matrix:
+                package: ${{ fromJson(needs.php-packages-matrix.outputs.packages) }}
+        steps:
+            -   name: Checkout
+                uses: actions/checkout@v4
+
+            - name: Configure environment
+              run: |
+                  echo COLUMNS=120 >> $GITHUB_ENV
+                  echo COMPOSER_MIN_STAB='composer config minimum-stability stable --ansi' >> $GITHUB_ENV
+                  echo COMPOSER_UP='composer update --no-progress --no-interaction --ansi' >> $GITHUB_ENV
+                  echo PHPSTAN='vendor/bin/phpstan' >> $GITHUB_ENV
+
+                  # TODO: Only Turbo has PHPStan configuration, let's improve this later :)
+                  PACKAGES=Turbo
+                  #PACKAGES=$(find src/ -mindepth 2 -type f -name composer.json -not -path "*/vendor/*" -printf '%h\n' | sed 's/^src\///' | sort |  tr '\n' ' ')
+                  echo "Packages: $PACKAGES"
+                  echo "PACKAGES=$PACKAGES" >> $GITHUB_ENV
+
+            -   name: Setup PHP
+                uses: shivammathur/setup-php@v2
+                with:
+                    php-version: '8.1'
+                    tools: flex
+
+            - name: Install root dependencies
+              run: composer install
+
+            - name: Build root packages
+              run: php .github/build-packages.php
+
+            - name: Run PHPStan on packages
+              run: |
+                  source .github/workflows/.utils.sh
+
+                  echo "$PACKAGES" | xargs -n1 | parallel -j +3 "_run_task {} '(cd src/{} && $COMPOSER_MIN_STAB && $COMPOSER_UP && $PHPSTAN)'"
diff --git a/.github/workflows/dist-files-unbuilt.yaml b/.github/workflows/dist-files-unbuilt.yaml
new file mode 100644
index 00000000..cd76bdaf
--- /dev/null
+++ b/.github/workflows/dist-files-unbuilt.yaml
@@ -0,0 +1,42 @@
+name: Dist Files Unbuilt
+
+on:
+    push:
+        paths-ignore:
+            - 'src/*/doc/**'
+            - 'src/**/*.md'
+            - 'ux.symfony.com/**'
+    pull_request:
+        paths-ignore:
+            - 'src/*/doc/**'
+            - 'src/**/*.md'
+            - 'ux.symfony.com/**'
+
+concurrency:
+    group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
+    cancel-in-progress: true
+
+jobs:
+  check:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - run: npm i -g corepack && corepack enable
+      - uses: actions/setup-node@v4
+        with:
+          cache: 'yarn'
+          cache-dependency-path: |
+            yarn.lock
+            **/package.json
+      - run: yarn --immutable && yarn build
+
+      - name: Check if JS dist files are current
+        run: |
+          if ! git diff --quiet; then
+            echo "The Git workspace is unclean! Changes detected:"
+            git status --porcelain
+            git diff
+            exit 1
+          else
+            echo "The Git workspace is clean. No changes detected."
+          fi
diff --git a/.github/workflows/toolkit-kits-cs.yaml b/.github/workflows/toolkit-kits-code-quality.yaml
similarity index 95%
rename from .github/workflows/toolkit-kits-cs.yaml
rename to .github/workflows/toolkit-kits-code-quality.yaml
index fcd5e36f..0ed5ccfc 100644
--- a/.github/workflows/toolkit-kits-cs.yaml
+++ b/.github/workflows/toolkit-kits-code-quality.yaml
@@ -1,4 +1,4 @@
-name: Toolkit Kits
+name: Toolkit Kits Code Quality
 
 on:
     push:


Test patch:
diff --git a/.github/workflows/app-tests.yaml b/.github/workflows/app-tests.yaml
new file mode 100644
index 00000000..c6b5da06
--- /dev/null
+++ b/.github/workflows/app-tests.yaml
@@ -0,0 +1,107 @@
+name: App Tests
+
+on:
+    push:
+        paths-ignore:
+            - 'src/*/doc/**'
+            - 'src/**/*.md'
+            - 'ux.symfony.com/**'
+    pull_request:
+        paths-ignore:
+            - 'src/*/doc/**'
+            - 'src/**/*.md'
+            - 'ux.symfony.com/**'
+
+concurrency:
+    group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
+    cancel-in-progress: true
+
+jobs:
+    encore-app:
+      name: "Encore (${{ matrix.name}})"
+      runs-on: ubuntu-latest
+      strategy:
+        fail-fast: false
+        matrix:
+          include:
+            - name: Internal, from "vendor/"
+              ux-packages-source: php-vendor
+            - name: External, from "npm add"
+              ux-packages-source: js-packages
+      steps:
+        - uses: actions/checkout@v4
+
+        - run: npm i -g corepack && corepack enable
+
+        - uses: actions/setup-node@v4
+          with:
+            cache: 'yarn'
+            cache-dependency-path: |
+              yarn.lock
+              package.json
+              src/**/package.json
+              test_apps/encore-app/package.json
+
+        - uses: shivammathur/setup-php@v2
+
+        - name: Install root dependencies
+          uses: ramsey/composer-install@v3
+          with:
+            working-directory: ${{ github.workspace }}
+
+        - name: Build root packages
+          run: php .github/build-packages.php
+          working-directory: ${{ github.workspace }}
+
+        # We always install PHP deps because we of the UX Translator, which requires `var/translations` to exists
+        - uses: ramsey/composer-install@v3
+          with:
+            dependency-versions: 'highest'
+            working-directory: test_apps/encore-app
+
+        - if: matrix.ux-packages-source == 'php-vendor'
+          name: Refresh dependencies from vendor/
+          working-directory: test_apps/encore-app
+          run: yarn
+          env:
+            YARN_ENABLE_HARDENED_MODE: 0
+            YARN_ENABLE_IMMUTABLE_INSTALLS: 0
+
+        - if: matrix.ux-packages-source == 'js-packages'
+          name: Install UX JS packages with a JS package manager
+          working-directory: test_apps/encore-app
+          run: |
+            PACKAGES_TO_INSTALL=''
+            for PACKAGE in $(cd ../..; yarn workspaces list --no-private --json); do
+              PACKAGE_DIR=../../$(echo $PACKAGE | jq -r '.location')
+              PACKAGES_TO_INSTALL="$PACKAGES_TO_INSTALL $PACKAGE_DIR"
+            done
+            echo "Installing packages: $PACKAGES_TO_INSTALL"
+            yarn add --dev $PACKAGES_TO_INSTALL
+
+        - name: Ensure UX packages are installed from "${{ matrix.ux-packages-source == 'php-vendor' && 'vendor/symfony/ux-...' || '../../../src/**/assets' }}"
+          working-directory: test_apps/encore-app
+          run: |
+            for PACKAGE in $(cat package.json | jq -c '(.dependencies // {}) + (.devDependencies // {}) | to_entries[] | select(.key | startswith("@symfony/ux-")) | {name: .key, version: .value}'); do
+              PACKAGE_NAME=$(echo $PACKAGE | jq -r '.name')
+              PACKAGE_VERSION=$(echo $PACKAGE | jq -r '.version')
+
+              echo -n "Checking $PACKAGE_NAME@$PACKAGE_VERSION..."
+              if [[ $PACKAGE_VERSION == $EXPECTED_PATTERN* ]]; then
+                echo " OK"
+              else
+                echo " KO"
+                echo "The package version of $PACKAGE_NAME must starts with the pattern (e.g.: $EXPECTED_PATTERN), got $PACKAGE_VERSION instead."
+                exit 1
+              fi
+            done;
+          env:
+            EXPECTED_PATTERN: ${{ matrix.ux-packages-source == 'php-vendor' && 'file:vendor/symfony/*' || '../../src/*' }}
+
+        - name: Run Encore (dev)
+          working-directory: test_apps/encore-app
+          run: yarn encore dev
+
+        - name: Run Encore (prod)
+          working-directory: test_apps/encore-app
+          run: yarn encore production
diff --git a/.github/workflows/test-turbo.yml b/.github/workflows/functional-tests.yml
similarity index 78%
rename from .github/workflows/test-turbo.yml
rename to .github/workflows/functional-tests.yml
index cabbea8d..27990123 100644
--- a/.github/workflows/test-turbo.yml
+++ b/.github/workflows/functional-tests.yml
@@ -1,4 +1,4 @@
-name: Symfony UX Turbo
+name: Functional Tests
 
 on:
     push:
@@ -9,31 +9,7 @@ on:
             - 'src/Turbo/**'
 
 jobs:
-    phpstan:
-        runs-on: ubuntu-latest
-        steps:
-            -   name: Checkout
-                uses: actions/checkout@v4
-
-            - name: Setup PHP
-              uses: shivammathur/setup-php@v2
-              with:
-                  php-version: '8.1'
-                  extensions: zip
-
-            - uses: ramsey/composer-install@v3
-              with:
-                  working-directory: src/Turbo
-
-            - name: Install PHPUnit dependencies
-              working-directory: src/Turbo
-              run: vendor/bin/simple-phpunit --version
-
-            - name: PHPStan
-              working-directory: src/Turbo
-              run: vendor/bin/phpstan analyse --no-progress
-
-    tests:
+    turbo-tests:
         runs-on: ubuntu-latest
         strategy:
            fail-fast: false
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
deleted file mode 100644
index 7ee1a445..00000000
--- a/.github/workflows/test.yaml
+++ /dev/null
@@ -1,236 +0,0 @@
-name: Symfony UX
-
-on:
-    push:
-        paths-ignore:
-            - 'src/*/doc/**'
-            - 'src/**/*.md'
-            - 'ux.symfony.com/**'
-    pull_request:
-        paths-ignore:
-            - 'src/*/doc/**'
-            - 'src/**/*.md'
-            - 'ux.symfony.com/**'
-
-concurrency:
-    group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
-    cancel-in-progress: true
-
-jobs:
-    coding-style-js:
-        name: JavaScript Coding Style
-        runs-on: ubuntu-latest
-        steps:
-            - uses: actions/checkout@v4
-            - run: npm i -g corepack && corepack enable
-            - uses: actions/setup-node@v4
-              with:
-                cache: 'yarn'
-            - run: yarn --immutable
-            - run: yarn ci
-
-    js-dist-current:
-        name: Check for UnBuilt JS Dist Files
-        runs-on: ubuntu-latest
-        steps:
-            - uses: actions/checkout@v4
-            - run: npm i -g corepack && corepack enable
-            - uses: actions/setup-node@v4
-              with:
-                cache: 'yarn'
-                cache-dependency-path: |
-                  yarn.lock
-                  **/package.json
-            - run: yarn --immutable && yarn build
-
-            - name: Check if JS dist files are current
-              run: |
-                if ! git diff --quiet; then
-                  echo "The Git workspace is unclean! Changes detected:"
-                  git status --porcelain
-                  git diff
-                  exit 1
-                else
-                  echo "The Git workspace is clean. No changes detected."
-                fi
-
-    tests-php:
-        runs-on: ubuntu-latest
-        strategy:
-            fail-fast: false
-            matrix:
-                php-version: ['8.1', '8.2', '8.3', '8.4']
-                dependency-version: ['']
-                symfony-version: ['']
-                minimum-stability: ['stable']
-                include:
-                    # dev packages (probably not needed to have multiple such jobs)
-                    -   minimum-stability: 'dev'
-                        php-version: '8.4'
-                    # lowest deps
-                    -   php-version: '8.1'
-                        dependency-version: 'lowest'
-                    # LTS version of Symfony
-                    -   php-version: '8.1'
-                        symfony-version: '6.4.*'
-
-        env:
-            SYMFONY_REQUIRE: ${{ matrix.symfony-version }}
-        steps:
-            - uses: actions/checkout@v4
-
-            - name: Configure environment
-              run: |
-                  echo COLUMNS=120 >> $GITHUB_ENV
-                  echo COMPOSER_MIN_STAB='composer config minimum-stability ${{ matrix.minimum-stability || 'stable' }} --ansi' >> $GITHUB_ENV
-                  echo COMPOSER_UP='composer update ${{ matrix.dependency-version == 'lowest' && '--prefer-lowest' || '' }} --no-progress --no-interaction --ansi' >> $GITHUB_ENV
-                  echo PHPUNIT='vendor/bin/simple-phpunit' >> $GITHUB_ENV
-                  [ 'lowest' = '${{ matrix.dependency-version }}' ] && export SYMFONY_DEPRECATIONS_HELPER=weak
-
-                  # Swup and Typed have no tests, Turbo has its own workflow file
-                  PACKAGES=$(find src/ -mindepth 2 -type f -name composer.json -not -path "*/vendor/*" -printf '%h\n' | sed 's/^src\///' | grep -Ev "Swup|Turbo|Typed" | sort |  tr '\n' ' ')
-                  echo "Packages: $PACKAGES"
-                  echo "PACKAGES=$PACKAGES" >> $GITHUB_ENV
-
-            - name: Setup PHP
-              uses: shivammathur/setup-php@v2
-              with:
-                php-version: ${{ matrix.php-version }}
-                tools: flex
-
-            - name: Install root dependencies
-              run: composer install
-
-            - name: Build root packages
-              run: php .github/build-packages.php
-
-            - name: Run packages tests
-              run: |
-                _run_task() {
-                    local ok=0
-                    local title="$1"
-                    local start=$(date -u +%s)
-                    OUTPUT=$(bash -xc "$2" 2>&1) || ok=$?
-                    local end=$(date -u +%s)
-
-                    if [[ $ok -ne 0 ]]; then
-                      printf "\n%-70s%10s\n" $title $(($end-$start))s
-                      echo "$OUTPUT"
-                      echo "Job exited with: $ok"
-                      echo -e "\n::error::KO $title\\n"
-                    else
-                      printf "::group::%-68s%10s\n" $title $(($end-$start))s
-                      echo "$OUTPUT"
-                      echo -e "\n\\e[32mOK\\e[0m $title\\n\\n::endgroup::"
-                    fi
-
-                    exit $ok
-                }
-                export -f _run_task
-
-                echo "$PACKAGES" | xargs -n1 | parallel -j +3 "_run_task {} '(cd src/{} && $COMPOSER_MIN_STAB && $COMPOSER_UP && $PHPUNIT)'"
-
-    tests-js:
-        runs-on: ubuntu-latest
-        steps:
-            - uses: actions/checkout@v4
-            - run: npm i -g corepack && corepack enable
-            - uses: actions/setup-node@v4
-              with:
-                cache: 'yarn'
-                cache-dependency-path: |
-                  yarn.lock
-                  package.json
-                  src/**/package.json
-            - run: yarn --immutable
-            - run: yarn playwright install
-            - run: yarn test
-
-    test-app-encore-app:
-      name: "Test Apps / Encore (${{ matrix.name}})"
-      runs-on: ubuntu-latest
-      strategy:
-        fail-fast: false
-        matrix:
-          include:
-            - name: Internal, from "vendor/"
-              ux-packages-source: php-vendor
-            - name: External, from "npm add"
-              ux-packages-source: js-packages
-      steps:
-        - uses: actions/checkout@v4
-
-        - run: npm i -g corepack && corepack enable
-
-        - uses: actions/setup-node@v4
-          with:
-            cache: 'yarn'
-            cache-dependency-path: |
-              yarn.lock
-              package.json
-              src/**/package.json
-              test_apps/encore-app/package.json
-
-        - uses: shivammathur/setup-php@v2
-
-        - name: Install root dependencies
-          uses: ramsey/composer-install@v3
-          with:
-            working-directory: ${{ github.workspace }}
-
-        - name: Build root packages
-          run: php .github/build-packages.php
-          working-directory: ${{ github.workspace }}
-
-        # We always install PHP deps because we of the UX Translator, which requires `var/translations` to exists
-        - uses: ramsey/composer-install@v3
-          with:
-            dependency-versions: 'highest'
-            working-directory: test_apps/encore-app
-
-        - if: matrix.ux-packages-source == 'php-vendor'
-          name: Refresh dependencies from vendor/
-          working-directory: test_apps/encore-app
-          run: yarn
-          env:
-            YARN_ENABLE_HARDENED_MODE: 0
-            YARN_ENABLE_IMMUTABLE_INSTALLS: 0
-
-        - if: matrix.ux-packages-source == 'js-packages'
-          name: Install UX JS packages with a JS package manager
-          working-directory: test_apps/encore-app
-          run: |
-            PACKAGES_TO_INSTALL=''
-            for PACKAGE in $(cd ../..; yarn workspaces list --no-private --json); do
-              PACKAGE_DIR=../../$(echo $PACKAGE | jq -r '.location')
-              PACKAGES_TO_INSTALL="$PACKAGES_TO_INSTALL $PACKAGE_DIR"
-            done
-            echo "Installing packages: $PACKAGES_TO_INSTALL"
-            yarn add --dev $PACKAGES_TO_INSTALL
-
-        - name: Ensure UX packages are installed from "${{ matrix.ux-packages-source == 'php-vendor' && 'vendor/symfony/ux-...' || '../../../src/**/assets' }}"
-          working-directory: test_apps/encore-app
-          run: |
-            for PACKAGE in $(cat package.json | jq -c '(.dependencies // {}) + (.devDependencies // {}) | to_entries[] | select(.key | startswith("@symfony/ux-")) | {name: .key, version: .value}'); do
-              PACKAGE_NAME=$(echo $PACKAGE | jq -r '.name')
-              PACKAGE_VERSION=$(echo $PACKAGE | jq -r '.version')
-
-              echo -n "Checking $PACKAGE_NAME@$PACKAGE_VERSION..."
-              if [[ $PACKAGE_VERSION == $EXPECTED_PATTERN* ]]; then
-                echo " OK"
-              else
-                echo " KO"
-                echo "The package version of $PACKAGE_NAME must starts with the pattern (e.g.: $EXPECTED_PATTERN), got $PACKAGE_VERSION instead."
-                exit 1
-              fi
-            done;
-          env:
-            EXPECTED_PATTERN: ${{ matrix.ux-packages-source == 'php-vendor' && 'file:vendor/symfony/*' || '../../src/*' }}
-
-        - name: Run Encore (dev)
-          working-directory: test_apps/encore-app
-          run: yarn encore dev
-
-        - name: Run Encore (prod)
-          working-directory: test_apps/encore-app
-          run: yarn encore production
diff --git a/.github/workflows/unit-tests.yaml b/.github/workflows/unit-tests.yaml
new file mode 100644
index 00000000..f9ee2aab
--- /dev/null
+++ b/.github/workflows/unit-tests.yaml
@@ -0,0 +1,90 @@
+name: Unit Tests
+
+on:
+    push:
+        paths-ignore:
+            - 'src/*/doc/**'
+            - 'src/**/*.md'
+            - 'ux.symfony.com/**'
+    pull_request:
+        paths-ignore:
+            - 'src/*/doc/**'
+            - 'src/**/*.md'
+            - 'ux.symfony.com/**'
+
+concurrency:
+    group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
+    cancel-in-progress: true
+
+jobs:
+    php:
+        runs-on: ubuntu-latest
+        strategy:
+            fail-fast: false
+            matrix:
+                php-version: ['8.1', '8.2', '8.3', '8.4']
+                dependency-version: ['']
+                symfony-version: ['']
+                minimum-stability: ['stable']
+                include:
+                    # dev packages (probably not needed to have multiple such jobs)
+                    -   minimum-stability: 'dev'
+                        php-version: '8.4'
+                    # lowest deps
+                    -   php-version: '8.1'
+                        dependency-version: 'lowest'
+                    # LTS version of Symfony
+                    -   php-version: '8.1'
+                        symfony-version: '6.4.*'
+
+        env:
+            SYMFONY_REQUIRE: ${{ matrix.symfony-version }}
+        steps:
+            - uses: actions/checkout@v4
+
+            - name: Configure environment
+              run: |
+                  echo COLUMNS=120 >> $GITHUB_ENV
+                  echo COMPOSER_MIN_STAB='composer config minimum-stability ${{ matrix.minimum-stability || 'stable' }} --ansi' >> $GITHUB_ENV
+                  echo COMPOSER_UP='composer update ${{ matrix.dependency-version == 'lowest' && '--prefer-lowest' || '' }} --no-progress --no-interaction --ansi' >> $GITHUB_ENV
+                  echo PHPUNIT='vendor/bin/simple-phpunit' >> $GITHUB_ENV
+                  [ 'lowest' = '${{ matrix.dependency-version }}' ] && export SYMFONY_DEPRECATIONS_HELPER=weak
+
+                  # Swup and Typed have no tests, Turbo has its own workflow file
+                  PACKAGES=$(find src/ -mindepth 2 -type f -name composer.json -not -path "*/vendor/*" -printf '%h\n' | sed 's/^src\///' | grep -Ev "Swup|Turbo|Typed" | sort |  tr '\n' ' ')
+                  echo "Packages: $PACKAGES"
+                  echo "PACKAGES=$PACKAGES" >> $GITHUB_ENV
+
+            - name: Setup PHP
+              uses: shivammathur/setup-php@v2
+              with:
+                php-version: ${{ matrix.php-version }}
+                tools: flex
+
+            - name: Install root dependencies
+              run: composer install
+
+            - name: Build root packages
+              run: php .github/build-packages.php
+
+            - name: Run packages tests
+              run: |
+                source .github/workflows/.utils.sh
+
+                echo "$PACKAGES" | xargs -n1 | parallel -j +3 "_run_task {} '(cd src/{} && $COMPOSER_MIN_STAB && $COMPOSER_UP && $PHPUNIT)'"
+
+    js:
+        runs-on: ubuntu-latest
+        steps:
+            - uses: actions/checkout@v4
+            - run: npm i -g corepack && corepack enable
+            - uses: actions/setup-node@v4
+              with:
+                cache: 'yarn'
+                cache-dependency-path: |
+                  yarn.lock
+                  package.json
+                  src/**/package.json
+            - run: yarn --immutable
+            - run: yarn playwright install
+            - run: yarn test