Repository navigation
Merge pull request #1033 from gplanchat/docs/backends-magento-database #2965
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
| # See DUR009 (test pyramid), DUR008 (PHP-FIG coding standards). | |
| name: CI | |
| # One run per ref: a second push to the same branch cancels the run the first one started. | |
| concurrency: | |
| group: ci-${{ github.ref }} | |
| cancel-in-progress: true | |
| on: | |
| push: | |
| branches: [main, master] | |
| # A prise commit changes no code: testing main again for it only queues runners ahead of the | |
| # pull requests. Push only: on pull_request `ci-ok` is the required check and must exist. | |
| paths-ignore: | |
| - '.worktrees/prises/**' | |
| - '.worktrees/PRISES.md' | |
| pull_request: | |
| jobs: | |
| qa: | |
| # Was the required status check on `main` until 2026-09-16; `ci-ok` below is now. Its steps | |
| # are what `loop/guardrails/verify.sh` mirrors: change one, change the other. | |
| name: Core · CS + tests (PHP ${{ matrix.php-version }}) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| php-version: ['8.2', '8.3', '8.4', '8.5'] | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Setup PHP | |
| uses: shivammathur/setup-php@v2 | |
| with: | |
| php-version: ${{ matrix.php-version }} | |
| # ext-grpc: required by WorkflowTaskRunner, WorkflowTaskProcessor and TemporalHistoryCursor (DUR027). | |
| extensions: mbstring, sqlite3, pdo_sqlite, grpc | |
| # The 8.2 leg measures coverage while it tests: it was a job of its own that re-ran | |
| # this leg's suite (#348 audit, 4). | |
| coverage: ${{ matrix.php-version == '8.2' && 'pcov' || 'none' }} | |
| ini-values: pcov.directory=. | |
| - name: Cache Composer downloads | |
| uses: actions/cache@v6 | |
| with: | |
| path: ~/.cache/composer | |
| key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock', '**/composer.json') }} | |
| restore-keys: composer-${{ runner.os }}- | |
| - name: Install Composer dependencies | |
| run: composer install --no-interaction --prefer-dist --no-progress | |
| - name: PHP-CS-Fixer (dry-run) | |
| run: composer cs:check | |
| - name: PHPUnit | |
| if: matrix.php-version != '8.2' | |
| run: composer test -- --exclude-testsuite temporal --fail-on-skipped | |
| # rector/rector ships its own nikic/php-parser and PCOV loads the project's: the rector | |
| # suite runs without coverage (phpunit.xml says why). | |
| - name: PHPUnit with coverage, red below the line threshold | |
| if: matrix.php-version == '8.2' | |
| env: | |
| # The figure on the day the threshold landed (#348). Raise it with the coverage; | |
| # never lower it to get to green. | |
| MIN_LINE_COVERAGE: '76' | |
| # The default shell is `bash -e` without pipefail: a red PHPUnit would hide behind tee. | |
| run: | | |
| set -o pipefail | |
| vendor/bin/phpunit --exclude-testsuite rector,temporal --fail-on-skipped \ | |
| --coverage-text --only-summary-for-coverage-text | tee coverage.txt | |
| vendor/bin/phpunit --testsuite rector --fail-on-skipped | |
| lines=$(sed -n 's/^ *Lines: *\([0-9.]*\)%.*/\1/p' coverage.txt) | |
| echo "Lines: ${lines}% (minimum ${MIN_LINE_COVERAGE}%)" | |
| awk -v l="$lines" -v m="$MIN_LINE_COVERAGE" 'BEGIN { exit !(l != "" && l >= m) }' | |
| symfony-matrix: | |
| # The `qa` job installs the composer.lock resolution: it only ever sees one Symfony line. | |
| # Yet the only compatibility bug found so far — `#[\Override]` on a method that Symfony | |
| # 6.4's DataCollector does not declare, fatal from PHP 8.3 on — was only visible at the | |
| # *crossing* of an old Symfony line and a recent PHP. Hence an explicit matrix rather | |
| # than a cartesian product: every entry is an edge that has a reason to exist. | |
| name: Symfony · package (${{ matrix.label }} / PHP ${{ matrix.php-version }}) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| # 6.4 LTS at the library's PHP floor. | |
| - { php-version: '8.2', symfony: '6.4.*', deps: 'lowest', label: '6.4 lowest' } | |
| # 6.4 LTS under a recent PHP: the crossing that bites. | |
| - { php-version: '8.4', symfony: '6.4.*', deps: 'highest', label: '6.4 highest' } | |
| # *Installable* floor of the 7 line. The constraint allows ^7.0, but | |
| # 7.0.x through 7.3.x are end of life and blocked by Composer's security | |
| # advisories: pinning them here would test nothing, it would fail to resolve. | |
| - { php-version: '8.2', symfony: '7.4.*', deps: 'lowest', label: '7.4 lowest' } | |
| # 7.4 LTS under a recent PHP: what the lock resolves today. | |
| - { php-version: '8.4', symfony: '7.4.*', deps: 'highest', label: '7.4 highest' } | |
| # Line 8: requires PHP >= 8.4, so never reachable locally on 8.2. | |
| - { php-version: '8.4', symfony: '8.0.*', deps: 'lowest', label: '8.0 lowest' } | |
| - { php-version: '8.5', symfony: '8.*', deps: 'highest', label: '8.x highest' } | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Setup PHP | |
| uses: shivammathur/setup-php@v2 | |
| with: | |
| php-version: ${{ matrix.php-version }} | |
| extensions: mbstring, sqlite3, pdo_sqlite, grpc | |
| coverage: none | |
| # Flex constrains *all* symfony/* dependencies, transitive ones included, to the | |
| # requested line. Without it every package would have to be listed by hand, and the | |
| # monorepo root no longer requires any of them directly. | |
| - name: Cache Composer downloads | |
| uses: actions/cache@v6 | |
| with: | |
| path: ~/.cache/composer | |
| key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock', '**/composer.json') }} | |
| restore-keys: composer-${{ runner.os }}- | |
| - name: Pin the Symfony line (Flex) | |
| run: | | |
| composer global config --no-plugins allow-plugins.symfony/flex true | |
| composer global require --no-progress --no-scripts --no-plugins symfony/flex | |
| composer config --no-plugins allow-plugins.symfony/flex true | |
| # Partial update: only the Symfony dimension moves. An unfiltered `--prefer-lowest` | |
| # would lower phpunit and phpstan too, and would make the matrix fail on something | |
| # other than what it measures. | |
| - name: Resolve the requested Symfony line | |
| env: | |
| SYMFONY_REQUIRE: ${{ matrix.symfony }} | |
| run: | | |
| composer update --no-interaction --prefer-dist --no-progress \ | |
| ${{ matrix.deps == 'lowest' && '--prefer-lowest' || '' }} 'symfony/*' | |
| - name: Symfony line actually installed | |
| run: composer show symfony/messenger --no-ansi | head -n 3 | |
| - name: PHPUnit | |
| run: composer test | |
| # PHPStan is the tool that caught `#[\Override]`: without it, the matrix would only | |
| # see the problem if a test loaded the offending class. | |
| - name: PHPStan | |
| run: composer phpstan | |
| laravel: | |
| # The counterpart of the Symfony matrix, for the other host family. Same principle: one | |
| # entry per *edge* that has a reason to exist, not a cartesian product. | |
| # | |
| # What this matrix measures and no other one can: `gplanchat/durable-laravel` builds on | |
| # `illuminate/*`, whose lines 11, 12 and 13 do not ask for the same PHP — 13 requires ^8.3 | |
| # when the repository's baseline is 8.2 — and whose contracts move from one line to the | |
| # next. The `unit` suite only proves the line in the lock. | |
| # | |
| # ⚠ This package does **not** require `illuminate/queue`, and that is not an oversight: | |
| # Laravel 11+ pulls `symfony/process ^7.2` in with it, which would make it irreconcilable | |
| # with the Symfony 6.4 line the neighbouring matrix still tests. The guard against the | |
| # `sync` queue therefore reads the driver name from the configuration rather than the | |
| # connection class. | |
| name: Laravel · package (${{ matrix.label }} / PHP ${{ matrix.php-version }}) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| # 11 at the PHP floor common to both. | |
| - { php-version: '8.2', laravel: '11.*', deps: 'lowest', label: '11 lowest' } | |
| # 11 under a recent PHP: the crossing that bites, as for Symfony 6.4. | |
| - { php-version: '8.4', laravel: '11.*', deps: 'highest', label: '11 highest' } | |
| # 12 at the floor. | |
| - { php-version: '8.2', laravel: '12.*', deps: 'lowest', label: '12 lowest' } | |
| # 12 under a recent PHP: what the lock resolves today. | |
| - { php-version: '8.4', laravel: '12.*', deps: 'highest', label: '12 highest' } | |
| # 13 requires PHP ^8.3: never reachable at the repository's floor. | |
| - { php-version: '8.4', laravel: '13.*', deps: 'lowest', label: '13 lowest' } | |
| - { php-version: '8.5', laravel: '13.*', deps: 'highest', label: '13 highest' } | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Setup PHP | |
| uses: shivammathur/setup-php@v2 | |
| with: | |
| php-version: ${{ matrix.php-version }} | |
| # grpc as in the Symfony matrix: the root requires the Temporal bridge, which | |
| # requires ext-grpc. Without it, `composer update` fails before having resolved | |
| # anything of Illuminate — the matrix would measure the absence of an extension. | |
| extensions: mbstring, sqlite3, pdo_sqlite, grpc | |
| coverage: none | |
| # Partial update, same reason as on the Symfony side: only the Laravel dimension | |
| # moves. An unfiltered `--prefer-lowest` would lower phpunit and phpstan, and the | |
| # matrix would fail on something other than what it measures. | |
| - name: Resolve the requested Laravel line | |
| run: | | |
| composer require --no-update --no-interaction --dev \ | |
| "illuminate/cache:${{ matrix.laravel }}" \ | |
| "illuminate/database:${{ matrix.laravel }}" | |
| composer update --no-interaction --prefer-dist --no-progress \ | |
| ${{ matrix.deps == 'lowest' && '--prefer-lowest' || '' }} 'illuminate/*' | |
| - name: Laravel line actually installed | |
| run: composer show illuminate/database --no-ansi | head -n 3 | |
| - name: PHPUnit (the integration package and the Illuminate bridge) | |
| run: vendor/bin/phpunit --testsuite laravel | |
| - name: PHPStan | |
| run: composer phpstan | |
| laravel-bench: | |
| # The Laravel bench's probe (#360): `config/durable.php` → the core's Nexus registry → the | |
| # handler, through the very method the Nexus worker calls, with no cluster (the job's DSN | |
| # names a closed port). Then the round trip (#666), against a dev server started below. The | |
| # `laravel` job above tests the package; this one tests the application that installs it, | |
| # from `laravel/`'s own lock. | |
| name: Laravel · bench, Nexus probe | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| env: | |
| # Read at boot, `package:discover` included: the bench's backend is temporal. | |
| DURABLE_DSN: 'temporal://127.0.0.1:7999?namespace=probe&nexus_task_queue=q&tls=0' | |
| # Without a `.env`, the bench's cache is the database one; the probe needs no database. | |
| CACHE_STORE: array | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Setup PHP | |
| uses: shivammathur/setup-php@v2 | |
| with: | |
| # The bench's floor, the PHP its README runs the probe with. | |
| php-version: '8.2' | |
| extensions: mbstring, sqlite3, pdo_sqlite, curl, grpc | |
| coverage: none | |
| - name: Cache Composer downloads | |
| uses: actions/cache@v6 | |
| with: | |
| path: ~/.cache/composer | |
| key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock', '**/composer.json') }} | |
| restore-keys: composer-${{ runner.os }}- | |
| - name: Composer install (laravel/) | |
| working-directory: laravel | |
| run: composer install --no-interaction --prefer-dist --no-progress | |
| - name: Probe — the bench serves delivery/schedule and delivery/ship | |
| working-directory: laravel | |
| run: php probe-nexus.php | |
| # #666: the same two operations, now served by artisan's workers to the root's demo harness | |
| # on a start-dev server. It needs the root's vendor/ and Durable's search attributes, which | |
| # the bench writes on every start. | |
| - name: Composer install (root, for the demo harness) | |
| run: composer install --no-interaction --prefer-dist --no-progress | |
| - name: Start a Temporal dev server | |
| run: | | |
| docker run -d --name temporal-dev -p 7233:7233 temporalio/temporal:1.9.1 \ | |
| server start-dev --ip 0.0.0.0 --port 7233 --namespace durable-test --headless \ | |
| --search-attribute DurableWorkflowName=Keyword --search-attribute DurableExecutionId=Keyword | |
| timeout 60 bash -c 'until nc -z 127.0.0.1 7233; do sleep 1; done' | |
| - name: PHPUnit — the bench serves delivery against a server | |
| working-directory: laravel | |
| timeout-minutes: 10 | |
| env: | |
| DURABLE_DSN: 'temporal://127.0.0.1:7233?namespace=durable-test&nexus_task_queue=laravel-bench-nexus&workflow_task_queue=laravel-bench-workflows&tls=0' | |
| run: vendor/bin/phpunit --testdox --fail-on-skipped | |
| filament: | |
| # gplanchat/durable-filament (#712, #899), outside the root graph: Filament pulls | |
| # laravel/framework, which requires Symfony ^7 and would break the 6.4 legs of | |
| # symfony-matrix. A throwaway project installs it through path repositories, on each | |
| # Filament line, with the Temporal and Illuminate bridges so a test that needs one (#871) | |
| # cannot skip: durable-laravel only suggests the Illuminate bridge (#847). | |
| name: Filament · package (${{ matrix.label }} / PHP ${{ matrix.php-version }}) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - { php-version: '8.2', filament: '^3.3', deps: 'lowest', label: 'Filament 3 lowest' } | |
| - { php-version: '8.4', filament: '^4.0', deps: 'highest', label: 'Filament 4 highest' } | |
| - { php-version: '8.5', filament: '^4.0', deps: 'highest', label: 'Filament 4 highest' } | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Setup PHP | |
| uses: shivammathur/setup-php@v2 | |
| with: | |
| php-version: ${{ matrix.php-version }} | |
| # grpc: the Temporal bridge is installed, as in the Laravel bench. | |
| extensions: mbstring, sqlite3, pdo_sqlite, curl, grpc, intl | |
| coverage: none | |
| # Composer's cache directory depends on the runner (#767): ask Composer instead of guessing. | |
| # No lock file here, so the key also carries the ISO week (#910): the cache renews weekly. | |
| - name: Composer cache directory | |
| id: composer-cache | |
| run: | | |
| echo "dir=$(composer config --global cache-dir)" >> "$GITHUB_OUTPUT" | |
| echo "week=$(date -u +%G-W%V)" >> "$GITHUB_OUTPUT" | |
| - name: Cache Composer | |
| uses: actions/cache@v6 | |
| with: | |
| path: ${{ steps.composer-cache.outputs.dir }} | |
| key: filament-${{ matrix.php-version }}-${{ hashFiles('src/**/composer.json') }}-${{ steps.composer-cache.outputs.week }} | |
| restore-keys: filament-${{ matrix.php-version }}- | |
| - name: A project that installs the package | |
| run: | | |
| mkdir -p /tmp/filament-app | |
| cat > /tmp/filament-app/composer.json <<JSON | |
| { | |
| "minimum-stability": "dev", | |
| "prefer-stable": true, | |
| "repositories": [ | |
| { "type": "path", "url": "${{ github.workspace }}/src/Durable" }, | |
| { "type": "path", "url": "${{ github.workspace }}/src/DurableLaravel" }, | |
| { "type": "path", "url": "${{ github.workspace }}/src/Bridge/Illuminate" }, | |
| { "type": "path", "url": "${{ github.workspace }}/src/GrpcClient" }, | |
| { "type": "path", "url": "${{ github.workspace }}/src/Bridge/Temporal" }, | |
| { "type": "path", "url": "${{ github.workspace }}/src/DurableFilament" } | |
| ], | |
| "require": { | |
| "filament/filament": "${{ matrix.filament }}", | |
| "gplanchat/durable-filament": "@dev", | |
| "gplanchat/durable-bridge-illuminate": "@dev", | |
| "gplanchat/durable-bridge-temporal": "@dev", | |
| "phpunit/phpunit": "^11.5" | |
| }, | |
| "autoload-dev": { | |
| "psr-4": { "Gplanchat\\\\Durable\\\\Filament\\\\Tests\\\\": "${{ github.workspace }}/src/DurableFilament/tests/" } | |
| } | |
| } | |
| JSON | |
| # Only the Filament dimension moves, as in the laravel job: an unfiltered `--prefer-lowest` | |
| # would lower phpunit and the bridge's dependencies too. The first update writes the lock | |
| # the partial one starts from. `--with-dependencies` lets filament/* drag livewire and the | |
| # rest of its own tree down with it; without it they stay locked at their highest and hold | |
| # filament up. The floor is 3.3.53 and livewire 3.8.3: every version below is blocked by a | |
| # Packagist security advisory (filament/forms <=3.3.52, livewire <=3.8.2). | |
| - name: Composer update | |
| working-directory: /tmp/filament-app | |
| run: | | |
| composer update --no-interaction --prefer-dist --no-progress | |
| composer update --no-interaction --prefer-dist --no-progress \ | |
| ${{ matrix.deps == 'lowest' && '--prefer-lowest --with-dependencies' || '' }} 'filament/*' | |
| - name: Filament line actually installed, and the bridges | |
| working-directory: /tmp/filament-app | |
| run: | | |
| for package in filament/filament livewire/livewire laravel/framework gplanchat/durable-bridge-temporal gplanchat/durable-bridge-illuminate; do | |
| composer show "$package" --no-ansi | grep -E '^(name|versions) ' | |
| done | |
| test -e vendor/gplanchat/durable-bridge-temporal | |
| test -e vendor/gplanchat/durable-bridge-illuminate | |
| - name: Start a Temporal dev server | |
| run: | | |
| docker run -d --name temporal-dev -p 7233:7233 temporalio/temporal:1.9.1 \ | |
| server start-dev --ip 0.0.0.0 --port 7233 --namespace durable-test --headless \ | |
| --search-attribute DurableWorkflowName=Keyword --search-attribute DurableExecutionId=Keyword | |
| timeout 60 bash -c 'until nc -z 127.0.0.1 7233; do sleep 1; done' | |
| - name: PHPUnit (src/DurableFilament/tests) | |
| working-directory: /tmp/filament-app | |
| env: | |
| DURABLE_DSN: 'temporal://127.0.0.1:7233?namespace=durable-test&tls=0' | |
| run: vendor/bin/phpunit -c "$GITHUB_WORKSPACE/src/DurableFilament/phpunit.xml" --bootstrap vendor/autoload.php --display-skipped --fail-on-skipped | |
| sylius-shop: | |
| # The only check in the repository that really renders a page. Everything else proves the | |
| # pieces are correct; this one proves that, assembled, they display something. | |
| # | |
| # Outside the main matrix, and for two reasons there is no way around: Sylius Standard 2.2 | |
| # requires PHP ^8.3 when the repository's baseline is 8.2, and the shop needs a real MySQL. | |
| # It is precisely the latter that revealed two faults SQLite was swallowing — a boolean | |
| # bound without a type, and an upsert that counted the rows affected by an UPDATE. | |
| name: Sylius · dashboard, rendered | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 45 | |
| services: | |
| mysql: | |
| image: mysql:8.4 | |
| env: | |
| MYSQL_ALLOW_EMPTY_PASSWORD: 1 | |
| ports: | |
| - 3306:3306 | |
| options: >- | |
| --health-cmd="mysqladmin ping -h 127.0.0.1" | |
| --health-interval=5s | |
| --health-timeout=5s | |
| --health-retries=20 | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Setup PHP | |
| uses: shivammathur/setup-php@v2 | |
| with: | |
| # 8.3: Sylius Standard 2.2's floor. The plugin itself stays at >=8.2. | |
| php-version: '8.3' | |
| # grpc: the shop serves a Nexus operation, and the Temporal bridge depends on it. | |
| # Its dashboard, for its part, still does not need it — that is DUR037, and | |
| # `temporal.journal: false` is what keeps the two apart. | |
| extensions: intl, gd, pdo_mysql, mbstring, zip, curl, sodium, exif, fileinfo, grpc | |
| coverage: none | |
| # The repository's packages come in through `path` repositories: the shop reads | |
| # `../src`, so it tests the code of this commit and not a published version. | |
| - name: Cache Composer downloads | |
| uses: actions/cache@v6 | |
| with: | |
| path: ~/.cache/composer | |
| key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock', '**/composer.json') }} | |
| restore-keys: composer-${{ runner.os }}- | |
| - name: Composer install (sylius/) | |
| working-directory: sylius | |
| run: composer install --no-interaction --prefer-dist --no-progress | |
| # The shop's layers: value objects, the Payments port's use case and the factories | |
| # that translate a Nexus payload. No kernel, no database, so it runs before both and | |
| # fails in seconds when it fails. | |
| - name: PHPUnit — the shop's own layers | |
| working-directory: sylius | |
| run: vendor/bin/phpunit tests/Unit | |
| - name: Test database | |
| working-directory: sylius | |
| env: | |
| APP_ENV: test | |
| run: | | |
| bin/console doctrine:database:create --if-not-exists | |
| bin/console doctrine:schema:create | |
| - name: PHPUnit — the dashboard is rendered, and a timer is drained by durable:worker | |
| working-directory: sylius | |
| env: | |
| APP_ENV: test | |
| run: vendor/bin/phpunit tests/Functional | |
| # #665: the shop serves stock and calls billing through its own workers, with the root's | |
| # demo harness on the other side: the root's vendor/ for the harness, and a start-dev | |
| # server, since serving Nexus needs one with Nexus on. | |
| - name: Composer install (root, for the demo harness) | |
| run: composer install --no-interaction --prefer-dist --no-progress | |
| - name: Start a Temporal dev server | |
| run: | | |
| docker run -d --name temporal-dev -p 7233:7233 temporalio/temporal:1.9.1 \ | |
| server start-dev --ip 0.0.0.0 --port 7233 --namespace durable-test --headless | |
| timeout 60 bash -c 'until nc -z 127.0.0.1 7233; do sleep 1; done' | |
| # Its own database, shared by the demo and demo_caller profiles as in demo/run.sh; the | |
| # test creates the schema and the stock it reserves. | |
| - name: PHPUnit — the shop serves and calls Nexus operations | |
| working-directory: sylius | |
| timeout-minutes: 10 | |
| env: | |
| DURABLE_TEMPORAL_DSN: 'temporal://127.0.0.1:7233?namespace=durable-test&nexus_task_queue=shop-bench-nexus&tls=0' | |
| DATABASE_URL: 'mysql://root@127.0.0.1/sylius_nexus?serverVersion=8&charset=utf8mb4' | |
| run: vendor/bin/phpunit --group nexus-bench --fail-on-skipped tests/Integration | |
| # #671: the admin page of a real run whose Nexus operation is in flight on this server. | |
| # The functional step above skips it, having no server yet; here it cannot skip. | |
| - name: PHPUnit — the run page shows a Nexus operation in flight | |
| working-directory: sylius | |
| timeout-minutes: 5 | |
| env: | |
| APP_ENV: test | |
| DURABLE_TEMPORAL_DSN: 'temporal://127.0.0.1:7233?namespace=durable-test&tls=0' | |
| run: vendor/bin/phpunit --fail-on-skipped tests/Functional/TheRunPageShowsANexusOperationInFlightTest.php | |
| magento-matrix: | |
| # The counterpart of the Symfony matrix, for the other foreign-container host. Same | |
| # principle: every entry is an **edge that has a reason**, not a cartesian product. What | |
| # the job proves is the resolution — that the module's constraint (`php >=8.2`, | |
| # `magento/framework ^103.0`) is honest against what each Mage-OS line requires. What it | |
| # does not prove is the boot: installing the distribution costs ~1 GB and several minutes | |
| # per entry, and that is the integration job's work, not a matrix's. | |
| # | |
| # The guard discriminates, verified locally before being written here: 2.2.0 on PHP 8.2 | |
| # resolves (code 0), 3.4.0 on PHP 8.2 fails naming the cause — "requires php ~8.3.0||~8.4.0|| | |
| # ~8.5.0 -> your php version (8.2.33) does not satisfy that requirement". | |
| name: Magento · package resolves (Mage-OS ${{ matrix.mageos }} / PHP ${{ matrix.php-version }}) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| # The oldest line that still accepts the module's PHP floor. | |
| # It puts `magento/framework: ^103.0` to the test from below. | |
| - { php-version: '8.2', mageos: '1.3.0' } | |
| # The bench's pin, at the PHP floor: the pair on which every measurement in | |
| # this change was made. | |
| - { php-version: '8.2', mageos: '2.2.0' } | |
| # Top of the 2 line under a recent PHP. | |
| - { php-version: '8.4', mageos: '2.3.0' } | |
| # The edge that bites: Mage-OS 3 refuses PHP 8.2 while the module allows it. | |
| # 8.3 is therefore this line's *reachable* floor, not the module's. | |
| - { php-version: '8.3', mageos: '3.4.0' } | |
| # The most recent on the most recent. | |
| - { php-version: '8.5', mageos: '3.4.0' } | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Setup PHP | |
| uses: shivammathur/setup-php@v2 | |
| with: | |
| php-version: ${{ matrix.php-version }} | |
| # What the metapackage demands of the host; `magento/check-php-extensions.sh` is | |
| # the reference list for it, kept up to date by the bench. | |
| extensions: bcmath, ctype, curl, dom, gd, iconv, intl, mbstring, openssl, pdo_mysql, simplexml, soap, sodium, xsl, zip | |
| coverage: none | |
| # A throwaway project rather than the bench: `magento/` carries a lock pinned to a | |
| # single line, and a matrix reusing it would test only that line. | |
| - name: Resolution project | |
| run: | | |
| mkdir -p /tmp/resolve | |
| cat > /tmp/resolve/composer.json <<JSON | |
| { | |
| "minimum-stability": "dev", | |
| "prefer-stable": true, | |
| "repositories": [ | |
| { "type": "composer", "url": "https://repo.mage-os.org/" }, | |
| { "type": "path", "url": "${{ github.workspace }}/src/DurableModule", "options": { "symlink": false } }, | |
| { "type": "path", "url": "${{ github.workspace }}/src/Durable", "options": { "symlink": false } }, | |
| { "type": "path", "url": "${{ github.workspace }}/src/GrpcClient", "options": { "symlink": false } }, | |
| { "type": "path", "url": "${{ github.workspace }}/src/Bridge/Temporal", "options": { "symlink": false } } | |
| ], | |
| "require": { | |
| "mage-os/product-community-edition": "${{ matrix.mageos }}", | |
| "gplanchat/durable-magento": "@dev" | |
| }, | |
| "config": { | |
| "allow-plugins": { | |
| "mage-os/composer-dependency-version-audit-plugin": false, | |
| "mage-os/magento-composer-installer": true, | |
| "mage-os/inventory-composer-installer": true, | |
| "laminas/laminas-dependency-plugin": true | |
| } | |
| } | |
| } | |
| JSON | |
| # `--dry-run`: the resolution is what we test, the download is not. | |
| # The path-repository audit plugin is disabled for the same reason as on the bench — | |
| # `gplanchat/durable` is both published and provided by path, and here the path is the | |
| # source of truth. | |
| - name: Does the Mage-OS line accept the module? | |
| working-directory: /tmp/resolve | |
| run: composer update --dry-run --no-interaction --no-audit --no-progress | |
| magento-without-temporal: | |
| # The module only *suggests* the Temporal bridge, so it has to install without it (#725). | |
| # The matrix only resolves, and the bench carries the bridge: this is the one job where a | |
| # bridge type reachable from a reflected constructor, or a preference the DI compiler | |
| # cannot see, fails the build. Services and install flags are magento-boot's. | |
| name: Magento · installs without the Temporal bridge (Mage-OS 2.2.0 / PHP 8.2) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 45 | |
| services: | |
| mysql: | |
| image: mysql:8.4 | |
| env: | |
| MYSQL_ROOT_PASSWORD: magento | |
| MYSQL_DATABASE: magento | |
| MYSQL_USER: magento | |
| MYSQL_PASSWORD: magento | |
| ports: | |
| - 3306:3306 | |
| options: >- | |
| --health-cmd="mysqladmin ping -h 127.0.0.1" | |
| --health-interval=5s --health-timeout=5s --health-retries=20 | |
| opensearch: | |
| image: opensearchproject/opensearch:2.12.0 | |
| env: | |
| discovery.type: single-node | |
| DISABLE_SECURITY_PLUGIN: 'true' | |
| OPENSEARCH_JAVA_OPTS: '-Xms512m -Xmx512m' | |
| ports: | |
| - 9200:9200 | |
| options: >- | |
| --health-cmd="curl -sf http://localhost:9200/_cluster/health || exit 1" | |
| --health-interval=10s --health-timeout=5s --health-retries=30 | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Setup PHP | |
| uses: shivammathur/setup-php@v2 | |
| with: | |
| php-version: '8.2' | |
| extensions: bcmath, ctype, curl, dom, gd, iconv, intl, mbstring, openssl, pdo_mysql, simplexml, soap, sodium, xsl, zip | |
| ini-values: memory_limit=-1 | |
| coverage: none | |
| # Composer's cache directory depends on the runner (#767): ask Composer instead of guessing. | |
| - name: Composer cache directory | |
| id: composer-cache | |
| run: echo "dir=$(composer config --global cache-dir)" >> "$GITHUB_OUTPUT" | |
| - name: Cache Composer | |
| uses: actions/cache@v6 | |
| with: | |
| path: ${{ steps.composer-cache.outputs.dir }} | |
| key: magento-no-temporal-${{ hashFiles('magento/composer.lock') }} | |
| restore-keys: magento-no-temporal- | |
| - name: MySQL accepts triggers | |
| run: mysql -h 127.0.0.1 -uroot -pmagento -e "SET GLOBAL log_bin_trust_function_creators = 1;" | |
| # A throwaway project on the bench's pin, with no path repository for | |
| # src/Bridge/Temporal: the bridge cannot be pulled in. | |
| - name: Project without the bridge | |
| run: | | |
| mkdir -p /tmp/no-temporal | |
| cat > /tmp/no-temporal/composer.json <<JSON | |
| { | |
| "minimum-stability": "dev", | |
| "prefer-stable": true, | |
| "repositories": [ | |
| { "type": "composer", "url": "https://repo.mage-os.org/" }, | |
| { "type": "path", "url": "${{ github.workspace }}/src/DurableModule", "options": { "symlink": false } }, | |
| { "type": "path", "url": "${{ github.workspace }}/src/Durable", "options": { "symlink": false } } | |
| ], | |
| "require": { | |
| "mage-os/product-community-edition": "2.2.0", | |
| "gplanchat/durable-magento": "@dev" | |
| }, | |
| "autoload": { | |
| "psr-4": { "Magento\\\\Setup\\\\": "setup/src/Magento/Setup/" }, | |
| "files": [ "app/etc/NonComposerComponentRegistration.php" ] | |
| }, | |
| "config": { | |
| "allow-plugins": { | |
| "mage-os/composer-dependency-version-audit-plugin": false, | |
| "mage-os/magento-composer-installer": true, | |
| "mage-os/inventory-composer-installer": true, | |
| "laminas/laminas-dependency-plugin": true | |
| } | |
| } | |
| } | |
| JSON | |
| # `--no-autoloader`: the autoload `files` entry comes with the skeleton, deployed below. | |
| - name: Composer install | |
| working-directory: /tmp/no-temporal | |
| run: composer install --no-interaction --prefer-dist --no-progress --no-autoloader | |
| # Same reason as magento-boot: the skeleton comes from mage-os/magento2-base. | |
| - name: Deploy the Magento skeleton | |
| working-directory: /tmp/no-temporal | |
| run: | | |
| cp -Rn vendor/mage-os/magento2-base/. . || true | |
| composer dump-autoload --no-interaction | |
| test -f bin/magento | |
| chmod +x bin/magento | |
| # `if`, not `! cmd`: bash's errexit ignores a negated command. | |
| - name: The bridge is absent | |
| working-directory: /tmp/no-temporal | |
| run: | | |
| if composer show gplanchat/durable-bridge-temporal >/dev/null 2>&1; then exit 1; fi | |
| test ! -e vendor/gplanchat/durable-bridge-temporal | |
| php -r 'require "vendor/autoload.php"; exit(class_exists("Gplanchat\\Bridge\\Temporal\\Http\\Psr18Http") ? 1 : 0);' | |
| - name: Magento install | |
| working-directory: /tmp/no-temporal | |
| run: | | |
| bin/magento setup:install \ | |
| --base-url=http://127.0.0.1:8080/ \ | |
| --db-host=127.0.0.1 --db-name=magento --db-user=magento --db-password=magento \ | |
| --admin-firstname=Durable --admin-lastname=Ops --admin-email=durable@example.com \ | |
| --admin-user=durable --admin-password='Durable123!' \ | |
| --language=en_US --currency=USD --timezone=UTC --use-rewrites=1 \ | |
| --search-engine=opensearch --opensearch-host=127.0.0.1 --opensearch-port=9200 \ | |
| --backend-frontname=admin --no-interaction | |
| bin/magento module:status Gplanchat_DurableModule | tee /tmp/status.txt | |
| grep -q 'Module is enabled' /tmp/status.txt | |
| - name: setup:di:compile | |
| working-directory: /tmp/no-temporal | |
| run: bin/magento setup:di:compile | |
| # No DSN, no bridge: the module runs in memory and says so. | |
| - name: The module answers in memory | |
| working-directory: /tmp/no-temporal | |
| run: bin/magento durable:health | |
| magento-boot: | |
| # The counterpart of "Sylius shop" for the other host: the job that | |
| # really **boots** Magento. It exists because until now every claim about this module | |
| # rested on a transcript produced by hand on a workstation's bench — and the Mage-OS × PHP | |
| # matrix, for its part, only proves the resolution. | |
| # | |
| # It is expensive: the distribution weighs ~1 GB and the install takes minutes. That is | |
| # why it is matrixed on PHP alone, over the one Mage-OS line pinned by the tracked lock of | |
| # `magento/`: the floor the bench was measured on, and 8.4, which a merchant on Magento | |
| # 2.4.8 runs and the lock accepts (Mage-OS 2.2.0 requires ~8.2.0||~8.3.0||~8.4.0). | |
| name: Magento · bench + dashboard (Mage-OS 2.2.0 / PHP ${{ matrix.php-version }}) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 60 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| php-version: ['8.2', '8.4'] | |
| services: | |
| mysql: | |
| image: mysql:8.4 | |
| env: | |
| MYSQL_ROOT_PASSWORD: magento | |
| MYSQL_DATABASE: magento | |
| MYSQL_USER: magento | |
| MYSQL_PASSWORD: magento | |
| ports: | |
| - 3306:3306 | |
| # `log_bin_trust_function_creators`: without it the install dies on | |
| # "You do not have the SUPER privilege … CREATE TRIGGER". The bench sets it the same way. | |
| options: >- | |
| --health-cmd="mysqladmin ping -h 127.0.0.1" | |
| --health-interval=5s --health-timeout=5s --health-retries=20 | |
| opensearch: | |
| image: opensearchproject/opensearch:2.12.0 | |
| env: | |
| discovery.type: single-node | |
| # The name 2.12 understands — the bench set it this way, and it is the setting | |
| # we know works. `plugins.security.disabled` is ignored here. | |
| DISABLE_SECURITY_PLUGIN: 'true' | |
| OPENSEARCH_JAVA_OPTS: '-Xms512m -Xmx512m' | |
| ports: | |
| - 9200:9200 | |
| options: >- | |
| --health-cmd="curl -sf http://localhost:9200/_cluster/health || exit 1" | |
| --health-interval=10s --health-timeout=5s --health-retries=30 | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Setup PHP | |
| uses: shivammathur/setup-php@v2 | |
| with: | |
| php-version: ${{ matrix.php-version }} | |
| extensions: bcmath, ctype, curl, dom, gd, iconv, intl, mbstring, openssl, pdo_mysql, simplexml, soap, sodium, xsl, zip | |
| ini-values: memory_limit=-1 | |
| coverage: none | |
| # Composer's cache directory depends on the runner (#767): ask Composer instead of guessing. | |
| - name: Composer cache directory | |
| id: composer-cache | |
| run: echo "dir=$(composer config --global cache-dir)" >> "$GITHUB_OUTPUT" | |
| # Without this cache, every execution re-downloads the distribution. | |
| - name: Cache Composer | |
| uses: actions/cache@v6 | |
| with: | |
| path: ${{ steps.composer-cache.outputs.dir }} | |
| key: magento-boot-${{ hashFiles('magento/composer.lock') }} | |
| restore-keys: magento-boot- | |
| - name: MySQL accepts triggers | |
| run: mysql -h 127.0.0.1 -uroot -pmagento -e "SET GLOBAL log_bin_trust_function_creators = 1;" | |
| # `magento/`'s `path` repositories read `../src`: the bench installs **this commit**, | |
| # not a published version. That is what makes this job able to say something about | |
| # the module. | |
| - name: Composer install (magento/) | |
| working-directory: magento | |
| run: composer install --no-interaction --prefer-dist --no-progress | |
| # The module's static analysis lives **here**, and not in the job that carries it for | |
| # the other packages. The reason is the one that made this job exist: the module talks | |
| # to classes the repository does not install, and the only machine that has them is | |
| # the one that has just paid for installing the distribution. The alternative — stubs | |
| # written by hand for the sixteen Magento classes the module touches — would not have | |
| # made an error: it would have hidden some, because an approximate stub makes the | |
| # analysis agree with itself. | |
| # | |
| # ⚠ `--ignore-platform-req=ext-grpc`: the Temporal bridge requires the extension | |
| # outright, and this job does not install it. Analysing does not need it — the `Grpc\` | |
| # classes come from the Composer package, and here the bridge is only *scanned*, never | |
| # run nor analysed. | |
| - name: Composer install (root, for the analysis tools) | |
| run: composer install --no-interaction --prefer-dist --no-progress --ignore-platform-req=ext-grpc | |
| - name: PHPStan (src/DurableModule, against the real Magento classes) | |
| run: composer phpstan-magento | |
| - name: Psalm (src/DurableModule, against the real Magento classes) | |
| run: composer psalm-magento | |
| # ⚠ The skeleton is not in the repository — `magento/.gitignore` keeps only the overlay | |
| # there, and the development workstation had obtained it once through `create-project`. | |
| # After a bare `composer install`, `bin/magento` therefore does not exist. | |
| # `mage-os/magento2-base` carries those files; we deploy them the way the installer | |
| # plugin would, without overwriting what the overlay has already laid down (`-n`). | |
| - name: Deploy the Magento skeleton | |
| working-directory: magento | |
| run: | | |
| cp -Rn vendor/mage-os/magento2-base/. . || true | |
| test -f bin/magento | |
| chmod +x bin/magento | |
| - name: Magento install | |
| working-directory: magento | |
| run: | | |
| bin/magento setup:install \ | |
| --base-url=http://127.0.0.1:8080/ \ | |
| --db-host=127.0.0.1 --db-name=magento --db-user=magento --db-password=magento \ | |
| --admin-firstname=Durable --admin-lastname=Ops --admin-email=durable@example.com \ | |
| --admin-user=durable --admin-password='Durable123!' \ | |
| --language=en_US --currency=USD --timezone=UTC --use-rewrites=1 \ | |
| --search-engine=opensearch --opensearch-host=127.0.0.1 --opensearch-port=9200 \ | |
| --backend-frontname=admin --no-interaction | |
| - name: The module is there | |
| working-directory: magento | |
| run: | | |
| # Two modules, and the distinction is the point: `Gplanchat_DurableModule` is the | |
| # published package — it declares no workflow. `Gplanchat_DurableProbe` is the | |
| # **bench**'s module, which carries the demonstration and the probes, and which will | |
| # never reach a user. It is the one that provides `durable:demo` to the next step. | |
| bin/magento module:enable Gplanchat_DurableModule Gplanchat_DurableProbe | |
| # The run page step below logs into the admin; two-factor auth would stop it at the | |
| # setup screen. Every enabled *TwoFactorAuth module goes, dependents included. | |
| tfa=$(bin/magento module:status --enabled | grep -E '_[A-Za-z]*TwoFactorAuth$' || true) | |
| if [ -n "$tfa" ]; then bin/magento module:disable $tfa; fi | |
| bin/magento setup:upgrade --keep-generated | |
| bin/magento module:status Gplanchat_DurableModule | tee /tmp/status.txt | |
| grep -q 'Module is enabled' /tmp/status.txt | |
| # The assertion worth the price of the job: a workflow written once runs inside | |
| # Magento, and its activities come from its contract's attributes, not copied strings. | |
| - name: A workflow runs inside it | |
| working-directory: magento | |
| run: | | |
| bin/magento durable:demo ORD-4242 | tee /tmp/demo.txt | |
| grep -q "notify:charge:ORD-4242" /tmp/demo.txt | |
| grep -q "durable.demo.charge" /tmp/demo.txt | |
| # Without a DSN, the screen must **say** why it is empty rather than look broken. | |
| - name: The admin screen responds | |
| working-directory: magento | |
| run: | | |
| php -S 127.0.0.1:8080 -t pub/ phpserver/router.php > /tmp/httpd.log 2>&1 & | |
| php -r 'usleep(5000000);' | |
| curl -sf --max-time 120 http://127.0.0.1:8080/admin -o /tmp/login.html | |
| grep -qi 'Magento Admin' /tmp/login.html | |
| # From here on, with a cluster: the admin finds a run by the execution id the | |
| # application started it with (#514), and that id may hold a slash. The steps above | |
| # stay as they are, since they assert the page without a DSN. | |
| - name: Install Temporal CLI | |
| uses: temporalio/setup-temporal@v0 | |
| # No search attribute is registered: `search_attributes` stays off in env.php, and the | |
| # execution id travels in the memo, which needs none. | |
| - name: Start a Temporal dev server | |
| timeout-minutes: 3 | |
| run: | | |
| temporal server start-dev --headless --port 7233 --namespace default --log-level warn > /tmp/temporal.log 2>&1 & | |
| timeout 60 bash -c 'until nc -z 127.0.0.1 7233; do sleep 1; done' | |
| timeout 60 bash -c 'until temporal workflow list --namespace default --limit 1 >/dev/null 2>&1; do sleep 1; done' | |
| # This job has no ext-grpc, hence gRPC over curl, named rather than left to `auto`. | |
| - name: Point Magento at the cluster | |
| working-directory: magento | |
| run: | | |
| php -r '$f = "app/etc/env.php"; $c = include $f; $c["durable"]["temporal"]["dsn"] = "temporal://127.0.0.1:7233?namespace=default&tls=0&transport=grpc-curl"; file_put_contents($f, "<?php\nreturn " . var_export($c, true) . ";\n");' | |
| bin/magento config:set admin/security/use_form_key 0 | |
| bin/magento config:set admin/captcha/enable 0 | |
| bin/magento cache:flush | |
| - name: Seed a run whose execution id holds a slash | |
| working-directory: magento | |
| timeout-minutes: 2 | |
| run: timeout 60 bin/magento durable:demo:start order/4242 | |
| # `findRun()` asks the cluster with DescribeWorkflowExecution, so the run is there as soon | |
| # as it is started: no worker, no visibility delay. The page title repeats the requested | |
| # id even when nothing is found, so the assertion reads what only a found run renders. | |
| - name: The admin opens the run by its execution id | |
| working-directory: magento | |
| timeout-minutes: 3 | |
| run: | | |
| nc -z 127.0.0.1 8080 || { php -S 127.0.0.1:8080 -t pub/ phpserver/router.php >> /tmp/httpd.log 2>&1 & timeout 30 bash -c 'until nc -z 127.0.0.1 8080; do sleep 1; done'; } | |
| jar=/tmp/admin.cookies | |
| # The login form posts to the page it is served on (`action=""`), so the URL the GET | |
| # ends on is where the credentials go. | |
| login_url=$(curl -sS -L --max-time 60 -c $jar -b $jar -o /tmp/login.html -w '%{url_effective}' http://127.0.0.1:8080/admin) | |
| form_key=$(grep -oE '<input[^>]*name="form_key"[^>]*>' /tmp/login.html | head -n1 | grep -oE 'value="[^"]+"' | sed -E 's/value="([^"]+)"/\1/' || true) | |
| action=$(grep -oE '<form[^>]*id="login-form"[^>]*' /tmp/login.html | grep -oE 'action="[^"]+"' | sed -E 's/action="([^"]+)"/\1/' || true) | |
| action=${action:-$login_url} | |
| if [ -z "$form_key" ]; then echo "No form_key on the login page ($login_url)." >&2; cat /tmp/login.html; tail -n 50 /tmp/httpd.log; exit 1; fi | |
| curl -s --max-time 60 -c $jar -b $jar -L -o /tmp/after-login.html \ | |
| --data-urlencode "form_key=$form_key" --data-urlencode 'login[username]=durable' --data-urlencode 'login[password]=Durable123!' \ | |
| "$action" | |
| if grep -q 'id="login-form"' /tmp/after-login.html; then echo 'The admin login failed.' >&2; cat /tmp/after-login.html; exit 1; fi | |
| curl -s --max-time 60 -c $jar -b $jar -o /tmp/run.html 'http://127.0.0.1:8080/admin/durable/process/view/?run_id=order%2F4242' | |
| if ! grep -q '<td>order/4242</td>' /tmp/run.html || ! grep -q 'Backend run' /tmp/run.html; then | |
| echo 'The run page did not render the run.' >&2 | |
| cat /tmp/run.html; tail -n 50 /tmp/httpd.log; tail -n 50 /tmp/temporal.log | |
| exit 1 | |
| fi | |
| # #667: Magento calls the three demo services through Nexus. The root's demo harness serves | |
| # them on the same dev server and namespace, over curl as everything in this job; the | |
| # bench's journal worker advances OrderNexusWorkflow. | |
| - name: The demo harness serves stock, billing and delivery | |
| timeout-minutes: 2 | |
| run: | | |
| php tests/integration/Temporal/nexus-demo-harness.php 127.0.0.1:7233 default --transport=grpc-curl > /tmp/harness.log 2>&1 & | |
| harness=$! | |
| echo "$harness" > /tmp/harness.pid | |
| # Fails at once if the harness dies before it is ready, rather than after the minute. | |
| timeout 60 bash -c "until grep -qx ready /tmp/harness.log; do kill -0 $harness 2>/dev/null || exit 1; sleep 1; done" || { cat /tmp/harness.log; exit 1; } | |
| # durable:demo:nexus exits non-zero unless the order was shipped; the order of the calls is | |
| # read from the run's history, not from the command's output. | |
| - name: Magento calls verify, schedule, reserve, then charge and ship | |
| working-directory: magento | |
| timeout-minutes: 5 | |
| run: | | |
| bin/magento durable:worker --role=journal > /tmp/magento-worker.log 2>&1 & | |
| order="MAG-NX-${GITHUB_RUN_ID}" | |
| if ! timeout 180 bin/magento durable:demo:nexus "$order" 1200 MUG_BLUE=1 --timeout=150; then | |
| tail -n 50 /tmp/magento-worker.log; tail -n 50 /tmp/harness.log; exit 1 | |
| fi | |
| calls=$(temporal workflow show --namespace default -w "durable-${order}" -o json \ | |
| | jq -r '[.events[] | select(.eventType == "EVENT_TYPE_NEXUS_OPERATION_SCHEDULED") | .nexusOperationScheduledEventAttributes | "\(.service)/\(.operation)"] | join(" ")') | |
| echo "calls: $calls" | |
| test "$calls" = 'billing/verify delivery/schedule stock/reserve billing/charge delivery/ship' \ | |
| || { tail -n 50 /tmp/magento-worker.log; tail -n 50 /tmp/harness.log; exit 1; } | |
| echo "$order" > /tmp/nexus-order | |
| # #672: the admin shows a run's Nexus operations. The harness stops (SIGTERM deletes its | |
| # endpoints), billing's endpoint comes back toward a queue nobody polls, and a new order's | |
| # verify stays in flight while the admin opens its page, then the shipped order's. | |
| - name: The admin shows a Nexus operation in flight, then settled ones | |
| working-directory: magento | |
| timeout-minutes: 3 | |
| run: | | |
| kill -TERM "$(cat /tmp/harness.pid)" || { echo "The #667 harness is already gone." >&2; cat /tmp/harness.log; exit 1; } | |
| timeout 30 bash -c 'while temporal operator nexus endpoint get --name demo-business-billing >/dev/null 2>&1; do sleep 1; done' | |
| temporal operator nexus endpoint create --name demo-business-billing --target-namespace default --target-task-queue nobody-polls | |
| held="MAG-INFLIGHT-${GITHUB_RUN_ID}" | |
| timeout 20 bin/magento durable:demo:nexus "$held" 1200 MUG_BLUE=1 --timeout=5 || true | |
| jar=/tmp/admin.cookies | |
| curl -s --max-time 60 -c $jar -b $jar -o /tmp/held.html "http://127.0.0.1:8080/admin/durable/process/view/?run_id=${held}" | |
| for shown in '<code>demo-business-billing</code>' '<td>billing</td>' '<td>verify</td>' 'in_flight">in flight<'; do | |
| grep -qF "$shown" /tmp/held.html || { echo "The run page lacks: $shown" >&2; cat /tmp/held.html; tail -n 50 /tmp/magento-worker.log; exit 1; } | |
| done | |
| curl -s --max-time 60 -c $jar -b $jar -o /tmp/shipped.html "http://127.0.0.1:8080/admin/durable/process/view/?run_id=$(cat /tmp/nexus-order)" | |
| test "$(grep -oF 'completed">completed<' /tmp/shipped.html | wc -l)" -eq 5 \ | |
| || { echo 'The shipped order does not show its five operations completed.' >&2; cat /tmp/shipped.html; exit 1; } | |
| # #669: now Magento serves. The probe module lists a billing handler in its di.xml and a | |
| # workflow fulfilling charge; a second, prefixed harness routes billing to the bench's | |
| # Nexus queue (the DSN names none, so durable-workflows) and calls it. The journal | |
| # worker of the step above runs the fulfilling workflow. | |
| - name: Magento serves billing to the demo harness | |
| working-directory: magento | |
| timeout-minutes: 5 | |
| run: | | |
| harness="php ../tests/integration/Temporal/nexus-demo-harness.php 127.0.0.1:7233 default --prefix=mg- --transport=grpc-curl" | |
| $harness --bench=billing@durable-workflows > /tmp/harness-mg.log 2>&1 & | |
| pid=$! | |
| timeout 60 bash -c "until grep -qx ready /tmp/harness-mg.log; do kill -0 $pid 2>/dev/null || exit 1; sleep 1; done" || { cat /tmp/harness-mg.log; exit 1; } | |
| bin/magento durable:worker --role=nexus > /tmp/magento-nexus.log 2>&1 & | |
| logs() { tail -n 50 /tmp/magento-nexus.log; tail -n 50 /tmp/magento-worker.log; tail -n 50 /tmp/harness-mg.log; } | |
| # Answers only the probe's handler and workflow give, not the harness's own. | |
| verify=$(timeout 120 $harness --call=billing/verify --input='{"order":"MAG-SV-1","amount":1200,"currency":"USD"}') || { logs; exit 1; } | |
| verify=$(tail -n 1 <<< "$verify") | |
| echo "verify: $verify" | |
| test "$(jq -c . <<< "$verify")" = '{"accepted":false,"reason":"the Magento bench bills in EUR, not USD"}' || { logs; exit 1; } | |
| charge=$(timeout 120 $harness --call=billing/charge --input='{"order":"MAG-SV-2","amount":1200,"currency":"EUR"}') || { logs; exit 1; } | |
| charge=$(tail -n 1 <<< "$charge") | |
| echo "charge: $charge" | |
| test "$(jq -c . <<< "$charge")" = '{"receipt":"MAGENTO-BENCH-EUR-MAG-SV-2","charged":1200}' || { logs; exit 1; } | |
| static-analysis: | |
| # Was the required status check on `main` until 2026-09-16; `ci-ok` below is now. Its steps | |
| # are what `loop/guardrails/verify.sh` mirrors: change one, change the other. | |
| name: Core · static analysis (PHPStan + Psalm) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Setup PHP | |
| uses: shivammathur/setup-php@v2 | |
| with: | |
| # grpc: the Temporal bridge now requires it outright, `composer install` | |
| # fails without it. | |
| php-version: '8.2' | |
| extensions: mbstring, sqlite3, pdo_sqlite, grpc | |
| coverage: none | |
| - name: Cache Composer downloads | |
| uses: actions/cache@v6 | |
| with: | |
| path: ~/.cache/composer | |
| key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock', '**/composer.json') }} | |
| restore-keys: composer-${{ runner.os }}- | |
| - name: Install Composer dependencies | |
| run: composer install --no-interaction --prefer-dist --no-progress | |
| - name: Tool versions (composer.lock) | |
| run: | | |
| composer show phpstan/phpstan --no-ansi | head -n 5 | |
| composer show vimeo/psalm --no-ansi | head -n 5 | |
| php vendor/bin/phpstan --version | |
| php vendor/bin/psalm --version | |
| - name: PHPStan (src/, tests/) | |
| run: composer phpstan | |
| - name: Psalm (src/, tests/) | |
| run: composer psalm | |
| symfony-sample: | |
| name: Symfony · bench, PHPUnit (${{ matrix.label }} / PHP ${{ matrix.php-version }}) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 45 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| php-version: ['8.2', '8.3', '8.4', '8.5'] | |
| symfony: ['7.4.*', '8.0.*'] | |
| # Symfony 8 requires PHP 8.4+: no 8.0 entry below 8.4. | |
| exclude: | |
| - php-version: '8.2' | |
| symfony: '8.0.*' | |
| - php-version: '8.3' | |
| symfony: '8.0.*' | |
| # Adds the job name's label to every combination of its line. | |
| include: | |
| - { symfony: '7.4.*', label: '7.4' } | |
| - { symfony: '8.0.*', label: '8.0' } | |
| env: | |
| SYMFONY_REQUIRE: ${{ matrix.symfony }} | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Setup PHP | |
| uses: shivammathur/setup-php@v2 | |
| with: | |
| php-version: ${{ matrix.php-version }} | |
| # ext-grpc: the Temporal bridge requires it outright, and the 8.0 leg resolves | |
| # (composer update) instead of installing from the lock — so the platform | |
| # constraint is checked there. | |
| extensions: mbstring, sqlite3, pdo_sqlite, grpc | |
| coverage: none | |
| # 7.4: install, so the versioned composer.lock stays exercised (and scanned by Dependabot). | |
| # 8.0: update, SYMFONY_REQUIRE only acting at resolution time, outside the lock. | |
| - name: Composer (symfony/, Symfony ${{ matrix.symfony }}) | |
| working-directory: symfony | |
| run: composer ${{ matrix.symfony == '7.4.*' && 'install' || 'update' }} --no-interaction --prefer-dist --no-progress | |
| - name: PHPUnit (symfony/) | |
| working-directory: symfony | |
| run: composer test | |
| conformance-sql: | |
| # The eight SQL conformance classes against real servers (#327). In `qa` they open in-memory | |
| # SQLite, which answers some questions differently: MySQL counts *changed* rows on an | |
| # UPDATE, and a re-link that SQLite accepted violated the primary key there. | |
| name: Core · SQL conformance (${{ matrix.database }}) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - { database: 'MySQL 8.4', dsn: 'mysql://root:root@127.0.0.1:3306/durable_test' } | |
| - { database: 'PostgreSQL 16', dsn: 'pgsql://postgres:root@127.0.0.1:5432/durable_test' } | |
| services: | |
| mysql: | |
| image: mysql:8.4 | |
| env: | |
| MYSQL_ROOT_PASSWORD: root | |
| MYSQL_DATABASE: durable_test | |
| ports: | |
| - 3306:3306 | |
| options: >- | |
| --health-cmd="mysqladmin ping -h 127.0.0.1 -proot" | |
| --health-interval=5s | |
| --health-timeout=5s | |
| --health-retries=20 | |
| postgres: | |
| image: postgres:16-alpine | |
| env: | |
| POSTGRES_PASSWORD: root | |
| POSTGRES_DB: durable_test | |
| ports: | |
| - 5432:5432 | |
| options: >- | |
| --health-cmd="pg_isready -U postgres" | |
| --health-interval=5s | |
| --health-timeout=5s | |
| --health-retries=20 | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Setup PHP | |
| uses: shivammathur/setup-php@v2 | |
| with: | |
| php-version: '8.4' | |
| extensions: mbstring, pdo_mysql, pdo_pgsql, grpc | |
| coverage: none | |
| - name: Cache Composer downloads | |
| uses: actions/cache@v6 | |
| with: | |
| path: ~/.cache/composer | |
| key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock', '**/composer.json') }} | |
| restore-keys: composer-${{ runner.os }}- | |
| - name: Install Composer dependencies | |
| run: composer install --no-interaction --prefer-dist --no-progress | |
| # --fail-on-skipped: a suite that quietly skips against the server proves nothing. | |
| - name: PHPUnit (the DBAL and Illuminate conformance classes) | |
| env: | |
| DURABLE_TEST_DSN: ${{ matrix.dsn }} | |
| run: vendor/bin/phpunit --fail-on-skipped --display-skipped --filter 'ConformanceTest' tests/unit/Bridge/Dbal tests/unit/Bridge/Illuminate | |
| guide-follows: | |
| # The bench that follows the guide (#363): a fresh `symfony/skeleton`, this commit's | |
| # packages, the getting-started blocks applied verbatim in both languages, and the first | |
| # workflow run to completion. #276 shipped a guide whose container did not compile; the | |
| # benches did not notice, because none of them is built from the guide. | |
| name: Docs · getting-started guide, followed verbatim | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Setup PHP | |
| uses: shivammathur/setup-php@v2 | |
| with: | |
| php-version: '8.4' | |
| coverage: none | |
| - name: Follow the guide | |
| run: bin/guide-follows.sh | |
| temporal-integration: | |
| name: Temporal · ext-grpc, bench, TLS | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 60 | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Setup PHP (ext-grpc required) | |
| uses: shivammathur/setup-php@v2 | |
| with: | |
| php-version: '8.2' | |
| extensions: mbstring, sqlite3, pdo_sqlite, grpc | |
| coverage: none | |
| - name: Cache Composer downloads | |
| uses: actions/cache@v6 | |
| with: | |
| path: ~/.cache/composer | |
| key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock', '**/composer.json') }} | |
| restore-keys: composer-${{ runner.os }}- | |
| # The root `integration` suite (#344): ninety tests that skipped in every job because | |
| # none set DURABLE_TEMPORAL_ADDRESS. They expect `temporal server start-dev`, not the | |
| # auto-setup image below: Nexus enabled, a `durable-test` namespace, two custom search | |
| # attributes and the HTTP gateway. Its own ports, so it can run beside compose. | |
| - name: Install Composer dependencies (root) | |
| run: composer install --no-interaction --prefer-dist --no-progress | |
| - name: Start a Temporal dev server for the root suite | |
| run: | | |
| docker run -d --name temporal-dev -p 7333:7233 -p 7343:7243 temporalio/temporal:1.9.1 \ | |
| server start-dev --ip 0.0.0.0 --port 7233 --http-port 7243 --namespace durable-test \ | |
| --search-attribute DurableOrderId=Keyword --search-attribute DurableAmount=Int --search-attribute DurablePrice=Double \ | |
| --search-attribute DurableWorkflowName=Keyword --search-attribute DurableExecutionId=Keyword | |
| timeout 60 bash -c 'until nc -z 127.0.0.1 7333; do sleep 1; done' | |
| # --fail-on-skipped: a missing variable or server turns the job red instead of green. | |
| # `temporal` only: the server-free `integration` suite already runs in `qa`, on every PHP. | |
| # This is the ext-grpc transport's run; `temporal-transport` covers grpc-curl and Guzzle. | |
| - name: PHPUnit — root Temporal suite (ext-grpc) | |
| env: | |
| DURABLE_TEMPORAL_ADDRESS: '127.0.0.1:7333' | |
| DURABLE_TEMPORAL_HTTP_ADDRESS: '127.0.0.1:7343' | |
| run: vendor/bin/phpunit --testsuite temporal --fail-on-skipped --display-skipped | |
| - name: Install Composer dependencies (symfony/) | |
| working-directory: symfony | |
| run: composer install --no-interaction --prefer-dist --no-progress | |
| # #664: the bench serves billing and calls stock, through its own workers, with the root's | |
| # demo harness on the other side. On this dev server, not the compose stack: its | |
| # auto-setup 1.25.2 answers "Nexus APIs are disabled". durable-test has the search | |
| # attributes the bench's dev env writes on every start. | |
| - name: PHPUnit — the bench serves and calls Nexus operations | |
| working-directory: symfony | |
| timeout-minutes: 10 | |
| env: | |
| DURABLE_DSN: 'temporal://127.0.0.1:7333?namespace=durable-test&nexus_task_queue=symfony-bench-nexus&tls=0' | |
| run: php bin/phpunit --testdox --fail-on-skipped --display-skipped --group nexus-bench tests/Integration/Temporal/ | |
| - name: Stop the dev server | |
| if: always() | |
| run: docker rm -f temporal-dev | |
| # `symfony/.env` publishes the frontend on 7234, so it can sit next to a local | |
| # Temporal on 7233. In CI there is nothing to sit next to: we publish on 7233, | |
| # the port the DSN and the tests' docblocks announce. Without this steering, | |
| # compose listened on 7234, the DSN aimed at 7233, and the nine tests that talk | |
| # to the server were skipped — the job stayed green while testing nothing. | |
| - name: Validate compose.yaml | |
| working-directory: symfony | |
| run: docker compose config --quiet | |
| # temporal-ui rides along: the stack the docs tell users to start, UI included. | |
| - name: Start PostgreSQL + Temporal + UI (symfony/compose.yaml) | |
| working-directory: symfony | |
| env: | |
| TEMPORAL_FRONTEND_PORT: '7233' | |
| run: docker compose up -d --wait --wait-timeout 300 database temporal temporal-ui | |
| - name: The frontend responds, otherwise we test nothing | |
| run: timeout 30 bash -c 'until nc -z 127.0.0.1 7233; do sleep 1; done' | |
| - name: PHPUnit — Temporal integration tests (the whole directory) | |
| working-directory: symfony | |
| env: | |
| DURABLE_DSN: 'temporal://127.0.0.1:7233?namespace=default&journal_task_queue=durable-journal&activity_task_queue=durable-activities&tls=0' | |
| run: | | |
| php bin/phpunit \ | |
| --testdox \ | |
| --fail-on-skipped \ | |
| --display-skipped \ | |
| --exclude-group nexus-bench \ | |
| tests/Integration/Temporal/ | |
| - name: Check Temporal UI (internal exec) | |
| working-directory: symfony | |
| run: | | |
| set -e | |
| for i in $(seq 1 20); do | |
| if docker compose exec -T temporal-ui wget -qO- http://localhost:8080/ > /dev/null 2>&1; then | |
| echo "Temporal UI OK (attempt $i)" | |
| exit 0 | |
| fi | |
| sleep 5 | |
| done | |
| docker compose logs temporal-ui --tail 40 | |
| exit 1 | |
| # #353: the DSN's TLS options (#516) against a server that demands them. `start-dev` serves | |
| # plain gRPC only, so nginx terminates TLS in front of it: it presents a certificate only | |
| # `ca=` verifies, requires the client certificate `cert=`/`key=`, and answers 401 without | |
| # the `api_key=` bearer. A green run proves the client sent all four. The CA, the | |
| # certificates and the key are throwaway: generated here, valid one day, never stored. | |
| - name: TLS — throwaway CA, server and client certificates | |
| run: | | |
| set -euo pipefail | |
| d="$RUNNER_TEMP/durable-tls"; mkdir -p "$d"; cd "$d" | |
| openssl req -x509 -newkey rsa:2048 -nodes -days 1 -subj '/CN=durable-ci-ca' -keyout ca.key -out ca.crt | |
| openssl req -newkey rsa:2048 -nodes -subj '/CN=localhost' -keyout server.key -out server.csr | |
| printf 'subjectAltName=DNS:localhost,IP:127.0.0.1\nextendedKeyUsage=serverAuth\n' > server.ext | |
| openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -days 1 -extfile server.ext -out server.crt | |
| openssl req -newkey rsa:2048 -nodes -subj '/CN=durable-ci-client' -keyout client.key -out client.csr | |
| printf 'extendedKeyUsage=clientAuth\n' > client.ext | |
| openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -days 1 -extfile client.ext -out client.crt | |
| chmod 644 ./*.key | |
| - name: TLS — a dev server behind an nginx gRPC terminator (mTLS + API key) | |
| run: | | |
| set -euo pipefail | |
| d="$RUNNER_TEMP/durable-tls" | |
| cat > "$d/nginx.conf" <<'NGINX' | |
| events {} | |
| http { | |
| server { | |
| listen 7443 ssl; | |
| http2 on; | |
| ssl_certificate /tls/server.crt; | |
| ssl_certificate_key /tls/server.key; | |
| ssl_client_certificate /tls/ca.crt; | |
| ssl_verify_client on; | |
| grpc_read_timeout 120s; | |
| grpc_send_timeout 120s; | |
| location / { | |
| if ($http_authorization != "Bearer durable-ci-throwaway-key") { | |
| return 401; | |
| } | |
| grpc_pass grpc://temporal-tls-dev:7233; | |
| } | |
| } | |
| } | |
| NGINX | |
| docker network create durable-tls | |
| docker run -d --name temporal-tls-dev --network durable-tls temporalio/temporal:1.9.1 \ | |
| server start-dev --ip 0.0.0.0 --port 7233 --headless | |
| docker run -d --name temporal-tls-proxy --network durable-tls -p 7443:7443 \ | |
| -v "$d:/tls:ro" -v "$d/nginx.conf:/etc/nginx/nginx.conf:ro" nginx:1.27-alpine | |
| timeout 60 bash -c 'until docker exec temporal-tls-dev temporal operator namespace describe default >/dev/null 2>&1; do sleep 1; done' | |
| timeout 30 bash -c 'until nc -z 127.0.0.1 7443; do sleep 1; done' | |
| - name: PHPUnit — the Temporal integration tests through a TLS DSN | |
| working-directory: symfony | |
| run: | | |
| d="$RUNNER_TEMP/durable-tls" | |
| export DURABLE_DSN="temporal://localhost:7443?namespace=default&journal_task_queue=durable-journal&activity_task_queue=durable-activities&tls=1&ca=$d/ca.crt&cert=$d/client.crt&key=$d/client.key&api_key=durable-ci-throwaway-key" | |
| php bin/phpunit --testdox --fail-on-skipped --display-skipped --exclude-group nexus-bench tests/Integration/Temporal/ | |
| - name: Stop the TLS server | |
| if: always() | |
| run: | | |
| docker logs --tail 40 temporal-tls-proxy || true | |
| docker rm -f temporal-tls-proxy temporal-tls-dev || true | |
| docker network rm durable-tls || true | |
| - name: Stop the containers | |
| if: always() | |
| working-directory: symfony | |
| run: docker compose down -v | |
| temporal-oldest-server: | |
| # The oldest Temporal server Durable supports (#523): 1.20, the first whose SQL visibility | |
| # takes custom search attributes. Every other Temporal job runs a current server (the CLI | |
| # dev server embeds 1.32), and a query only a newer server accepts passed them all (#508). | |
| # On PostgreSQL, 1.20 needs the postgres12 plugin: the legacy `postgresql` one only offers | |
| # standard visibility, which cannot filter on custom search attributes. | |
| name: Temporal · oldest server (1.20) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 40 | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Setup PHP (ext-grpc required) | |
| uses: shivammathur/setup-php@v2 | |
| with: | |
| php-version: '8.2' | |
| extensions: mbstring, sqlite3, pdo_sqlite, grpc | |
| coverage: none | |
| - name: Cache Composer downloads | |
| uses: actions/cache@v6 | |
| with: | |
| path: ~/.cache/composer | |
| key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock', '**/composer.json') }} | |
| restore-keys: composer-${{ runner.os }}- | |
| - name: Install Composer dependencies | |
| run: composer install --no-interaction --prefer-dist --no-progress | |
| - name: Start PostgreSQL and Temporal 1.20 | |
| run: | | |
| docker network create oldest | |
| docker run -d --name oldest-pg --network oldest -e POSTGRES_USER=temporal -e POSTGRES_PASSWORD=temporal postgres:13 | |
| timeout 60 bash -c 'until docker exec oldest-pg pg_isready -U temporal >/dev/null 2>&1; do sleep 1; done' | |
| # Every test registers a fresh namespace, and 1.20's operator service and visibility | |
| # learn of it on the namespace cache's refresh, every 10 s by default: two refreshes | |
| # per test, 18 of this job's 19 minutes. 1 s is not the server's default: it only | |
| # shortens setup. The tests keep their waits and assert the same things; the lag is | |
| # still there, just 1 s instead of 10. | |
| printf 'system.namespaceCacheRefreshInterval:\n - value: 1s\n' > "$RUNNER_TEMP/temporal-dc.yaml" | |
| docker run -d --name oldest-temporal --network oldest -p 7233:7233 \ | |
| -e DB=postgres12 -e DB_PORT=5432 -e POSTGRES_USER=temporal -e POSTGRES_PWD=temporal -e POSTGRES_SEEDS=oldest-pg \ | |
| -e DYNAMIC_CONFIG_FILE_PATH=/etc/temporal/config/dynamicconfig/ci.yaml \ | |
| -v "$RUNNER_TEMP/temporal-dc.yaml:/etc/temporal/config/dynamicconfig/ci.yaml:ro" \ | |
| temporalio/auto-setup:1.20 | |
| # Probe the server, not a log line: 1.20 does not always print "Search attributes have been | |
| # added" (#650). The attributes Durable needs are registered, with a retry, by the next step. | |
| timeout 300 bash -c 'until docker exec oldest-temporal tctl --address oldest-temporal:7233 cluster health 2>/dev/null | grep -q SERVING; do sleep 2; done' | |
| # SearchAttributesTest runs in `durable-test`, which the dev server of the other jobs | |
| # creates at start. tctl, because the `temporal` CLI shipped in this image ignores | |
| # --address on its operator commands. 1.20 keeps these attributes cluster-wide, and | |
| # its operator service learns of a new namespace late: hence the retry. | |
| - name: The durable-test namespace and its search attributes | |
| run: | | |
| TCTL="docker exec oldest-temporal tctl --address oldest-temporal:7233 --namespace durable-test" | |
| # Register until describe answers: a server that just turned SERVING may refuse the first try. | |
| timeout 60 bash -c "until $TCTL namespace describe >/dev/null 2>&1; do $TCTL namespace register >/dev/null 2>&1 || true; sleep 1; done" | |
| timeout 90 bash -c "until $TCTL admin cluster get-search-attributes 2>/dev/null | grep -q DurableAmount; do | |
| docker exec oldest-temporal sh -c 'yes | tctl --address oldest-temporal:7233 --namespace durable-test admin cluster add-search-attributes --name DurableOrderId --type Keyword --name DurableAmount --type Int --name DurablePrice --type Double --name DurableWorkflowName --type Keyword --name DurableExecutionId --type Keyword' >/dev/null 2>&1 || true | |
| sleep 2 | |
| done" | |
| # --fail-on-skipped: a missing variable or server turns the job red instead of green. | |
| # No WorkflowUpdateTest here: updates need Server 1.21 (#860). 1.20 drops the update's | |
| # events when the same workflow task completes the workflow. | |
| - name: PHPUnit — the run catalogue and the visibility queries on 1.20 | |
| env: | |
| DURABLE_TEMPORAL_ADDRESS: '127.0.0.1:7233' | |
| run: >- | |
| vendor/bin/phpunit --fail-on-skipped --display-skipped | |
| tests/integration/Temporal/TemporalWorkflowRunCatalogConformanceTest.php | |
| tests/integration/Temporal/TemporalWorkflowRunCatalogWithoutSearchAttributesConformanceTest.php | |
| tests/integration/Temporal/TemporalRunCatalogFailedFilterTest.php | |
| tests/integration/Temporal/SearchAttributesTest.php | |
| tests/integration/Temporal/ContinueAsNewKeepsTheExecutionIdTest.php | |
| - name: Server logs | |
| if: failure() | |
| run: docker logs oldest-temporal 2>&1 | tail -200 | |
| - name: Stop the containers | |
| if: always() | |
| run: docker rm -f oldest-temporal oldest-pg | |
| temporal-transport: | |
| # The Temporal suite over the two transports `temporal-integration` does not take: gRPC over | |
| # curl without ext-grpc, and Guzzle (GuzzleGrpcTransport) with it. The ext-grpc transport | |
| # itself is `temporal-integration`'s, which runs the same suite over it. | |
| # | |
| # What the curl leg proves and no other job can: the Temporal bridge loads, installs and | |
| # talks to the server **without** `ext-grpc`. Every other job installs the extension; without | |
| # this leg, a regression that put `\Grpc\BaseStub` back on a collaborator's path would stay | |
| # invisible until a user's first `composer install`. The tests that need the extension | |
| # (`RequiresPhpExtension('grpc')`: Nexus, the message batch) skip there, hence no | |
| # `--fail-on-skipped` on that leg. | |
| name: Temporal · ${{ matrix.label }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - { transport: 'grpc-curl', label: 'grpc-curl, no ext-grpc', grpc: false, php-version: '8.4', extensions: 'mbstring, bcmath, sqlite3, pdo_sqlite, curl' } | |
| - { transport: 'guzzle', label: 'Guzzle', grpc: true, php-version: '8.2', extensions: 'mbstring, bcmath, sqlite3, pdo_sqlite, curl, grpc' } | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Setup PHP | |
| uses: shivammathur/setup-php@v2 | |
| with: | |
| php-version: ${{ matrix.php-version }} | |
| extensions: ${{ matrix.extensions }} | |
| coverage: none | |
| - name: ext-grpc is absent, otherwise this leg measures nothing | |
| if: ${{ !matrix.grpc }} | |
| run: | | |
| if php -m | grep -qi '^grpc$'; then echo 'ext-grpc is loaded; this leg must run without it.' >&2; exit 1; fi | |
| - name: Cache Composer downloads | |
| uses: actions/cache@v6 | |
| with: | |
| path: ~/.cache/composer | |
| key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock', '**/composer.json') }} | |
| restore-keys: composer-${{ runner.os }}- | |
| - name: Install Composer dependencies | |
| run: composer install --no-interaction --prefer-dist --no-progress | |
| - name: PHPUnit — unit suite without ext-grpc | |
| if: ${{ !matrix.grpc }} | |
| run: vendor/bin/phpunit --testsuite unit | |
| # The same pinned server as `temporal-integration`, search attributes registered at start. | |
| # Durable writes DurableWorkflowName and DurableExecutionId on every start (#558): the probe | |
| # waits until a query on one of them is accepted. | |
| - name: Start a Temporal dev server (gRPC 7233, HTTP 7243) | |
| run: | | |
| docker run -d --name temporal-dev -p 7233:7233 -p 7243:7243 temporalio/temporal:1.9.1 \ | |
| server start-dev --ip 0.0.0.0 --port 7233 --http-port 7243 --namespace durable-test --headless \ | |
| --search-attribute DurableOrderId=Keyword --search-attribute DurableAmount=Int --search-attribute DurablePrice=Double \ | |
| --search-attribute DurableWorkflowName=Keyword --search-attribute DurableExecutionId=Keyword | |
| timeout 60 bash -c 'until nc -z 127.0.0.1 7233 && nc -z 127.0.0.1 7243; do sleep 1; done' | |
| timeout 30 bash -c "until docker exec temporal-dev temporal workflow list --namespace durable-test --limit 1 --query \"DurableExecutionId = 'probe'\" >/dev/null 2>&1; do sleep 1; done" | |
| - name: PHPUnit — Temporal suite over ${{ matrix.transport }} | |
| env: | |
| DURABLE_TEMPORAL_ADDRESS: '127.0.0.1:7233' | |
| DURABLE_TEMPORAL_HTTP_ADDRESS: '127.0.0.1:7243' | |
| DURABLE_TEMPORAL_TRANSPORT: ${{ matrix.transport }} | |
| run: vendor/bin/phpunit --testsuite temporal --display-skipped ${{ matrix.grpc && '--fail-on-skipped' || '' }} | |
| - name: PHPUnit — JSON gateway over a PSR-18 client (Guzzle) | |
| if: ${{ !matrix.grpc }} | |
| env: | |
| DURABLE_TEMPORAL_HTTP_ADDRESS: '127.0.0.1:7243' | |
| DURABLE_TEMPORAL_HTTP_CLIENT: 'psr18' | |
| run: vendor/bin/phpunit tests/integration/Temporal/Http/JsonGatewayRoundTripTest.php --display-skipped --fail-on-skipped | |
| - name: Stop the dev server | |
| if: always() | |
| run: docker rm -f temporal-dev | |
| manifests: | |
| # Every package declares what it imports (#346): a split package installs alone, and an | |
| # import the monorepo happens to provide is a fatal error there. | |
| name: Core · manifests (validate + require-checker) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Setup PHP | |
| uses: shivammathur/setup-php@v2 | |
| with: | |
| # 8.4: composer-require-checker 4.24 requires it. | |
| php-version: '8.4' | |
| extensions: mbstring, sqlite3, pdo_sqlite, grpc | |
| coverage: none | |
| # The root lock against the path packages' manifests (#640). No vendor needed, so it | |
| # runs before the install and fails fast; its test runs on throwaway fixtures. | |
| - name: The root composer.lock matches the path packages | |
| run: bin/check-root-lock.sh && bin/check-root-lock-test.sh | |
| # setup-php's `tools:` input fails on it without saying why: the release phar, pinned. | |
| - name: Install composer-require-checker 4.24.0 | |
| run: | | |
| phar="$RUNNER_TEMP/composer-require-checker" | |
| curl -sSfL -o "$phar" \ | |
| https://github.com/maglnet/ComposerRequireChecker/releases/download/4.24.0/composer-require-checker.phar | |
| echo "35d1dfdd7aa94a750f4b4c9b0b22dd5e76b5e9e35fb910c8901f3dafd2ad2ab9 $phar" | sha256sum -c | |
| chmod +x "$phar" | |
| echo "COMPOSER_REQUIRE_CHECKER=$phar" >> "$GITHUB_ENV" | |
| - name: Cache Composer downloads | |
| uses: actions/cache@v6 | |
| with: | |
| path: ~/.cache/composer | |
| key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock', '**/composer.json') }} | |
| restore-keys: composer-${{ runner.os }}- | |
| - name: Install Composer dependencies | |
| run: composer install --no-interaction --prefer-dist --no-progress | |
| - name: Validate manifests and check imports | |
| run: bin/check-manifests.sh | |
| ci-ok: | |
| # The one required status check on `main` (branch protection, 2026-09-16). It needs every | |
| # job above, so a red matrix leg blocks a merge the way a red QA leg used to; the individual | |
| # job names can move without touching the protection. A skipped job is not a failure. | |
| name: ci-ok | |
| if: always() | |
| needs: [qa, symfony-matrix, laravel, laravel-bench, filament, sylius-shop, magento-matrix, magento-without-temporal, magento-boot, static-analysis, symfony-sample, conformance-sql, guide-follows, temporal-integration, temporal-oldest-server, temporal-transport, manifests] | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Every job succeeded or was skipped | |
| env: | |
| RESULTS: ${{ toJSON(needs) }} | |
| run: | | |
| echo "$RESULTS" | jq -r 'to_entries[] | "\(.key): \(.value.result)"' | |
| echo "$RESULTS" | jq -e 'all(.[]; .result == "success" or .result == "skipped")' >/dev/null |